diff --git a/firmware/application/ui_external_items_menu_loader.cpp b/firmware/application/ui_external_items_menu_loader.cpp index 2948fcd2..f5b453d7 100644 --- a/firmware/application/ui_external_items_menu_loader.cpp +++ b/firmware/application/ui_external_items_menu_loader.cpp @@ -6,6 +6,44 @@ namespace ui { /* static */ std::vector> ExternalItemsMenuLoader::bitmaps; +// iterates over all ppma-s, and if it is runnable on the current system, it'll call the callback, and pass info. +/* static */ void ExternalItemsMenuLoader::load_all_external_items_callback(std::function callback) { + if (!callback) return; + if (sd_card::status() != sd_card::Status::Mounted) + return; + for (const auto& entry : std::filesystem::directory_iterator(u"APPS", u"*.ppma")) { + auto filePath = u"/APPS/" + entry.path(); + File app; + + auto openError = app.open(filePath); + if (openError) + continue; + + application_information_t application_information = {}; + + auto readResult = app.read(&application_information, sizeof(application_information_t)); + if (!readResult) + continue; + + if (application_information.header_version != CURRENT_HEADER_VERSION) + continue; + + bool versionMatches = VERSION_MD5 == application_information.app_version; + if (!versionMatches) continue; + // here the app is startable and good. + std::string appshortname = filePath.filename().string(); + if (appshortname.size() >= 5 && appshortname.substr(appshortname.size() - 5) == ".ppma") { + // Remove the ".ppma" suffix + appshortname = appshortname.substr(0, appshortname.size() - 5); + } + AppInfoConsole info{ + .appCallName = appshortname.c_str(), + .appFriendlyName = reinterpret_cast(&application_information.app_name[0]), + .appLocation = application_information.menu_location}; + callback(info); + } +} + /* static */ std::vector ExternalItemsMenuLoader::load_external_items(app_location_t app_location, NavigationView& nav) { bitmaps.clear(); @@ -47,7 +85,9 @@ namespace ui { bitmaps.push_back(std::move(dyn_bmp)); gridItem.on_select = [&nav, app_location, filePath]() { - run_external_app(nav, filePath); + if (!run_external_app(nav, filePath)) { + nav.display_modal("Error", "The .ppma file in your APPS\nfolder can't be read. Please\nupdate your SD Card content."); + } }; } else { gridItem.color = Color::light_grey(); @@ -65,19 +105,19 @@ namespace ui { return external_apps; } -/* static */ void ExternalItemsMenuLoader::run_external_app(ui::NavigationView& nav, std::filesystem::path filePath) { +/* static */ bool ExternalItemsMenuLoader::run_external_app(ui::NavigationView& nav, std::filesystem::path filePath) { File app; auto openError = app.open(filePath); if (openError) - chDbgPanic("file gone"); + return false; application_information_t application_information = {}; auto readResult = app.read(&application_information, sizeof(application_information_t)); if (!readResult) - chDbgPanic("no data"); + return false; app.seek(0); @@ -93,7 +133,7 @@ namespace ui { readResult = app.read(&application_information.memory_location[file_read_index], bytes_to_read); if (!readResult) - chDbgPanic("read error"); + return false; if (readResult.value() < std::filesystem::max_file_block_size) break; @@ -114,7 +154,7 @@ namespace ui { readResult = app.read(target_memory, bytes_to_read); if (!readResult) - chDbgPanic("read error #2"); + return false; if (readResult.value() != bytes_to_read) break; @@ -126,7 +166,7 @@ namespace ui { readResult = app.read(&application_information.memory_location[file_read_index], bytes_to_read); if (!readResult) - chDbgPanic("read error #3"); + return false; if (readResult.value() < std::filesystem::max_file_block_size) break; @@ -134,6 +174,7 @@ namespace ui { } application_information.externalAppEntry(nav); + return true; } } // namespace ui diff --git a/firmware/application/ui_external_items_menu_loader.hpp b/firmware/application/ui_external_items_menu_loader.hpp index 288a536f..b4345b9e 100644 --- a/firmware/application/ui_external_items_menu_loader.hpp +++ b/firmware/application/ui_external_items_menu_loader.hpp @@ -54,11 +54,11 @@ class ExternalItemsMenuLoader { public: static std::vector load_external_items(app_location_t, NavigationView&); ExternalItemsMenuLoader() = delete; + static bool run_external_app(ui::NavigationView&, std::filesystem::path); + static void load_all_external_items_callback(std::function callback); private: static std::vector> bitmaps; - - static void run_external_app(ui::NavigationView&, std::filesystem::path); }; } // namespace ui diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index dde11278..66808b86 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -111,6 +111,169 @@ namespace pmem = portapack::persistent_memory; namespace ui { +// When adding or removing apps from the Menu, please update it here: +std::vector NavigationView::fixedAppListFC = { + {"adsbrx", "ADS-B", RX}, + {"ais", "AIS Boats", RX}, + {"aprsrx", "APRS", RX}, + {"audio", "Audio", RX}, + {"blerx", "BLE Rx", RX}, + {"ert", "ERT Meter", RX}, + {"level", "Level", RX}, + {"pocsagrx", "POCSAG", RX}, + {"radiosnde", "Radiosnde", RX}, + {"recon", "Recon", RX}, + {"search", "Search", RX}, + {"tpms", "TPMS Cars", RX}, + {"weather", "Weather", RX}, + {"subghzd", "SubGhzD", RX}, + {"adsbtx", "ADS-B", TX}, + {"aprstx", "APRS TX", TX}, + {"bht", "BHT Xy/EP", TX}, + {"bletx", "BLE Tx", TX}, + {"morsetx", "Morse", TX}, + {"ooktx", "OOK", TX}, + {"pocsatx", "POCSAG TX", TX}, + {"rdstx", "RDS TX", TX}, + {"soundbrd", "Soundbrd", TX}, + {"sstvtx", "SSTV", TX}, + {"touchtune", "TouchTune", TX}, + {"capture", "Capture", RX}, + {"replay", "Replay", TX}, + {"remote", "Remote", TX}, + {"scanner", "Scanner", RX}, + {"microphone", "Microphone", TX}, + {"lookingglass", "Looking Glass", RX}}; + +bool NavigationView::StartAppByName(const char* name) { + pop(); + if (strcmp(name, "adsbrx") == 0) { + push(); + return true; + } + if (strcmp(name, "ais") == 0) { + push(); + return true; + } + if (strcmp(name, "aprsrx") == 0) { + push(); + return true; + } + if (strcmp(name, "audio") == 0) { + push(); + return true; + } + if (strcmp(name, "blerx") == 0) { + push(); + return true; + } + if (strcmp(name, "ert") == 0) { + push(); + return true; + } + if (strcmp(name, "level") == 0) { + push(); + return true; + } + if (strcmp(name, "pocsagrx") == 0) { + push(); + return true; + } + if (strcmp(name, "radiosnode") == 0) { + push(); + return true; + } + if (strcmp(name, "recon") == 0) { + push(); + return true; + } + if (strcmp(name, "search") == 0) { + push(); + return true; + } + if (strcmp(name, "tpms") == 0) { + push(); + return true; + } + if (strcmp(name, "weather") == 0) { + push(); + return true; + } + if (strcmp(name, "subghzd") == 0) { + push(); + return true; + } + if (strcmp(name, "adsbtx") == 0) { + push(); + return true; + } + if (strcmp(name, "aprstx") == 0) { + push(); + return true; + } + if (strcmp(name, "bht") == 0) { + push(); + return true; + } + if (strcmp(name, "bletx") == 0) { + push(); + return true; + } + if (strcmp(name, "morsetx") == 0) { + push(); + return true; + } + if (strcmp(name, "ooktx") == 0) { + push(); + return true; + } + if (strcmp(name, "pocsatx") == 0) { + push(); + return true; + } + if (strcmp(name, "rdstx") == 0) { + push(); + return true; + } + if (strcmp(name, "soundbrd") == 0) { + push(); + return true; + } + if (strcmp(name, "sstvtx") == 0) { + push(); + return true; + } + if (strcmp(name, "touchtune") == 0) { + push(); + return true; + } + if (strcmp(name, "capture") == 0) { + push(); + return true; + } + if (strcmp(name, "replay") == 0) { + push(); + return true; + } + if (strcmp(name, "remote") == 0) { + push(); + return true; + } + if (strcmp(name, "scanner") == 0) { + push(); + return true; + } + if (strcmp(name, "microphone") == 0) { + push(); + return true; + } + if (strcmp(name, "lookingglass") == 0) { + push(); + return true; + } + return false; +} + /* StatusTray ************************************************************/ StatusTray::StatusTray(Point pos) @@ -751,6 +914,9 @@ SystemView::SystemView( Context& SystemView::context() const { return context_; } +NavigationView* SystemView::get_navigation_view() { + return &navigation_view; +} void SystemView::toggle_overlay() { switch (++overlay_active) { diff --git a/firmware/application/ui_navigation.hpp b/firmware/application/ui_navigation.hpp index d155686c..02c2bd46 100644 --- a/firmware/application/ui_navigation.hpp +++ b/firmware/application/ui_navigation.hpp @@ -40,7 +40,7 @@ #include "diskio.h" #include "lfsr_random.hpp" #include "sd_card.hpp" - +#include "external_app.hpp" #include #include @@ -57,6 +57,12 @@ enum modal_t { ABORT }; +struct AppInfoConsole { + const char* appCallName; + const char* appFriendlyName; + const app_location_t appLocation; +}; + class NavigationView : public View { public: std::function on_view_changed{}; @@ -99,6 +105,8 @@ class NavigationView : public View { * Returns true if the handler was bound successfully. */ bool set_on_pop(std::function on_pop); + static std::vector fixedAppListFC; // fixed app list for console. vector, so can be incomplete and still iterateable + bool StartAppByName(const char* name); // Starts a View (app) by name stored in fixedAppListFC. This is to start apps from console private: struct ViewState { std::unique_ptr view; @@ -321,6 +329,8 @@ class SystemView : public View { void toggle_overlay(); void paint_overlay(); + NavigationView* get_navigation_view(); + private: uint8_t overlay_active{0}; diff --git a/firmware/application/usb_serial_shell.cpp b/firmware/application/usb_serial_shell.cpp index 42d3623c..cdff3036 100644 --- a/firmware/application/usb_serial_shell.cpp +++ b/firmware/application/usb_serial_shell.cpp @@ -40,9 +40,12 @@ #include "ff.h" #include "chprintf.h" #include "chqueues.h" +#include "ui_external_items_menu_loader.hpp" #include "untar.hpp" #include "ui_widget.hpp" +#include "ui_navigation.hpp" + #include #include #include @@ -843,6 +846,78 @@ static void cmd_accessibility_readcurr(BaseSequentialStream* chp, int argc, char chprintf(chp, "\r\nok\r\n"); } +static void cmd_appstart(BaseSequentialStream* chp, int argc, char* argv[]) { + (void)argc; + (void)argv; + if (argc != 1) { + chprintf(chp, "Usage: appstart APPCALLNAME"); + return; + } + auto evtd = getEventDispatcherInstance(); + if (!evtd) return; + auto top_widget = evtd->getTopWidget(); + if (!top_widget) return; + auto nav = static_cast(top_widget)->get_navigation_view(); + if (!nav) return; + if (nav->StartAppByName(argv[0])) { + chprintf(chp, "ok\r\n"); + return; + } + // since ext app loader changed, we can just pass the string to it, and it"ll return if started or not. + std::string appwithpath = "/APPS/"; + appwithpath += argv[0]; + appwithpath += ".ppma"; + bool ret = ui::ExternalItemsMenuLoader::run_external_app(*nav, path_from_string8((char*)appwithpath.c_str())); + if (!ret) { + chprintf(chp, "error\r\n"); + return; + } + chprintf(chp, "ok\r\n"); +} + +static void printAppInfo(BaseSequentialStream* chp, ui::AppInfoConsole& element) { + if (strlen(element.appCallName) == 0) return; + chprintf(chp, element.appCallName); + chprintf(chp, " "); + chprintf(chp, element.appFriendlyName); + chprintf(chp, " "); + switch (element.appLocation) { + case RX: + chprintf(chp, "[RX]\r\n"); + break; + case TX: + chprintf(chp, "[TX]\r\n"); + break; + case UTILITIES: + chprintf(chp, "[UTIL]\r\n"); + break; + case DEBUG: + chprintf(chp, "[DEBUG]\r\n"); + break; + default: + break; + } +} + +// returns the installed apps, those can be called by appstart APPNAME +static void cmd_applist(BaseSequentialStream* chp, int argc, char* argv[]) { + (void)argc; + (void)argv; + auto evtd = getEventDispatcherInstance(); + if (!evtd) return; + auto top_widget = evtd->getTopWidget(); + if (!top_widget) return; + auto nav = static_cast(top_widget)->get_navigation_view(); + if (!nav) return; + for (auto element : ui::NavigationView::fixedAppListFC) { + printAppInfo(chp, element); + } + ui::ExternalItemsMenuLoader::load_all_external_items_callback([chp](ui::AppInfoConsole& info) { + printAppInfo(chp, info); + }); + chprintf(chp, "ok\r\n"); +} + static void cmd_cpld_read(BaseSequentialStream* chp, int argc, char* argv[]) { const char* usage = "usage: cpld_read \r\n" @@ -1032,6 +1107,8 @@ static const ShellCommand commands[] = { {"cpld_read", cmd_cpld_read}, {"accessibility_readall", cmd_accessibility_readall}, {"accessibility_readcurr", cmd_accessibility_readcurr}, + {"applist", cmd_applist}, + {"appstart", cmd_appstart}, {NULL, NULL}}; static const ShellConfig shell_cfg1 = {