diff --git a/App/Source/HelloWorld/HelloWorld.cpp b/App/Source/HelloWorld/HelloWorld.cpp index b11b9843..c6856a20 100644 --- a/App/Source/HelloWorld/HelloWorld.cpp +++ b/App/Source/HelloWorld/HelloWorld.cpp @@ -1,17 +1,23 @@ +#include "app/AppManifest.h" #include "lvgl.h" #include "lvgl/Toolbar.h" -static void onShow(tt::app::AppContext& context, lv_obj_t* parent) { - lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context); - lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); +using namespace tt::app; - lv_obj_t* label = lv_label_create(parent); - lv_label_set_text(label, "Hello, world!"); - lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); -} +class HelloWorldApp : public App { -extern const tt::app::AppManifest hello_world_app = { + void onShow(AppContext& context, lv_obj_t* parent) override { + lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + lv_obj_t* label = lv_label_create(parent); + lv_label_set_text(label, "Hello, world!"); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + } +}; + +extern const AppManifest hello_world_app = { .id = "HelloWorld", .name = "Hello World", - .onShow = onShow, + .createApp = create }; diff --git a/Boards/UnPhone/Source/bq24295/Bq24295.cpp b/Boards/UnPhone/Source/bq24295/Bq24295.cpp index a2ca7c5c..1d185f35 100644 --- a/Boards/UnPhone/Source/bq24295/Bq24295.cpp +++ b/Boards/UnPhone/Source/bq24295/Bq24295.cpp @@ -3,12 +3,15 @@ #define TAG "bq24295" -/** Reference: https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.h?ref_type=heads */ +/** Reference: + * https://www.ti.com/lit/ds/symlink/bq24295.pdf + * https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.h?ref_type=heads + */ namespace registers { - static const uint8_t WATCHDOG = 0x05U; // Charge end/timer cntrl - static const uint8_t OPERATION_CONTROL = 0x07U; // Misc operation control - static const uint8_t STATUS = 0x08U; // System status - static const uint8_t VERSION = 0x0AU; // Vendor/part/revision status + static const uint8_t WATCHDOG = 0x05U; // Datasheet page 35: Charge end/timer cntrl + static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control + static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status + static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status } // namespace registers // region Watchdog diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56c27412..be881e99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,4 +8,4 @@ Please open an [Issue](https://github.com/ByteWelder/Tactility/issues/new) on Gi # Code Style -See [this document](CODING_STYLE.md). +See [this document](CODING_STYLE.md) and [.clang-format](.clang-format). diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 51690fcc..3c94332c 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -9,33 +9,33 @@ - Add statusbar icon for memory pressure. - Show error in WiFi screen (e.g. AlertDialog when SPI is not enabled and available memory is below a certain amount) - Clean up static_cast when casting to base class. -- M5Stack CoreS3 SD card mounts, but cannot be read. There is currently a notice about it [here](https://github.com/espressif/esp-bsp/blob/master/bsp/m5stack_core_s3/README.md). - EventFlag: Fix return value of set/get/wait (the errors are weirdly mixed in) -- Consistently use either ESP_TARGET or ESP_PLATFORM - tt_check() failure during app argument bundle nullptr check seems to trigger SIGSEGV - Fix bug in T-Deck/etc: esp_lvgl_port settings has a large stack size (~9kB) to fix an issue where the T-Deck would get a stackoverflow. This sometimes happens when WiFi is auto-enabled and you open the app while it is still connecting. - M5Stack Core only shows 4MB of SPIRAM in use - Try to improve Core2 and CoreS3 performance by setting swap_bytes of display driver to false (this is a software operation on the display buffer!) and use 24 bit colour mode if needed - Files app: When SD card is not mounted, don't show it +- Crash log must mention board type +- Oops crashlog site: Add copy-pasteable addr2line command (e.g. xtensa-esp32s3-elf-addr2line -pfiaC -e Tactility.elf 00000000) # TODOs +- Experiment with what happens when using C++ code in an external app (without using standard library!) +- Get rid of "ESP_TARGET" and use official "ESP_PLATFORM" +- SpiSdCard should use SDMMC_FREQ_DEFAULT by default - Boards' CMakeLists.txt manually declare each source folder. Update them all to do a recursive search of all folders. -- We currently make all boards for a given platform (e.g. ESP32S3), but it's better to filter all irrelevant ones based on the Kconfig board settings: +- We currently build all boards for a given platform (e.g. ESP32S3), but it's better to filter all irrelevant ones based on the Kconfig board settings: Projects will load and compile faster as it won't compile all the dependencies of all these other boards - Make a ledger for setting CPU affinity of various services and tasks -- Make "blocking" argument the last one, and put it default to false (or remove it entirely?): void startApp(const std::string& id, bool blocking, std::shared_ptr parameters) { - Boot hooks instead of a single boot method in config. Define different boot phases/levels in enum. - Add toggle to Display app for sysmon overlay: https://docs.lvgl.io/master/API/others/sysmon/index.html - CrashHandler: use "corrupted" flag - CrashHandler: process other types of crashes (WDT?) - Call tt::lvgl::isSyncSet after HAL init and show error (and crash?) when it is not set. - Create different partitions files for different ESP flash size targets (N4, N8, N16, N32) -- Attach ELF data to wrapper app (as app data) (check that app state is "running"!) so you can run more than 1 external apps at a time. - We'll need to keep track of all manifest instances, so that the wrapper can look up the relevant manifest for the relevant callbacks. - T-Deck: Clear screen before turning on blacklight -- T-Deck: Use knob for UI selection +- T-Deck: Use knob for UI selection? - Crash monitoring: Keep track of which system phase the app crashed in (e.g. which app in which state) -- AppContext's onResult should pass the app id (or launch request id!) that was started, so we can differentiate between multiple types of apps being launched +- App::onResult should pass the app id (or launch request id!) that was started, so we can differentiate between multiple types of apps being launched - Create more unit tests for `tactility-core` and `tactility` (PC-only for now) - Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials. - Show a warning screen when a user plugs in the SD card on a device that only supports mounting at boot. @@ -47,6 +47,8 @@ - Support hot-plugging SD card # Nice-to-haves +- CoreS3 has a hardware issue that prevents mounting SD cards while using the display too: allow USB Mass Storage to use `/data` instead? Perhaps give the USB settings app a drop down to select the root filesystem to attach. +- Give external app a different icon. Allow an external app update their id, icon, type and name once they are running(, and persist that info?). Loader will need to be able to find app by (external) location. - Audio player app - Audio recording app - OTA updates @@ -63,6 +65,8 @@ - On crash, try to save current log to flash or SD card? (this is risky, though, so ask in Discord first) # App Ideas +- Weather app: https://lab.flipper.net/apps/flip_weather +- wget app: https://lab.flipper.net/apps/web_crawler (add profiles for known public APIs?) - USB implementation to make device act as mass storage device. - System logger - BlueTooth keyboard app diff --git a/ExternalApps/HelloWorld/main/Source/main.c b/ExternalApps/HelloWorld/main/Source/main.c index c1ab0443..70333274 100644 --- a/ExternalApps/HelloWorld/main/Source/main.c +++ b/ExternalApps/HelloWorld/main/Source/main.c @@ -5,7 +5,7 @@ * Note: LVGL and Tactility methods need to be exposed manually from TactilityC/Source/tt_init.cpp * Only C is supported for now (C++ symbols fail to link) */ -static void onShow(AppContextHandle context, lv_obj_t* parent) { +static void onShow(AppContextHandle context, void* data, lv_obj_t* parent) { lv_obj_t* toolbar = tt_lvgl_toolbar_create(parent, context); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); @@ -14,15 +14,12 @@ static void onShow(AppContextHandle context, lv_obj_t* parent) { lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); } +ExternalAppManifest manifest = { + .name = "Hello World", + .onShow = onShow +}; + int main(int argc, char* argv[]) { - tt_set_app_manifest( - "Hello World", - NULL, - NULL, - NULL, - onShow, - NULL, - NULL - ); + tt_app_register(&manifest); return 0; } diff --git a/Tactility/Private/app/AppInstance.h b/Tactility/Private/app/AppInstance.h index 511fcc47..1408fdd6 100644 --- a/Tactility/Private/app/AppInstance.h +++ b/Tactility/Private/app/AppInstance.h @@ -1,9 +1,10 @@ #pragma once -#include "app/AppContext.h" -#include "app/AppManifest.h" #include "Bundle.h" #include "Mutex.h" +#include "app/AppContext.h" +#include "app/AppManifest.h" +#include "app/ElfApp.h" #include #include @@ -17,19 +18,6 @@ typedef enum { StateStopped // App is not in memory } State; -struct ResultHolder { - Result result; - std::shared_ptr resultData; - - explicit ResultHolder(Result result) : result(result), resultData(nullptr) {} - - ResultHolder(Result result, std::shared_ptr resultData) : - result(result), - resultData(std::move(resultData)) - {} - -}; - /** * Thread-safe app instance. */ @@ -38,7 +26,7 @@ class AppInstance : public AppContext { private: Mutex mutex = Mutex(Mutex::Type::Normal); - const AppManifest& manifest; + const std::shared_ptr manifest; State state = StateInitial; Flags flags = { .showStatusbar = true }; /** @brief Optional parameters to start the app with @@ -52,16 +40,40 @@ private: * These manifest methods can optionally allocate/free data that is attached here. */ std::shared_ptr _Nullable data; - std::unique_ptr _Nullable resultHolder; + + std::shared_ptr app; + + static std::shared_ptr createApp( + const std::shared_ptr& manifest + ) { + if (manifest->location.isInternal()) { + tt_assert(manifest->createApp != nullptr); + return manifest->createApp(); + } else if (manifest->location.isExternal()) { + if (manifest->createApp != nullptr) { + TT_LOG_W("", "Manifest specifies createApp, but this is not used with external apps"); + } +#ifdef ESP_PLATFORM + return app::createElfApp(manifest); +#else + tt_crash("not supported"); +#endif + } else { + tt_crash("not implemented"); + } + } public: - explicit AppInstance(const AppManifest& manifest) : - manifest(manifest) {} - - AppInstance(const AppManifest& manifest, std::shared_ptr parameters) : + explicit AppInstance(const std::shared_ptr& manifest) : manifest(manifest), - parameters(std::move(parameters)) {} + app(createApp(manifest)) + {} + + AppInstance(const std::shared_ptr& manifest, std::shared_ptr parameters) : + manifest(manifest), + parameters(std::move(parameters)), + app(createApp(manifest)) {} ~AppInstance() override = default; @@ -70,22 +82,15 @@ public: const AppManifest& getManifest() const override; - Flags getFlags() const override; + Flags getFlags() const; void setFlags(Flags flags); Flags& mutableFlags() { return flags; } // TODO: locking mechanism - std::shared_ptr _Nullable getData() const override; - void setData(std::shared_ptr data) override; - std::shared_ptr getParameters() const override; - void setResult(Result result) override; - void setResult(Result result, std::shared_ptr bundle) override; - bool hasResult() const override; - std::unique_ptr getPaths() const override; - std::unique_ptr& getResult() { return resultHolder; } + std::shared_ptr getApp() const override { return app; } }; } // namespace diff --git a/Tactility/Private/app/files/FilesPrivate.h b/Tactility/Private/app/files/FilesPrivate.h deleted file mode 100644 index 25d4c0b4..00000000 --- a/Tactility/Private/app/files/FilesPrivate.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "./View.h" -#include "./State.h" - -#include "app/AppManifest.h" - -#include -#include -#include - -namespace tt::app::files { - -class Files { - std::unique_ptr view; - std::shared_ptr state; - -public: - Files() { - state = std::make_shared(); - view = std::make_unique(state); - } - - void onShow(lv_obj_t* parent) { - view->init(parent); - } - - void onResult(Result result, const Bundle& bundle) { - view->onResult(result, bundle); - } -}; - - -} // namespace diff --git a/Tactility/Private/app/files/View.h b/Tactility/Private/app/files/View.h index 65839e43..e3838786 100644 --- a/Tactility/Private/app/files/View.h +++ b/Tactility/Private/app/files/View.h @@ -35,7 +35,7 @@ public: void onRenamePressed(); void onDeletePressed(); void onDirEntryListScrollBegin(); - void onResult(Result result, const Bundle& bundle); + void onResult(Result result, std::unique_ptr bundle); }; } diff --git a/Tactility/Private/app/i2cscanner/I2cScannerPrivate.h b/Tactility/Private/app/i2cscanner/I2cScannerPrivate.h index 97761098..0d6a951a 100644 --- a/Tactility/Private/app/i2cscanner/I2cScannerPrivate.h +++ b/Tactility/Private/app/i2cscanner/I2cScannerPrivate.h @@ -19,17 +19,7 @@ enum ScanState { }; struct Data { - // Core - Mutex mutex = Mutex(Mutex::Type::Recursive); - std::unique_ptr scanTimer = nullptr; - // State - ScanState scanState; - i2c_port_t port = I2C_NUM_0; - std::vector scannedAddresses; - // Widgets - lv_obj_t* scanButtonLabelWidget = nullptr; - lv_obj_t* portDropdownWidget = nullptr; - lv_obj_t* scanListWidget = nullptr; + }; void onScanTimerFinished(std::shared_ptr data); diff --git a/Tactility/Private/app/screenshot/ScreenshotUi.h b/Tactility/Private/app/screenshot/ScreenshotUi.h deleted file mode 100644 index 36919d61..00000000 --- a/Tactility/Private/app/screenshot/ScreenshotUi.h +++ /dev/null @@ -1,42 +0,0 @@ -#include "Timer.h" -#include "TactilityConfig.h" - -#if TT_FEATURE_SCREENSHOT_ENABLED - -#pragma once - -#include "app/AppContext.h" -#include "lvgl.h" - -namespace tt::app::screenshot { - -class ScreenshotUi { - - lv_obj_t* modeDropdown = nullptr; - lv_obj_t* pathTextArea = nullptr; - lv_obj_t* startStopButtonLabel = nullptr; - lv_obj_t* timerWrapper = nullptr; - lv_obj_t* delayTextArea = nullptr; - std::unique_ptr updateTimer; - - void createTimerSettingsWidgets(lv_obj_t* parent); - void createModeSettingWidgets(lv_obj_t* parent); - void createFilePathWidgets(lv_obj_t* parent); - - void updateScreenshotMode(); - -public: - - ScreenshotUi(); - ~ScreenshotUi(); - - void createWidgets(const AppContext& app, lv_obj_t* parent); - void onStartPressed(); - void onModeSet(); - void onTimerTick(); -}; - - -} // namespace - -#endif diff --git a/Tactility/Private/app/wificonnect/WifiConnectPrivate.h b/Tactility/Private/app/wificonnect/WifiConnectPrivate.h index 43df34da..88a2ce61 100644 --- a/Tactility/Private/app/wificonnect/WifiConnectPrivate.h +++ b/Tactility/Private/app/wificonnect/WifiConnectPrivate.h @@ -1,5 +1,6 @@ #pragma once +#include "app/App.h" #include "app/wificonnect/Bindings.h" #include "app/wificonnect/State.h" #include "app/wificonnect/View.h" @@ -9,7 +10,10 @@ namespace tt::app::wificonnect { -class WifiConnect { +class WifiConnect : public App { + +private: + Mutex mutex; State state; Bindings bindings = { @@ -28,8 +32,8 @@ public: void lock(); void unlock(); - void onShow(AppContext& app, lv_obj_t* parent); - void onHide(AppContext& app); + void onShow(AppContext& app, lv_obj_t* parent) override; + void onHide(AppContext& app) override; State& getState() { return state; } Bindings& getBindings() { return bindings; } diff --git a/Tactility/Private/app/wifimanage/WifiManagePrivate.h b/Tactility/Private/app/wifimanage/WifiManagePrivate.h index b3941998..c91d0f34 100644 --- a/Tactility/Private/app/wifimanage/WifiManagePrivate.h +++ b/Tactility/Private/app/wifimanage/WifiManagePrivate.h @@ -1,5 +1,6 @@ #pragma once +#include "app/App.h" #include "Mutex.h" #include "./View.h" #include "./State.h" @@ -7,7 +8,9 @@ namespace tt::app::wifimanage { -class WifiManage { +class WifiManage : public App { + +private: PubSubSubscription* wifiSubscription = nullptr; Mutex mutex; @@ -23,8 +26,8 @@ public: void lock(); void unlock(); - void onShow(AppContext& app, lv_obj_t* parent); - void onHide(AppContext& app); + void onShow(AppContext& app, lv_obj_t* parent) override; + void onHide(AppContext& app) override; Bindings& getBindings() { return bindings; } State& getState() { return state; } diff --git a/Tactility/Private/service/gui/Gui_i.h b/Tactility/Private/service/gui/Gui_i.h index c8777afc..c047e587 100644 --- a/Tactility/Private/service/gui/Gui_i.h +++ b/Tactility/Private/service/gui/Gui_i.h @@ -4,9 +4,8 @@ #include "Mutex.h" #include "Pubsub.h" #include "service/gui/Gui.h" -#include "service/gui/ViewPort.h" -#include "service/gui/ViewPort_i.h" #include +#include namespace tt::service::gui { @@ -27,7 +26,7 @@ struct Gui { lv_obj_t* statusbarWidget = nullptr; // App-specific - ViewPort* appViewPort = nullptr; + std::shared_ptr appToRender = nullptr; lv_obj_t* _Nullable keyboard = nullptr; lv_group_t* keyboardGroup = nullptr; diff --git a/Tactility/Private/service/gui/ViewPort_i.h b/Tactility/Private/service/gui/ViewPort_i.h deleted file mode 100644 index b8d4c8e7..00000000 --- a/Tactility/Private/service/gui/ViewPort_i.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "service/gui/ViewPort.h" - -namespace tt::service::gui { - -/** Process draw call. Calls onShow callback. - * To be used by GUI, called on redraw. - * - * @param view_port ViewPort instance - * @param canvas canvas to draw at - */ -void view_port_show(ViewPort* view_port, lv_obj_t* parent); - -/** - * Process draw clearing call. Calls on_hdie callback. - * To be used by GUI, called on redraw. - * - * @param view_port - */ -void view_port_hide(ViewPort* view_port); - -} // namespace diff --git a/Tactility/Private/service/loader/Loader_i.h b/Tactility/Private/service/loader/Loader_i.h index 860415a0..625f3875 100644 --- a/Tactility/Private/service/loader/Loader_i.h +++ b/Tactility/Private/service/loader/Loader_i.h @@ -6,7 +6,6 @@ #include "MessageQueue.h" #include "Pubsub.h" #include "Thread.h" -#include "service/gui/ViewPort.h" #include "service/loader/Loader.h" #include "RtosCompatSemaphore.h" #include @@ -25,31 +24,9 @@ typedef enum { LoaderEventTypeApplicationStopped } LoaderEventType; -typedef struct { - app::AppInstance& app; -} LoaderEventAppStarted; - -typedef struct { - app::AppInstance& app; -} LoaderEventAppShowing; - -typedef struct { - app::AppInstance& app; -} LoaderEventAppHiding; - -typedef struct { - const app::AppManifest& manifest; -} LoaderEventAppStopped; - -typedef struct { +struct LoaderEvent { LoaderEventType type; - union { - LoaderEventAppStarted app_started; - LoaderEventAppShowing app_showing; - LoaderEventAppHiding app_hiding; - LoaderEventAppStopped app_stopped; - }; -} LoaderEvent; +}; // endregion LoaderEvent @@ -77,10 +54,9 @@ public: // endregion LoaderMessage struct Loader { - std::shared_ptr pubsubInternal = std::make_shared(); std::shared_ptr pubsubExternal = std::make_shared(); Mutex mutex = Mutex(Mutex::Type::Recursive); - std::stack appStack; + std::stack> appStack; /** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods. * This includes full LVGL redraw via Gui::redraw() */ diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 8d648910..18051e0e 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -64,7 +64,6 @@ namespace app { namespace screenshot { extern const AppManifest manifest; } #endif #ifdef ESP_PLATFORM - extern const AppManifest elfWrapperManifest; namespace crashdiagnostics { extern const AppManifest manifest; } #endif } @@ -99,8 +98,7 @@ static const std::vector system_apps = { &app::screenshot::manifest, #endif #ifdef ESP_PLATFORM - &app::crashdiagnostics::manifest, - &app::elfWrapperManifest, // For hot-loading ELF apps + &app::crashdiagnostics::manifest #endif }; @@ -109,11 +107,11 @@ static const std::vector system_apps = { static void register_system_apps() { TT_LOG_I(TAG, "Registering default apps"); for (const auto* app_manifest: system_apps) { - addApp(app_manifest); + addApp(*app_manifest); } if (getConfiguration()->hardware->power != nullptr) { - addApp(&app::power::manifest); + addApp(app::power::manifest); } } @@ -121,7 +119,7 @@ static void register_user_apps(const std::vector& apps) TT_LOG_I(TAG, "Registering user apps"); for (auto* manifest : apps) { assert(manifest != nullptr); - addApp(manifest); + addApp(*manifest); } } diff --git a/Tactility/Source/app/App.h b/Tactility/Source/app/App.h new file mode 100644 index 00000000..9d85caeb --- /dev/null +++ b/Tactility/Source/app/App.h @@ -0,0 +1,78 @@ +#pragma once + +#include "AppContext.h" +#include "Bundle.h" +#include + +// Forward declarations +typedef struct _lv_obj_t lv_obj_t; + +namespace tt::app { + +// Forward declarations +class AppContext; +enum class Result; + +class App { + +private: + + Mutex mutex; + + struct ResultHolder { + Result result; + std::unique_ptr resultData; + + explicit ResultHolder(Result result) : result(result), resultData(nullptr) {} + + ResultHolder(Result result, std::unique_ptr resultData) : + result(result), + resultData(std::move(resultData)) {} + }; + + std::unique_ptr resultHolder; + +public: + + App() = default; + virtual ~App() = default; + + virtual void onStart(AppContext& appContext) {} + virtual void onStop(AppContext& appContext) {} + virtual void onShow(AppContext& appContext, lv_obj_t* parent) {} + virtual void onHide(AppContext& appContext) {} + virtual void onResult(AppContext& appContext, Result result, std::unique_ptr _Nullable resultData) {} + + Mutex& getMutex() { return mutex; } + + bool hasResult() const { return resultHolder != nullptr; } + + void setResult(Result result, std::unique_ptr resultData = nullptr) { + auto lockable = getMutex().scoped(); + lockable->lock(portMAX_DELAY); + resultHolder = std::make_unique(result, std::move(resultData)); + } + + /** + * Used by system to extract the result data when this application is finished. + * Note that this removes the data from the class! + */ + bool moveResult(Result& outResult, std::unique_ptr& outBundle) { + auto lockable = getMutex().scoped(); + lockable->lock(portMAX_DELAY); + + if (resultHolder != nullptr) { + outResult = resultHolder->result; + outBundle = std::move(resultHolder->resultData); + resultHolder = nullptr; + return true; + } else { + return false; + } + } +}; + +template +std::shared_ptr create() { return std::shared_ptr(new T); } + +} diff --git a/Tactility/Source/app/AppCompatC.h b/Tactility/Source/app/AppCompatC.h new file mode 100644 index 00000000..f6afc38e --- /dev/null +++ b/Tactility/Source/app/AppCompatC.h @@ -0,0 +1,109 @@ +#pragma once + +#include "./App.h" +#include "./AppManifest.h" + +namespace tt::app { + +typedef void* (*CreateData)(); +typedef void (*DestroyData)(void* data); +typedef void (*OnStart)(AppContext& app, void* _Nullable data); +typedef void (*OnStop)(AppContext& app, void* _Nullable data); +typedef void (*OnShow)(AppContext& app, void* _Nullable data, lv_obj_t* parent); +typedef void (*OnHide)(AppContext& app, void* _Nullable data); +typedef void (*OnResult)(AppContext& app, void* _Nullable data, Result result, std::unique_ptr resultData); + +class AppCompatC : public App { + +private: + + CreateData _Nullable createData; + DestroyData _Nullable destroyData; + OnStart _Nullable onStartCallback; + OnStop _Nullable onStopCallback; + OnShow _Nullable onShowCallback; + OnHide _Nullable onHideCallback; + OnResult _Nullable onResultCallback; + + void* data = nullptr; + +public: + + AppCompatC( + CreateData _Nullable createData, + DestroyData _Nullable destroyData, + OnStart _Nullable onStart, + OnStop _Nullable onStop, + OnShow _Nullable onShow, + OnHide _Nullable onHide, + OnResult _Nullable onResult + ) : createData(createData), + destroyData(destroyData), + onStartCallback(onStart), + onStopCallback(onStop), + onShowCallback(onShow), + onHideCallback(onHide), + onResultCallback(onResult) + {} + + void onStart(AppContext& appContext) override { + if (createData != nullptr) { + data = createData(); + } + + if (onStartCallback != nullptr) { + onStartCallback(appContext, data); + } + } + + void onStop(AppContext& appContext) override { + if (onStopCallback != nullptr) { + onStopCallback(appContext, data); + } + + if (destroyData != nullptr && data != nullptr) { + destroyData(data); + } + } + + void onShow(AppContext& appContext, lv_obj_t* parent) override { + if (onShowCallback != nullptr) { + onShowCallback(appContext, data, parent); + } + } + + void onHide(AppContext& appContext) override { + if (onHideCallback != nullptr) { + onHideCallback(appContext, data); + } + } + + void onResult(AppContext& appContext, Result result, std::unique_ptr _Nullable resultData) override { + if (onResultCallback != nullptr) { + onResultCallback(appContext, data, result, std::move(resultData)); + } + } +}; + +template +App* createC( + CreateData _Nullable createData, + DestroyData _Nullable destroyData, + OnStart _Nullable onStartCallback, + OnStop _Nullable onStopCallback, + OnShow _Nullable onShowCallback, + OnHide _Nullable onHideCallback, + OnResult _Nullable onResultCallback +) { + return new AppCompatC( + createData, + destroyData, + onStartCallback, + onStopCallback, + onShowCallback, + onHideCallback, + onResultCallback + ); +} + +} diff --git a/Tactility/Source/app/AppContext.h b/Tactility/Source/app/AppContext.h index 7477e051..fadc548a 100644 --- a/Tactility/Source/app/AppContext.h +++ b/Tactility/Source/app/AppContext.h @@ -1,12 +1,15 @@ #pragma once -#include "AppManifest.h" #include "Bundle.h" #include namespace tt::app { +// Forward declarations +class App; class Paths; +struct AppManifest; +enum class Result; typedef union { struct { @@ -28,14 +31,10 @@ protected: public: virtual const AppManifest& getManifest() const = 0; - virtual std::shared_ptr _Nullable getData() const = 0; - virtual void setData(std::shared_ptr data) = 0; virtual std::shared_ptr getParameters() const = 0; - virtual Flags getFlags() const = 0; - virtual void setResult(Result result) = 0; - virtual void setResult(Result result, std::shared_ptr bundle)= 0; - virtual bool hasResult() const = 0; virtual std::unique_ptr getPaths() const = 0; + + virtual std::shared_ptr getApp() const = 0; }; class Paths { diff --git a/Tactility/Source/app/AppInstance.cpp b/Tactility/Source/app/AppInstance.cpp index fc07a7a9..1c995a98 100644 --- a/Tactility/Source/app/AppInstance.cpp +++ b/Tactility/Source/app/AppInstance.cpp @@ -25,7 +25,8 @@ State AppInstance::getState() const { * Consider not exposing bundle, but expose `app_get_bundle_int(key)` methods with locking in it. */ const AppManifest& AppInstance::getManifest() const { - return manifest; + tt_assert(manifest != nullptr); + return *manifest; } Flags AppInstance::getFlags() const { @@ -41,19 +42,6 @@ void AppInstance::setFlags(Flags newFlags) { mutex.release(); } -std::shared_ptr _Nullable AppInstance::getData() const { - mutex.acquire(TtWaitForever); - auto result = data; - mutex.release(); - return result; -} - -void AppInstance::setData(std::shared_ptr newData) { - mutex.acquire(TtWaitForever); - data = newData; - mutex.release(); -} - std::shared_ptr AppInstance::getParameters() const { mutex.acquire(TtWaitForever); std::shared_ptr result = parameters; @@ -61,29 +49,9 @@ std::shared_ptr AppInstance::getParameters() const { return result; } -void AppInstance::setResult(Result result) { - std::unique_ptr new_holder(new ResultHolder(result)); - mutex.acquire(TtWaitForever); - resultHolder = std::move(new_holder); - mutex.release(); -} - -void AppInstance::setResult(Result result, std::shared_ptr bundle) { - std::unique_ptr new_holder(new ResultHolder(result, std::move(bundle))); - mutex.acquire(TtWaitForever); - resultHolder = std::move(new_holder); - mutex.release(); -} - -bool AppInstance::hasResult() const { - mutex.acquire(TtWaitForever); - bool has_result = resultHolder != nullptr; - mutex.release(); - return has_result; -} - std::unique_ptr AppInstance::getPaths() const { - return std::make_unique(manifest); + tt_assert(manifest != nullptr); + return std::make_unique(*manifest); } } // namespace diff --git a/Tactility/Source/app/AppManifest.h b/Tactility/Source/app/AppManifest.h index f8579111..2de245f1 100644 --- a/Tactility/Source/app/AppManifest.h +++ b/Tactility/Source/app/AppManifest.h @@ -1,44 +1,63 @@ #pragma once -#include -#include #include "CoreDefines.h" +#include "ManifestRegistry.h" +#include +#include // Forward declarations typedef struct _lv_obj_t lv_obj_t; namespace tt::app { +class App; class AppContext; /** Application types */ -enum Type { +enum class Type { /** Boot screen, shown before desktop is launched. */ - TypeBoot, + Boot, /** A launcher app sits at the root of the app stack after the boot splash is finished */ - TypeLauncher, + Launcher, /** Apps that generally aren't started from the desktop (e.g. image viewer) */ - TypeHidden, + Hidden, /** Standard apps, provided by the system. */ - TypeSystem, + System, /** The apps that are launched/shown by the Settings app. The Settings app itself is of type AppTypeSystem. */ - TypeSettings, + Settings, /** User-provided apps. */ - TypeUser + User }; /** Result status code for application result callback. */ -typedef enum { - ResultOk, - ResultCancelled, - ResultError -} Result; +enum class Result { + Ok = 0U, + Cancelled = 1U, + Error = 2U +}; -typedef void (*AppOnStart)(AppContext& app); -typedef void (*AppOnStop)(AppContext& app); -typedef void (*AppOnShow)(AppContext& app, lv_obj_t* parent); -typedef void (*AppOnHide)(AppContext& app); -typedef void (*AppOnResult)(AppContext& app, Result result, const Bundle& resultData); +class Location { + +private: + + std::string path; + Location() = default; + explicit Location(const std::string& path) : path(path) {} + +public: + + static Location internal() { return {}; } + + static Location external(const std::string& path) { + return Location(path); + } + + bool isInternal() const { return path.empty(); } + bool isExternal() const { return !path.empty(); } + const std::string& getPath() const { return path; } +}; + +typedef std::shared_ptr(*CreateApp)(); struct AppManifest { /** The identifier by which the app is launched by the system and other apps. */ @@ -51,26 +70,17 @@ struct AppManifest { std::string icon = {}; /** App type affects launch behaviour. */ - Type type = TypeUser; + Type type = Type::User; - /** Non-blocking method to call when app is started. */ - AppOnStart onStart = nullptr; + /** Where the app is located */ + Location location = Location::internal(); - /** Non-blocking method to call when app is stopped. */ - AppOnStop _Nullable onStop = nullptr; - - /** Non-blocking method to create the GUI. */ - AppOnShow _Nullable onShow = nullptr; - - /** Non-blocking method, called before gui is destroyed. */ - AppOnHide _Nullable onHide = nullptr; - - /** Handle the result for apps that are launched. */ - AppOnResult _Nullable onResult = nullptr; + /** Create the instance of the app */ + CreateApp createApp = nullptr; }; struct { - bool operator()(const AppManifest* left, const AppManifest* right) const { return left->name < right->name; } + bool operator()(const std::shared_ptr& left, const std::shared_ptr& right) const { return left->name < right->name; } } SortAppManifestByName; } // namespace diff --git a/Tactility/Source/app/ElfApp.cpp b/Tactility/Source/app/ElfApp.cpp index 7447ada2..71d2ad63 100644 --- a/Tactility/Source/app/ElfApp.cpp +++ b/Tactility/Source/app/ElfApp.cpp @@ -1,126 +1,198 @@ #ifdef ESP_PLATFORM -#include "file/File.h" #include "ElfApp.h" +#include "Log.h" +#include "StringUtils.h" #include "TactilityCore.h" #include "esp_elf.h" - +#include "file/File.h" #include "service/loader/Loader.h" +#include + namespace tt::app { #define TAG "elf_app" -#define ELF_WRAPPER_APP_ID "ElfWrapper" + +struct ElfManifest { + /** The user-readable name of the app. Used in UI. */ + std::string name; + /** Optional icon. */ + std::string icon; + CreateData _Nullable createData; + DestroyData _Nullable destroyData; + OnStart _Nullable onStart; + OnStop _Nullable onStop; + OnShow _Nullable onShow; + OnHide _Nullable onHide; + OnResult _Nullable onResult; +}; static size_t elfManifestSetCount = 0; -std::unique_ptr elfFileData; -esp_elf_t elf; +static ElfManifest elfManifest; -bool startElfApp(const std::string& filePath) { - TT_LOG_I(TAG, "Starting ELF %s", filePath.c_str()); +class ElfApp : public App { - assert(elfFileData == nullptr); +private: - size_t size = 0; - elfFileData = file::readBinary(filePath, size); - if (elfFileData == nullptr) { - return false; + const std::string filePath; + std::unique_ptr elfFileData; + esp_elf_t elf; + bool shouldCleanupElf = false; // Whether we have to clean up the above "elf" object + std::unique_ptr manifest; + void* data = nullptr; + + bool startElf() { + TT_LOG_I(TAG, "Starting ELF %s", filePath.c_str()); + assert(elfFileData == nullptr); + + size_t size = 0; + elfFileData = file::readBinary(filePath, size); + if (elfFileData == nullptr) { + return false; + } + + if (esp_elf_init(&elf) < 0) { + TT_LOG_E(TAG, "Failed to initialize"); + shouldCleanupElf = true; + return false; + } + + if (esp_elf_relocate(&elf, elfFileData.get()) < 0) { + TT_LOG_E(TAG, "Failed to load executable"); + return false; + } + + int argc = 0; + char* argv[] = {}; + + if (esp_elf_request(&elf, 0, argc, argv) < 0) { + TT_LOG_W(TAG, "Executable returned error code"); + return false; + } + + return true; } - if (esp_elf_init(&elf) < 0) { - TT_LOG_E(TAG, "Failed to initialize"); - return false; + void stopElf() { + TT_LOG_I(TAG, "Cleaning up ELF"); + + if (shouldCleanupElf) { + esp_elf_deinit(&elf); + } + + if (elfFileData != nullptr) { + elfFileData = nullptr; + } } - if (esp_elf_relocate(&elf, elfFileData.get()) < 0) { - TT_LOG_E(TAG, "Failed to load executable"); - return false; +public: + + explicit ElfApp(const std::string& filePath) : filePath(filePath) {} + + void onStart(AppContext& appContext) override { + auto initial_count = elfManifestSetCount; + if (startElf()) { + if (elfManifestSetCount > initial_count) { + manifest = std::make_unique(elfManifest); + + if (manifest->createData != nullptr) { + data = manifest->createData(); + } + + if (manifest->onStart != nullptr) { + manifest->onStart(appContext, data); + } + } + } else { + service::loader::stopApp(); + } } - int argc = 0; - char* argv[] = {}; + void onStop(AppContext& appContext) override { + TT_LOG_I(TAG, "Cleaning up app"); + if (manifest != nullptr) { + if (manifest->onStop != nullptr) { + manifest->onStop(appContext, data); + } - size_t manifest_set_count = elfManifestSetCount; - if (esp_elf_request(&elf, 0, argc, argv) < 0) { - TT_LOG_W(TAG, "Executable returned error code"); - return false; + if (manifest->destroyData != nullptr && data != nullptr) { + manifest->destroyData(data); + } + + this->manifest = nullptr; + } + stopElf(); } - if (elfManifestSetCount > manifest_set_count) { - service::loader::startApp(ELF_WRAPPER_APP_ID); - } else { - TT_LOG_W(TAG, "App did not set manifest to run - cleaning up ELF"); - esp_elf_deinit(&elf); - elfFileData = nullptr; + void onShow(AppContext& appContext, lv_obj_t* parent) override { + if (manifest != nullptr && manifest->onShow != nullptr) { + manifest->onShow(appContext, data, parent); + } } - return true; -} + void onHide(AppContext& appContext) override { + if (manifest != nullptr && manifest->onHide != nullptr) { + manifest->onHide(appContext, data); + } + } -static void onStart(AppContext& app) {} -static void onStop(AppContext& app) {} -static void onShow(AppContext& app, lv_obj_t* parent) {} -static void onHide(AppContext& app) {} -static void onResult(AppContext& app, Result result, const Bundle& resultBundle) {} - -AppManifest elfManifest = { - .id = "", - .name = "", - .type = TypeHidden, - .onStart = onStart, - .onStop = onStop, - .onShow = onShow, - .onHide = onHide, - .onResult = onResult + void onResult(AppContext& appContext, Result result, std::unique_ptr resultBundle) override { + if (manifest != nullptr && manifest->onResult != nullptr) { + manifest->onResult(appContext, data, result, std::move(resultBundle)); + } + } }; -static void onStartWrapper(AppContext& app) { - elfManifest.onStart(app); -} - -static void onStopWrapper(AppContext& app) { - elfManifest.onStop(app); - TT_LOG_I(TAG, "Cleaning up ELF"); - esp_elf_deinit(&elf); - elfFileData = nullptr; -} - -static void onShowWrapper(AppContext& app, lv_obj_t* parent) { - elfManifest.onShow(app, parent); -} - -static void onHideWrapper(AppContext& app) { - elfManifest.onHide(app); -} - -static void onResultWrapper(AppContext& app, Result result, const Bundle& bundle) { - elfManifest.onResult(app, result, bundle); -} - -AppManifest elfWrapperManifest = { - .id = ELF_WRAPPER_APP_ID, - .name = "ELF Wrapper", - .type = TypeHidden, - .onStart = onStartWrapper, - .onStop = onStopWrapper, - .onShow = onShowWrapper, - .onHide = onHideWrapper, - .onResult = onResultWrapper -}; - -void setElfAppManifest(const AppManifest& manifest) { - elfManifest.id = manifest.id; - elfManifest.name = manifest.name; - elfWrapperManifest.name = manifest.name; - elfManifest.onStart = manifest.onStart; - elfManifest.onStop = manifest.onStop; - elfManifest.onShow = manifest.onShow; - elfManifest.onHide = manifest.onHide; - elfManifest.onResult = manifest.onResult; - +void setElfAppManifest( + const char* name, + const char* _Nullable icon, + CreateData _Nullable createData, + DestroyData _Nullable destroyData, + OnStart _Nullable onStart, + OnStop _Nullable onStop, + OnShow _Nullable onShow, + OnHide _Nullable onHide, + OnResult _Nullable onResult +) { + elfManifest = ElfManifest { + .name = name ? name : "", + .icon = icon ? icon : "", + .createData = createData, + .destroyData = destroyData, + .onStart = onStart, + .onStop = onStop, + .onShow = onShow, + .onHide = onHide, + .onResult = onResult + }; elfManifestSetCount++; } +std::string getElfAppId(const std::string& filePath) { + return filePath; +} + +bool registerElfApp(const std::string& filePath) { + if (findAppById(filePath) == nullptr) { + auto manifest = AppManifest { + .id = getElfAppId(filePath), + .name = tt::string::removeFileExtension(tt::string::getLastPathSegment(filePath)), + .type = Type::User, + .location = Location::external(filePath) + }; + addApp(manifest); + } + return false; +} + +std::shared_ptr createElfApp(const std::shared_ptr& manifest) { + TT_LOG_I(TAG, "createElfApp"); + tt_assert(manifest != nullptr); + tt_assert(manifest->location.isExternal()); + return std::make_shared(manifest->location.getPath()); +} + } // namespace -#endif // ESP_PLATFORM \ No newline at end of file +#endif // ESP_PLATFORM diff --git a/Tactility/Source/app/ElfApp.h b/Tactility/Source/app/ElfApp.h index 55a9e4c3..566117c1 100644 --- a/Tactility/Source/app/ElfApp.h +++ b/Tactility/Source/app/ElfApp.h @@ -1,16 +1,35 @@ #pragma once +#include "AppCompatC.h" #include "AppManifest.h" #ifdef ESP_PLATFORM namespace tt::app { -bool startElfApp(const std::string& filePath); +void setElfAppManifest( + const char* name, + const char* _Nullable icon, + CreateData _Nullable createData, + DestroyData _Nullable destroyData, + OnStart _Nullable onStart, + OnStop _Nullable onStop, + OnShow _Nullable onShow, + OnHide _Nullable onHide, + OnResult _Nullable onResult +); -void setElfAppManifest(const AppManifest& manifest); +/** + * @return the app ID based on the executable's file path. + */ +std::string getElfAppId(const std::string& filePath); + +/** + * @return true when registration was done, false when app was already registered + */ +bool registerElfApp(const std::string& filePath); + +std::shared_ptr createElfApp(const std::shared_ptr& manifest); } - - #endif // ESP_PLATFORM diff --git a/Tactility/Source/app/ManifestRegistry.cpp b/Tactility/Source/app/ManifestRegistry.cpp index e7359594..298e2d03 100644 --- a/Tactility/Source/app/ManifestRegistry.cpp +++ b/Tactility/Source/app/ManifestRegistry.cpp @@ -1,40 +1,44 @@ #include "ManifestRegistry.h" #include "Mutex.h" -#include "TactilityCore.h" +#include "AppManifest.h" #include #define TAG "app" namespace tt::app { -typedef std::unordered_map AppManifestMap; +typedef std::unordered_map> AppManifestMap; static AppManifestMap app_manifest_map; static Mutex hash_mutex(Mutex::Type::Normal); -void addApp(const AppManifest* manifest) { - TT_LOG_I(TAG, "Registering manifest %s", manifest->id.c_str()); +void addApp(const AppManifest& manifest) { + TT_LOG_I(TAG, "Registering manifest %s", manifest.id.c_str()); hash_mutex.acquire(TtWaitForever); - if (app_manifest_map[manifest->id] == nullptr) { - app_manifest_map[manifest->id] = manifest; + if (!app_manifest_map.contains(manifest.id)) { + app_manifest_map[manifest.id] = std::make_shared(manifest); } else { - TT_LOG_E(TAG, "App id in use: %s", manifest->id.c_str()); + TT_LOG_E(TAG, "App id in use: %s", manifest.id.c_str()); } hash_mutex.release(); } -_Nullable const AppManifest * findAppById(const std::string& id) { +_Nullable std::shared_ptr findAppById(const std::string& id) { hash_mutex.acquire(TtWaitForever); - _Nullable const AppManifest* result = app_manifest_map[id.c_str()]; + auto result = app_manifest_map.find(id); hash_mutex.release(); - return result; + if (result != app_manifest_map.end()) { + return result->second; + } else { + return nullptr; + } } -std::vector getApps() { - std::vector manifests; +std::vector> getApps() { + std::vector> manifests; hash_mutex.acquire(TtWaitForever); for (const auto& item: app_manifest_map) { manifests.push_back(item.second); diff --git a/Tactility/Source/app/ManifestRegistry.h b/Tactility/Source/app/ManifestRegistry.h index 86cef60d..649b3eb8 100644 --- a/Tactility/Source/app/ManifestRegistry.h +++ b/Tactility/Source/app/ManifestRegistry.h @@ -1,21 +1,23 @@ #pragma once -#include "AppManifest.h" +#include "App.h" #include #include namespace tt::app { +struct AppManifest; + /** Register an application with its manifest */ -void addApp(const AppManifest* manifest); +void addApp(const AppManifest& manifest); /** Find an application manifest by its id * @param[in] id the manifest id * @return the application manifest if it was found */ -const AppManifest _Nullable* findAppById(const std::string& id); +_Nullable std::shared_ptr findAppById(const std::string& id); /** @return a list of all registered apps. This includes user and system apps. */ -std::vector getApps(); +std::vector> getApps(); } // namespace diff --git a/Tactility/Source/app/alertdialog/AlertDialog.cpp b/Tactility/Source/app/alertdialog/AlertDialog.cpp index 337babef..53dce733 100644 --- a/Tactility/Source/app/alertdialog/AlertDialog.cpp +++ b/Tactility/Source/app/alertdialog/AlertDialog.cpp @@ -34,10 +34,6 @@ int32_t getResultIndex(const Bundle& bundle) { return index; } -void setResultIndex(std::shared_ptr bundle, int32_t index) { - bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, index); -} - static std::string getTitleParameter(std::shared_ptr bundle) { std::string result; if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) { @@ -47,78 +43,93 @@ static std::string getTitleParameter(std::shared_ptr bundle) { } } -static void onButtonClicked(lv_event_t* e) { - lv_event_code_t code = lv_event_get_code(e); - auto index = reinterpret_cast(lv_event_get_user_data(e)); - TT_LOG_I(TAG, "Selected item at index %d", index); - tt::app::AppContext* app = service::loader::getCurrentApp(); - auto bundle = std::make_shared(); - setResultIndex(bundle, (int32_t)index); - app->setResult(app::ResultOk, bundle); - service::loader::stopApp(); -} -static void createButton(lv_obj_t* parent, const std::string& text, size_t index) { - lv_obj_t* button = lv_button_create(parent); - lv_obj_t* button_label = lv_label_create(button); - lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0); - lv_label_set_text(button_label, text.c_str()); - lv_obj_add_event_cb(button, &onButtonClicked, LV_EVENT_SHORT_CLICKED, (void*)index); -} +class AlertDialogApp : public App { -static void onShow(AppContext& app, lv_obj_t* parent) { - auto parameters = app.getParameters(); - tt_check(parameters != nullptr, "Parameters missing"); +private: - std::string title = getTitleParameter(app.getParameters()); - lv_obj_t* toolbar = lvgl::toolbar_create(parent, title); - lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); - - lv_obj_t* message_label = lv_label_create(parent); - lv_obj_align(message_label, LV_ALIGN_CENTER, 0, 0); - lv_obj_set_width(message_label, LV_PCT(80)); - - std::string message; - if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) { - lv_label_set_text(message_label, message.c_str()); - lv_label_set_long_mode(message_label, LV_LABEL_LONG_WRAP); + static void onButtonClickedCallback(lv_event_t* e) { + auto appContext = service::loader::getCurrentAppContext(); + tt_assert(appContext != nullptr); + auto app = std::static_pointer_cast(appContext->getApp()); + app->onButtonClicked(e); } - lv_obj_t* button_wrapper = lv_obj_create(parent); - lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW); - lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(button_wrapper, 0, 0); - lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_border_width(button_wrapper, 0, 0); - lv_obj_align(button_wrapper, LV_ALIGN_BOTTOM_MID, 0, -4); + void onButtonClicked(lv_event_t* e) { + lv_event_code_t code = lv_event_get_code(e); + auto index = reinterpret_cast(lv_event_get_user_data(e)); + TT_LOG_I(TAG, "Selected item at index %d", index); - std::string items_concatenated; - if (parameters->optString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_concatenated)) { - std::vector labels = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN); - if (labels.empty() || labels.front().empty()) { - TT_LOG_E(TAG, "No items provided"); - app.setResult(ResultError); - service::loader::stopApp(); - } else if (labels.size() == 1) { - auto result_bundle = std::make_shared(); - setResultIndex(result_bundle, 0); - app.setResult(ResultOk, result_bundle); - service::loader::stopApp(); - TT_LOG_W(TAG, "Auto-selecting single item"); - } else { - size_t index = 0; - for (const auto& label: labels) { - createButton(button_wrapper, label, index++); + auto bundle = std::make_unique(); + bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index); + setResult(app::Result::Ok, std::move(bundle)); + + service::loader::stopApp(); + } + + static void createButton(lv_obj_t* parent, const std::string& text, size_t index) { + lv_obj_t* button = lv_button_create(parent); + lv_obj_t* button_label = lv_label_create(button); + lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0); + lv_label_set_text(button_label, text.c_str()); + lv_obj_add_event_cb(button, onButtonClickedCallback, LV_EVENT_SHORT_CLICKED, (void*)index); + } +public: + + void onShow(AppContext& app, lv_obj_t* parent) override { + auto parameters = app.getParameters(); + tt_check(parameters != nullptr, "Parameters missing"); + + std::string title = getTitleParameter(app.getParameters()); + lv_obj_t* toolbar = lvgl::toolbar_create(parent, title); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + lv_obj_t* message_label = lv_label_create(parent); + lv_obj_align(message_label, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_width(message_label, LV_PCT(80)); + + std::string message; + if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) { + lv_label_set_text(message_label, message.c_str()); + lv_label_set_long_mode(message_label, LV_LABEL_LONG_WRAP); + } + + lv_obj_t* button_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(button_wrapper, 0, 0); + lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_border_width(button_wrapper, 0, 0); + lv_obj_align(button_wrapper, LV_ALIGN_BOTTOM_MID, 0, -4); + + std::string items_concatenated; + if (parameters->optString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_concatenated)) { + std::vector labels = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN); + if (labels.empty() || labels.front().empty()) { + TT_LOG_E(TAG, "No items provided"); + setResult(Result::Error); + service::loader::stopApp(); + } else if (labels.size() == 1) { + auto result_bundle = std::make_unique(); + result_bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, 0); + setResult(Result::Ok, std::move(result_bundle)); + service::loader::stopApp(); + TT_LOG_W(TAG, "Auto-selecting single item"); + } else { + size_t index = 0; + for (const auto& label: labels) { + createButton(button_wrapper, label, index++); + } } } } -} +}; extern const AppManifest manifest = { - .id = "AlertDialog", - .name = "Alert Dialog", - .type = TypeHidden, - .onShow = onShow + .id = "AlertDialog", + .name = "Alert Dialog", + .type = Type::Hidden, + .createApp = create }; } diff --git a/Tactility/Source/app/applist/AppList.cpp b/Tactility/Source/app/applist/AppList.cpp index 1da03aef..04f9d9b0 100644 --- a/Tactility/Source/app/applist/AppList.cpp +++ b/Tactility/Source/app/applist/AppList.cpp @@ -8,54 +8,61 @@ namespace tt::app::applist { -static void onAppPressed(lv_event_t* e) { - const auto* manifest = static_cast(lv_event_get_user_data(e)); - service::loader::startApp(manifest->id); -} -static void createAppWidget(const AppManifest* manifest, void* parent) { - tt_check(parent); - auto* list = reinterpret_cast(parent); - const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; - lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str()); - lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest); -} +class AppListApp : public App { -static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { - auto* toolbar = lvgl::toolbar_create(parent, app); - lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); +private: - lv_obj_t* list = lv_list_create(parent); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_align_to(list, toolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0); - - auto toolbar_height = lv_obj_get_height(toolbar); - auto parent_content_height = lv_obj_get_content_height(parent); - lv_obj_set_height(list, parent_content_height - toolbar_height); - - auto manifests = getApps(); - std::sort(manifests.begin(), manifests.end(), SortAppManifestByName); - - lv_list_add_text(list, "User"); - for (const auto& manifest: manifests) { - if (manifest->type == TypeUser) { - createAppWidget(manifest, list); - } + static void onAppPressed(lv_event_t* e) { + const auto* manifest = static_cast(lv_event_get_user_data(e)); + service::loader::startApp(manifest->id); } - lv_list_add_text(list, "System"); - for (const auto& manifest: manifests) { - if (manifest->type == TypeSystem) { - createAppWidget(manifest, list); + static void createAppWidget(const std::shared_ptr& manifest, lv_obj_t* list) { + const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; + lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str()); + lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest.get()); + } + +public: + + void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override { + auto* toolbar = lvgl::toolbar_create(parent, app); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + lv_obj_t* list = lv_list_create(parent); + lv_obj_set_width(list, LV_PCT(100)); + lv_obj_align_to(list, toolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0); + + auto toolbar_height = lv_obj_get_height(toolbar); + auto parent_content_height = lv_obj_get_content_height(parent); + lv_obj_set_height(list, parent_content_height - toolbar_height); + + auto manifests = getApps(); + std::sort(manifests.begin(), manifests.end(), SortAppManifestByName); + + lv_list_add_text(list, "User"); + for (const auto& manifest: manifests) { + if (manifest->type == Type::User) { + createAppWidget(manifest, list); + } + } + + lv_list_add_text(list, "System"); + for (const auto& manifest: manifests) { + if (manifest->type == Type::System) { + createAppWidget(manifest, list); + } } } -} +}; + extern const AppManifest manifest = { .id = "AppList", .name = "Apps", - .type = TypeHidden, - .onShow = onShow, + .type = Type::Hidden, + .createApp = create, }; } // namespace diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index df4e16eb..57069d0e 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -24,101 +24,93 @@ namespace tt::app::boot { -static int32_t bootThreadCallback(void* context); -static void startNextApp(); +class BootApp : public App { -struct Data { - Data() : thread("boot", 4096, bootThreadCallback, this) {} +private: - Thread thread; -}; + Thread thread = Thread("boot", 4096, bootThreadCallback, this); -static int32_t bootThreadCallback(TT_UNUSED void* context) { - TickType_t start_time = kernel::getTicks(); + static int32_t bootThreadCallback(TT_UNUSED void* context) { + TickType_t start_time = kernel::getTicks(); - kernel::systemEventPublish(kernel::SystemEvent::BootSplash); + kernel::systemEventPublish(kernel::SystemEvent::BootSplash); - auto* lvgl_display = lv_display_get_default(); - tt_assert(lvgl_display != nullptr); - auto* hal_display = (hal::Display*)lv_display_get_user_data(lvgl_display); - tt_assert(hal_display != nullptr); - if (hal_display->supportsBacklightDuty()) { - int32_t backlight_duty = app::display::getBacklightDuty(); - hal_display->setBacklightDuty(backlight_duty); - } - - if (hal::usb::isUsbBootMode()) { - TT_LOG_I(TAG, "Rebooting into mass storage device mode"); - hal::usb::resetUsbBootMode(); - hal::usb::startMassStorageWithSdmmc(); - } else { - TickType_t end_time = tt::kernel::getTicks(); - TickType_t ticks_passed = end_time - start_time; - TickType_t minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS); - if (minimum_ticks > ticks_passed) { - kernel::delayTicks(minimum_ticks - ticks_passed); + auto* lvgl_display = lv_display_get_default(); + tt_assert(lvgl_display != nullptr); + auto* hal_display = (hal::Display*)lv_display_get_user_data(lvgl_display); + tt_assert(hal_display != nullptr); + if (hal_display->supportsBacklightDuty()) { + int32_t backlight_duty = app::display::getBacklightDuty(); + hal_display->setBacklightDuty(backlight_duty); } - tt::service::loader::stopApp(); - startNextApp(); + if (hal::usb::isUsbBootMode()) { + TT_LOG_I(TAG, "Rebooting into mass storage device mode"); + hal::usb::resetUsbBootMode(); + hal::usb::startMassStorageWithSdmmc(); + } else { + TickType_t end_time = tt::kernel::getTicks(); + TickType_t ticks_passed = end_time - start_time; + TickType_t minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS); + if (minimum_ticks > ticks_passed) { + kernel::delayTicks(minimum_ticks - ticks_passed); + } + + tt::service::loader::stopApp(); + startNextApp(); + } + + return 0; } - return 0; -} - - -static void startNextApp() { + static void startNextApp() { #ifdef ESP_PLATFORM - esp_reset_reason_t reason = esp_reset_reason(); - if (reason == ESP_RST_PANIC) { - app::crashdiagnostics::start(); - return; - } + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_PANIC) { + app::crashdiagnostics::start(); + return; + } #endif - auto* config = tt::getConfiguration(); - if (config->autoStartAppId) { - TT_LOG_I(TAG, "init auto-starting %s", config->autoStartAppId); - tt::service::loader::startApp(config->autoStartAppId); - } else { - app::launcher::start(); + auto* config = tt::getConfiguration(); + if (config->autoStartAppId) { + TT_LOG_I(TAG, "init auto-starting %s", config->autoStartAppId); + tt::service::loader::startApp(config->autoStartAppId); + } else { + app::launcher::start(); + } } -} -static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { - auto data = std::static_pointer_cast(app.getData()); +public: - auto* image = lv_image_create(parent); - lv_obj_set_size(image, LV_PCT(100), LV_PCT(100)); + void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override { + auto* image = lv_image_create(parent); + lv_obj_set_size(image, LV_PCT(100), LV_PCT(100)); - auto paths = app.getPaths(); - const char* logo = hal::usb::isUsbBootMode() ? "logo_usb.png" : "logo.png"; - auto logo_path = paths->getSystemPathLvgl(logo); - TT_LOG_I(TAG, "%s", logo_path.c_str()); - lv_image_set_src(image, logo_path.c_str()); + auto paths = app.getPaths(); + const char* logo = hal::usb::isUsbBootMode() ? "logo_usb.png" : "logo.png"; + auto logo_path = paths->getSystemPathLvgl(logo); + TT_LOG_I(TAG, "%s", logo_path.c_str()); + lv_image_set_src(image, logo_path.c_str()); - lvgl::obj_set_style_bg_blacken(parent); + lvgl::obj_set_style_bg_blacken(parent); - data->thread.start(); -} + // Just in case this app is somehow resumed + if (thread.getState() == Thread::State::Stopped) { + thread.start(); + } + } -static void onStart(AppContext& app) { - auto data = std::make_shared(); - app.setData(data); -} - -static void onStop(AppContext& app) { - auto data = std::static_pointer_cast(app.getData()); - data->thread.join(); -} + void onStop(AppContext& app) override { + thread.join(); + } +}; extern const AppManifest manifest = { .id = "Boot", .name = "Boot", - .type = TypeBoot, - .onStart = onStart, - .onStop = onStop, - .onShow = onShow, + .type = Type::Boot, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp index 88d46736..552f9872 100644 --- a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp +++ b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp @@ -17,103 +17,108 @@ void onContinuePressed(TT_UNUSED lv_event_t* event) { tt::app::launcher::start(); } -static void onShow(AppContext& app, lv_obj_t* parent) { - auto* display = lv_obj_get_display(parent); - int32_t parent_height = lv_display_get_vertical_resolution(display) - STATUSBAR_HEIGHT; +class CrashDiagnosticsApp : public App { - lv_obj_add_event_cb(parent, onContinuePressed, LV_EVENT_SHORT_CLICKED, nullptr); - auto* top_label = lv_label_create(parent); - lv_label_set_text(top_label, "Oops! We've crashed ..."); // TODO: Funny messages - lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2); +public: - auto* bottom_label = lv_label_create(parent); - lv_label_set_text(bottom_label, "Tap screen to continue"); - lv_obj_align(bottom_label, LV_ALIGN_BOTTOM_MID, 0, -2); + void onShow(AppContext& app, lv_obj_t* parent) override { + auto* display = lv_obj_get_display(parent); + int32_t parent_height = lv_display_get_vertical_resolution(display) - STATUSBAR_HEIGHT; - std::string url = getUrlFromCrashData(); - TT_LOG_I(TAG, "%s", url.c_str()); - size_t url_length = url.length(); + lv_obj_add_event_cb(parent, onContinuePressed, LV_EVENT_SHORT_CLICKED, nullptr); + auto* top_label = lv_label_create(parent); + lv_label_set_text(top_label, "Oops! We've crashed ..."); // TODO: Funny messages + lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2); - int qr_version; - if (!getQrVersionForBinaryDataLength(url_length, qr_version)) { - TT_LOG_E(TAG, "QR is too large"); - service::loader::stopApp(); - return; - } + auto* bottom_label = lv_label_create(parent); + lv_label_set_text(bottom_label, "Tap screen to continue"); + lv_obj_align(bottom_label, LV_ALIGN_BOTTOM_MID, 0, -2); - TT_LOG_I(TAG, "QR version %d (length: %d)", qr_version, url_length); - auto qrcodeData = std::make_shared(qrcode_getBufferSize(qr_version)); - if (qrcodeData == nullptr) { - TT_LOG_E(TAG, "Failed to allocate QR buffer"); - service::loader::stopApp(); - return; - } + std::string url = getUrlFromCrashData(); + TT_LOG_I(TAG, "%s", url.c_str()); + size_t url_length = url.length(); - QRCode qrcode; - TT_LOG_I(TAG, "QR init text"); - if (qrcode_initText(&qrcode, qrcodeData.get(), qr_version, ECC_LOW, url.c_str()) != 0) { - TT_LOG_E(TAG, "QR init text failed"); - service::loader::stopApp(); - return; - } + int qr_version; + if (!getQrVersionForBinaryDataLength(url_length, qr_version)) { + TT_LOG_E(TAG, "QR is too large"); + service::loader::stopApp(); + return; + } - TT_LOG_I(TAG, "QR size: %d", qrcode.size); + TT_LOG_I(TAG, "QR version %d (length: %d)", qr_version, url_length); + auto qrcodeData = std::make_shared(qrcode_getBufferSize(qr_version)); + if (qrcodeData == nullptr) { + TT_LOG_E(TAG, "Failed to allocate QR buffer"); + service::loader::stopApp(); + return; + } - // Calculate QR dot size - int32_t top_label_height = lv_obj_get_height(top_label) + 2; - int32_t bottom_label_height = lv_obj_get_height(bottom_label) + 2; - TT_LOG_I(TAG, "Create canvas"); - int32_t available_height = parent_height - top_label_height - bottom_label_height; - int32_t available_width = lv_display_get_horizontal_resolution(display); - int32_t smallest_size = TT_MIN(available_height, available_width); - int32_t pixel_size; - if (qrcode.size * 2 <= smallest_size) { - pixel_size = 2; - } else if (qrcode.size <= smallest_size) { - pixel_size = 1; - } else { - TT_LOG_E(TAG, "QR code won't fit screen"); - service::loader::stopApp(); - return; - } + QRCode qrcode; + TT_LOG_I(TAG, "QR init text"); + if (qrcode_initText(&qrcode, qrcodeData.get(), qr_version, ECC_LOW, url.c_str()) != 0) { + TT_LOG_E(TAG, "QR init text failed"); + service::loader::stopApp(); + return; + } - auto* canvas = lv_canvas_create(parent); - lv_obj_set_size(canvas, pixel_size * qrcode.size, pixel_size * qrcode.size); - lv_obj_align(canvas, LV_ALIGN_CENTER, 0, 0); - lv_canvas_fill_bg(canvas, lv_color_black(), LV_OPA_COVER); - lv_obj_set_content_height(canvas, qrcode.size * pixel_size); - lv_obj_set_content_width(canvas, qrcode.size * pixel_size); + TT_LOG_I(TAG, "QR size: %d", qrcode.size); - TT_LOG_I(TAG, "Create draw buffer"); - auto* draw_buf = lv_draw_buf_create(pixel_size * qrcode.size, pixel_size * qrcode.size, LV_COLOR_FORMAT_RGB565, LV_STRIDE_AUTO); - if (draw_buf == nullptr) { - TT_LOG_E(TAG, "Draw buffer alloc"); - service::loader::stopApp(); - return; - } + // Calculate QR dot size + int32_t top_label_height = lv_obj_get_height(top_label) + 2; + int32_t bottom_label_height = lv_obj_get_height(bottom_label) + 2; + TT_LOG_I(TAG, "Create canvas"); + int32_t available_height = parent_height - top_label_height - bottom_label_height; + int32_t available_width = lv_display_get_horizontal_resolution(display); + int32_t smallest_size = TT_MIN(available_height, available_width); + int32_t pixel_size; + if (qrcode.size * 2 <= smallest_size) { + pixel_size = 2; + } else if (qrcode.size <= smallest_size) { + pixel_size = 1; + } else { + TT_LOG_E(TAG, "QR code won't fit screen"); + service::loader::stopApp(); + return; + } - lv_canvas_set_draw_buf(canvas, draw_buf); + auto* canvas = lv_canvas_create(parent); + lv_obj_set_size(canvas, pixel_size * qrcode.size, pixel_size * qrcode.size); + lv_obj_align(canvas, LV_ALIGN_CENTER, 0, 0); + lv_canvas_fill_bg(canvas, lv_color_black(), LV_OPA_COVER); + lv_obj_set_content_height(canvas, qrcode.size * pixel_size); + lv_obj_set_content_width(canvas, qrcode.size * pixel_size); - for (uint8_t y = 0; y < qrcode.size; y++) { - for (uint8_t x = 0; x < qrcode.size; x++) { - bool colored = qrcode_getModule(&qrcode, x, y); - auto color = colored ? lv_color_white() : lv_color_black(); - int32_t pos_x = x * pixel_size; - int32_t pos_y = y * pixel_size; - for (int px = 0; px < pixel_size; px++) { - for (int py = 0; py < pixel_size; py++) { - lv_canvas_set_px(canvas, pos_x + px, pos_y + py, color, LV_OPA_COVER); + TT_LOG_I(TAG, "Create draw buffer"); + auto* draw_buf = lv_draw_buf_create(pixel_size * qrcode.size, pixel_size * qrcode.size, LV_COLOR_FORMAT_RGB565, LV_STRIDE_AUTO); + if (draw_buf == nullptr) { + TT_LOG_E(TAG, "Draw buffer alloc"); + service::loader::stopApp(); + return; + } + + lv_canvas_set_draw_buf(canvas, draw_buf); + + for (uint8_t y = 0; y < qrcode.size; y++) { + for (uint8_t x = 0; x < qrcode.size; x++) { + bool colored = qrcode_getModule(&qrcode, x, y); + auto color = colored ? lv_color_white() : lv_color_black(); + int32_t pos_x = x * pixel_size; + int32_t pos_y = y * pixel_size; + for (int px = 0; px < pixel_size; px++) { + for (int py = 0; py < pixel_size; py++) { + lv_canvas_set_px(canvas, pos_x + px, pos_y + py, color, LV_OPA_COVER); + } } } } } -} +}; extern const AppManifest manifest = { .id = "CrashDiagnostics", .name = "Crash Diagnostics", - .type = TypeHidden, - .onShow = onShow + .type = Type::Hidden, + .createApp = create }; void start() { diff --git a/Tactility/Source/app/display/Display.cpp b/Tactility/Source/app/display/Display.cpp index 305fe501..0cb6cf47 100644 --- a/Tactility/Source/app/display/Display.cpp +++ b/Tactility/Source/app/display/Display.cpp @@ -98,92 +98,92 @@ static void onOrientationSet(lv_event_t* event) { } } -static void onShow(AppContext& app, lv_obj_t* parent) { - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); +class DisplayApp : public App { - lvgl::toolbar_create(parent, app); + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - lv_obj_t* main_wrapper = lv_obj_create(parent); - lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_width(main_wrapper, LV_PCT(100)); - lv_obj_set_flex_grow(main_wrapper, 1); + lvgl::toolbar_create(parent, app); - lv_obj_t* wrapper = lv_obj_create(main_wrapper); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_style_pad_all(wrapper, 8, 0); - lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_t* main_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_width(main_wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(main_wrapper, 1); - lv_obj_t* brightness_label = lv_label_create(wrapper); - lv_label_set_text(brightness_label, "Brightness"); + lv_obj_t* wrapper = lv_obj_create(main_wrapper); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_style_pad_all(wrapper, 8, 0); + lv_obj_set_style_border_width(wrapper, 0, 0); - lv_obj_t* brightness_slider = lv_slider_create(wrapper); - lv_obj_set_width(brightness_slider, LV_PCT(50)); - lv_obj_align(brightness_slider, LV_ALIGN_TOP_RIGHT, -8, 0); - lv_slider_set_range(brightness_slider, 0, 255); - lv_obj_add_event_cb(brightness_slider, onBacklightSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr); + lv_obj_t* brightness_label = lv_label_create(wrapper); + lv_label_set_text(brightness_label, "Brightness"); - lv_obj_t* gamma_label = lv_label_create(wrapper); - lv_label_set_text(gamma_label, "Gamma"); - lv_obj_set_y(gamma_label, 40); + lv_obj_t* brightness_slider = lv_slider_create(wrapper); + lv_obj_set_width(brightness_slider, LV_PCT(50)); + lv_obj_align(brightness_slider, LV_ALIGN_TOP_RIGHT, -8, 0); + lv_slider_set_range(brightness_slider, 0, 255); + lv_obj_add_event_cb(brightness_slider, onBacklightSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr); - lv_obj_t* gamma_slider = lv_slider_create(wrapper); - lv_obj_set_width(gamma_slider, LV_PCT(50)); - lv_obj_align(gamma_slider, LV_ALIGN_TOP_RIGHT, -8, 40); - lv_slider_set_range(gamma_slider, 0, getHalDisplay(parent)->getGammaCurveCount()); - lv_obj_add_event_cb(gamma_slider, onGammaSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr); + lv_obj_t* gamma_label = lv_label_create(wrapper); + lv_label_set_text(gamma_label, "Gamma"); + lv_obj_set_y(gamma_label, 40); - auto* hal_display = getHalDisplay(parent); - tt_assert(hal_display != nullptr); + lv_obj_t* gamma_slider = lv_slider_create(wrapper); + lv_obj_set_width(gamma_slider, LV_PCT(50)); + lv_obj_align(gamma_slider, LV_ALIGN_TOP_RIGHT, -8, 40); + lv_slider_set_range(gamma_slider, 0, getHalDisplay(parent)->getGammaCurveCount()); + lv_obj_add_event_cb(gamma_slider, onGammaSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr); - if (!hal_display->supportsBacklightDuty()) { - lv_slider_set_value(brightness_slider, 255, LV_ANIM_OFF); - lv_obj_add_state(brightness_slider, LV_STATE_DISABLED); - } else { - uint8_t value = getBacklightDuty(); - lv_slider_set_value(brightness_slider, value, LV_ANIM_OFF); + auto* hal_display = getHalDisplay(parent); + tt_assert(hal_display != nullptr); + + if (!hal_display->supportsBacklightDuty()) { + lv_slider_set_value(brightness_slider, 255, LV_ANIM_OFF); + lv_obj_add_state(brightness_slider, LV_STATE_DISABLED); + } else { + uint8_t value = getBacklightDuty(); + lv_slider_set_value(brightness_slider, value, LV_ANIM_OFF); + } + + lv_slider_set_value(gamma_slider, 128, LV_ANIM_OFF); + + lv_obj_t* orientation_label = lv_label_create(wrapper); + lv_label_set_text(orientation_label, "Orientation"); + lv_obj_align(orientation_label, LV_ALIGN_TOP_LEFT, 0, 80); + + auto lvgl_display = lv_obj_get_display(parent); + auto horizontal_px = lv_display_get_horizontal_resolution(lvgl_display); + auto vertical_px = lv_display_get_vertical_resolution(lvgl_display); + bool is_landscape_display = horizontal_px > vertical_px; + + lv_obj_t* orientation_dropdown = lv_dropdown_create(wrapper); + if (is_landscape_display) { + lv_dropdown_set_options(orientation_dropdown, "Landscape\nLandscape (flipped)\nPortrait Left\nPortrait Right"); + } else { + lv_dropdown_set_options(orientation_dropdown, "Portrait\nPortrait (flipped)\nLandscape Left\nLandscape Right"); + } + + lv_obj_align(orientation_dropdown, LV_ALIGN_TOP_RIGHT, 0, 72); + lv_obj_add_event_cb(orientation_dropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, nullptr); + uint32_t orientation_selected = dipslayOrientationToOrientationSetting( + lv_display_get_rotation(lv_display_get_default()) + ); + lv_dropdown_set_selected(orientation_dropdown, orientation_selected); } - lv_slider_set_value(gamma_slider, 128, LV_ANIM_OFF); - - lv_obj_t* orientation_label = lv_label_create(wrapper); - lv_label_set_text(orientation_label, "Orientation"); - lv_obj_align(orientation_label, LV_ALIGN_TOP_LEFT, 0, 80); - - auto lvgl_display = lv_obj_get_display(parent); - auto horizontal_px = lv_display_get_horizontal_resolution(lvgl_display); - auto vertical_px = lv_display_get_vertical_resolution(lvgl_display); - bool is_landscape_display = horizontal_px > vertical_px; - - lv_obj_t* orientation_dropdown = lv_dropdown_create(wrapper); - if (is_landscape_display) { - lv_dropdown_set_options(orientation_dropdown, "Landscape\nLandscape (flipped)\nPortrait Left\nPortrait Right"); - } else { - lv_dropdown_set_options(orientation_dropdown, "Portrait\nPortrait (flipped)\nLandscape Left\nLandscape Right"); + void onHide(TT_UNUSED AppContext& app) override { + if (backlight_duty_set) { + setBacklightDuty(backlight_duty); + } } - - lv_obj_align(orientation_dropdown, LV_ALIGN_TOP_RIGHT, 0, 72); - lv_obj_add_event_cb(orientation_dropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, nullptr); - uint32_t orientation_selected = dipslayOrientationToOrientationSetting( - lv_display_get_rotation(lv_display_get_default()) - ); - lv_dropdown_set_selected(orientation_dropdown, orientation_selected); -} - -static void onHide(TT_UNUSED AppContext& app) { - if (backlight_duty_set) { - setBacklightDuty(backlight_duty); - } -} +}; extern const AppManifest manifest = { .id = "Display", .name = "Display", .icon = TT_ASSETS_APP_ICON_DISPLAY_SETTINGS, - .type = TypeSettings, - .onStart = nullptr, - .onStop = nullptr, - .onShow = onShow, - .onHide = onHide + .type = Type::Settings, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/files/Files.cpp b/Tactility/Source/app/files/Files.cpp index 41ba5831..e5d7ac3e 100644 --- a/Tactility/Source/app/files/Files.cpp +++ b/Tactility/Source/app/files/Files.cpp @@ -1,4 +1,5 @@ -#include "app/files/FilesPrivate.h" +#include "app/files/View.h" +#include "app/files/State.h" #include "app/AppContext.h" #include "Assets.h" @@ -12,31 +13,31 @@ namespace tt::app::files { extern const AppManifest manifest; -/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ +class FilesApp : public App { + std::unique_ptr view; + std::shared_ptr state; -static void onShow(AppContext& app, lv_obj_t* parent) { - auto files = std::static_pointer_cast(app.getData()); - files->onShow(parent); -} +public: + FilesApp() { + state = std::make_shared(); + view = std::make_unique(state); + } -static void onStart(AppContext& app) { - auto files = std::make_shared(); - app.setData(files); -} + void onShow(AppContext& appContext, lv_obj_t* parent) override { + view->init(parent); + } -static void onResult(AppContext& app, Result result, const Bundle& bundle) { - auto files = std::static_pointer_cast(app.getData()); - files->onResult(result, bundle); -} + void onResult(AppContext& appContext, Result result, std::unique_ptr bundle) override { + view->onResult(result, std::move(bundle)); + } +}; extern const AppManifest manifest = { .id = "Files", .name = "Files", .icon = TT_ASSETS_APP_ICON_FILES, - .type = TypeHidden, - .onStart = onStart, - .onShow = onShow, - .onResult = onResult + .type = Type::Hidden, + .createApp = create }; void start() { diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index 283552cd..e1370dd6 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -8,12 +8,15 @@ #include "app/ElfApp.h" #include "lvgl/Toolbar.h" #include "lvgl/LvglSync.h" -#include "service/loader/Loader.h" #include "Tactility.h" #include "StringUtils.h" #include #include +#ifdef ESP_PLATFORM +#include "service/loader/Loader.h" +#endif + #define TAG "files_app" namespace tt::app::files { @@ -81,7 +84,9 @@ void View::viewFile(const std::string& path, const std::string& filename) { if (isSupportedExecutableFile(filename)) { #ifdef ESP_PLATFORM - app::startElfApp(processed_filepath); + app::registerElfApp(processed_filepath); + auto app_id = app::getElfAppId(processed_filepath); + service::loader::startApp(app_id); #endif } else if (isSupportedImageFile(filename)) { app::imageviewer::start(processed_filepath); @@ -282,8 +287,8 @@ void View::onNavigate() { } } -void View::onResult(Result result, const Bundle& bundle) { - if (result != ResultOk) { +void View::onResult(Result result, std::unique_ptr bundle) { + if (result != Result::Ok || bundle == nullptr) { return; } @@ -292,7 +297,7 @@ void View::onResult(Result result, const Bundle& bundle) { switch (state->getPendingAction()) { case State::ActionDelete: { - if (alertdialog::getResultIndex(bundle) == 0) { + if (alertdialog::getResultIndex(*bundle) == 0) { int delete_count = (int)remove(filepath.c_str()); if (delete_count > 0) { TT_LOG_I(TAG, "Deleted %d items", delete_count); @@ -305,7 +310,7 @@ void View::onResult(Result result, const Bundle& bundle) { break; } case State::ActionRename: { - auto new_name = app::inputdialog::getResult(bundle); + auto new_name = app::inputdialog::getResult(*bundle); if (!new_name.empty() && new_name != state->getSelectedChildEntry()) { std::string rename_to = getChildPath(state->getCurrentPath(), new_name); if (rename(filepath.c_str(), rename_to.c_str())) { diff --git a/Tactility/Source/app/gpio/Gpio.cpp b/Tactility/Source/app/gpio/Gpio.cpp index 4a4ba7da..0c01be70 100644 --- a/Tactility/Source/app/gpio/Gpio.cpp +++ b/Tactility/Source/app/gpio/Gpio.cpp @@ -1,6 +1,5 @@ #include "Mutex.h" #include "Thread.h" -#include "Tactility.h" #include "service/loader/Loader.h" #include "lvgl/Toolbar.h" @@ -10,7 +9,9 @@ namespace tt::app::gpio { -class Gpio { +extern const AppManifest manifest; + +class GpioApp : public App { private: @@ -19,6 +20,9 @@ private: std::unique_ptr timer; Mutex mutex; + static lv_obj_t* createGpioRowWrapper(lv_obj_t* parent); + static void onTimer(TT_UNUSED std::shared_ptr context); + public: void lock() const { @@ -29,18 +33,17 @@ public: tt_check(mutex.release() == TtStatusOk); } - void onShow(AppContext& app, lv_obj_t* parent); - void onHide(AppContext& app); + void onShow(AppContext& app, lv_obj_t* parent) override; + void onHide(AppContext& app) override; - void startTask(std::shared_ptr ptr); + void startTask(); void stopTask(); void updatePinStates(); void updatePinWidgets(); }; - -void Gpio::updatePinStates() { +void GpioApp::updatePinStates() { lock(); // Update pin states for (int i = 0; i < GPIO_NUM_MAX; ++i) { @@ -53,7 +56,7 @@ void Gpio::updatePinStates() { unlock(); } -void Gpio::updatePinWidgets() { +void GpioApp::updatePinWidgets() { if (lvgl::lock(100)) { lock(); for (int j = 0; j < GPIO_NUM_MAX; ++j) { @@ -75,7 +78,7 @@ void Gpio::updatePinWidgets() { } } -static lv_obj_t* createGpioRowWrapper(lv_obj_t* parent) { +lv_obj_t* GpioApp::createGpioRowWrapper(lv_obj_t* parent) { lv_obj_t* wrapper = lv_obj_create(parent); lv_obj_set_style_pad_all(wrapper, 0, 0); lv_obj_set_style_border_width(wrapper, 0, 0); @@ -85,26 +88,29 @@ static lv_obj_t* createGpioRowWrapper(lv_obj_t* parent) { // region Task -static void onTimer(std::shared_ptr context) { - auto gpio = std::static_pointer_cast(context); - - gpio->updatePinStates(); - gpio->updatePinWidgets(); +void GpioApp::onTimer(TT_UNUSED std::shared_ptr context) { + auto appContext = service::loader::getCurrentAppContext(); + if (appContext->getManifest().id == manifest.id) { + auto app = std::static_pointer_cast(appContext->getApp()); + if (app != nullptr) { + app->updatePinStates(); + app->updatePinWidgets(); + } + } } -void Gpio::startTask(std::shared_ptr ptr) { +void GpioApp::startTask() { lock(); tt_assert(timer == nullptr); timer = std::make_unique( Timer::Type::Periodic, - &onTimer, - ptr + &onTimer ); timer->start(100 / portTICK_PERIOD_MS); unlock(); } -void Gpio::stopTask() { +void GpioApp::stopTask() { tt_assert(timer); timer->stop(); @@ -114,9 +120,7 @@ void Gpio::stopTask() { // endregion Task -void Gpio::onShow(AppContext& app, lv_obj_t* parent) { - auto gpio = std::static_pointer_cast(app.getData()); - +void GpioApp::onShow(AppContext& app, lv_obj_t* parent) { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_t* toolbar = lvgl::toolbar_create(parent, app); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); @@ -139,7 +143,7 @@ void Gpio::onShow(AppContext& app, lv_obj_t* parent) { lv_obj_t* row_wrapper = createGpioRowWrapper(wrapper); lv_obj_align(row_wrapper, LV_ALIGN_TOP_MID, 0, 0); - gpio->lock(); + lock(); for (int i = GPIO_NUM_MIN; i < GPIO_NUM_MAX; ++i) { // Add the GPIO number before the first item on a row @@ -152,7 +156,7 @@ void Gpio::onShow(AppContext& app, lv_obj_t* parent) { lv_obj_t* status_label = lv_label_create(row_wrapper); lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing), 0); lv_label_set_text_fmt(status_label, "%s", LV_SYMBOL_STOP); - gpio->lvPins[i] = status_label; + lvPins[i] = status_label; column++; @@ -170,42 +174,20 @@ void Gpio::onShow(AppContext& app, lv_obj_t* parent) { column = 0; } } - gpio->unlock(); + unlock(); - gpio->startTask(gpio); + startTask(); } -void Gpio::onHide(AppContext& app) { - auto gpio = std::static_pointer_cast(app.getData()); - gpio->stopTask(); +void GpioApp::onHide(AppContext& app) { + stopTask(); } -// region App lifecycle - -static void onShow(AppContext& app, lv_obj_t* parent) { - auto gpio = std::static_pointer_cast(app.getData()); - gpio->onShow(app, parent); -} - -static void onHide(AppContext& app) { - auto gpio = std::static_pointer_cast(app.getData()); - gpio->onHide(app); -} - -static void onStart(AppContext& app) { - auto gpio = std::make_shared(); - app.setData(gpio); -} - -// endregion App lifecycle - extern const AppManifest manifest = { .id = "Gpio", .name = "GPIO", - .type = TypeSystem, - .onStart = onStart, - .onShow = onShow, - .onHide = onHide + .type = Type::System, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/i2cscanner/I2cScanner.cpp b/Tactility/Source/app/i2cscanner/I2cScanner.cpp index e4dad0c0..fc6bb5ab 100644 --- a/Tactility/Source/app/i2cscanner/I2cScanner.cpp +++ b/Tactility/Source/app/i2cscanner/I2cScanner.cpp @@ -14,108 +14,64 @@ namespace tt::app::i2cscanner { -static void updateViews(std::shared_ptr data); extern const AppManifest manifest; +class I2cScannerApp : public App { + +private: + + // Core + Mutex mutex = Mutex(Mutex::Type::Recursive); + std::unique_ptr scanTimer = nullptr; + // State + ScanState scanState = ScanStateInitial; + i2c_port_t port = I2C_NUM_0; + std::vector scannedAddresses; + // Widgets + lv_obj_t* scanButtonLabelWidget = nullptr; + lv_obj_t* portDropdownWidget = nullptr; + lv_obj_t* scanListWidget = nullptr; + + static void onSelectBusCallback(lv_event_t* event); + static void onPressScanCallback(lv_event_t* event); + static void onScanTimerCallback(std::shared_ptr context); + + void onSelectBus(lv_event_t* event); + void onPressScan(lv_event_t* event); + void onScanTimer(); + + bool shouldStopScanTimer(); + bool getPort(i2c_port_t* outPort); + bool addAddressToList(uint8_t address); + bool hasScanThread(); + void startScanning(); + void stopScanning(); + + void updateViews(); + void updateViewsSafely(); + + void onScanTimerFinished(); + +public: + + void onShow(AppContext& app, lv_obj_t* parent) override; + void onHide(AppContext& app) override; +}; + /** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ -std::shared_ptr _Nullable optData() { - app::AppContext* app = service::loader::getCurrentApp(); - if (app->getManifest().id == manifest.id) { - return std::static_pointer_cast(app->getData()); +std::shared_ptr _Nullable optApp() { + auto appContext = service::loader::getCurrentAppContext(); + if (appContext != nullptr && appContext->getManifest().id == manifest.id) { + return std::static_pointer_cast(appContext->getApp()); } else { return nullptr; } } -static void onSelectBus(lv_event_t* event) { - auto data = optData(); - if (data == nullptr) { - return; - } +// region Lifecycle - auto* dropdown = static_cast(lv_event_get_target(event)); - uint32_t selected = lv_dropdown_get_selected(dropdown); - auto i2c_devices = tt::getConfiguration()->hardware->i2c; - assert(selected < i2c_devices.size()); - if (data->mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { - data->scannedAddresses.clear(); - data->port = i2c_devices[selected].port; - data->scanState = ScanStateInitial; - tt_check(data->mutex.release() == TtStatusOk); - - updateViews(data); - } - - TT_LOG_I(TAG, "Selected %ld", selected); -} - -static void onPressScan(TT_UNUSED lv_event_t* event) { - auto data = optData(); - if (data != nullptr) { - if (data->scanState == ScanStateScanning) { - stopScanning(data); - } else { - startScanning(data); - } - updateViews(data); - } -} - -static void updateViews(std::shared_ptr data) { - if (data->mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { - if (data->scanState == ScanStateScanning) { - lv_label_set_text(data->scanButtonLabelWidget, STOP_SCAN_TEXT); - lv_obj_remove_flag(data->portDropdownWidget, LV_OBJ_FLAG_CLICKABLE); - } else { - lv_label_set_text(data->scanButtonLabelWidget, START_SCAN_TEXT); - lv_obj_add_flag(data->portDropdownWidget, LV_OBJ_FLAG_CLICKABLE); - } - - lv_obj_clean(data->scanListWidget); - if (data->scanState == ScanStateStopped) { - lv_obj_remove_flag(data->scanListWidget, LV_OBJ_FLAG_HIDDEN); - if (!data->scannedAddresses.empty()) { - for (auto address: data->scannedAddresses) { - std::string address_text = getAddressText(address); - lv_list_add_text(data->scanListWidget, address_text.c_str()); - } - } else { - lv_list_add_text(data->scanListWidget, "No devices found"); - } - } else { - lv_obj_add_flag(data->scanListWidget, LV_OBJ_FLAG_HIDDEN); - } - - tt_check(data->mutex.release() == TtStatusOk); - } else { - TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "updateViews"); - } -} - -static void updateViewsSafely(std::shared_ptr data) { - if (lvgl::lock(100 / portTICK_PERIOD_MS)) { - updateViews(data); - lvgl::unlock(); - } else { - TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "updateViewsSafely"); - } -} - -void onScanTimerFinished(std::shared_ptr data) { - if (data->mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { - if (data->scanState == ScanStateScanning) { - data->scanState = ScanStateStopped; - updateViewsSafely(data); - } - tt_check(data->mutex.release() == TtStatusOk); - } else { - TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "onScanTimerFinished"); - } -} - -static void onShow(AppContext& app, lv_obj_t* parent) { - auto data = std::static_pointer_cast(app.getData()); +void I2cScannerApp::onShow(AppContext& app, lv_obj_t* parent) { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lvgl::toolbar_create(parent, app); @@ -134,61 +90,266 @@ static void onShow(AppContext& app, lv_obj_t* parent) { lv_obj_t* scan_button = lv_button_create(wrapper); lv_obj_set_width(scan_button, LV_PCT(48)); lv_obj_align(scan_button, LV_ALIGN_TOP_LEFT, 0, 1); // Shift 1 pixel to align with selection box - lv_obj_add_event_cb(scan_button, &onPressScan, LV_EVENT_SHORT_CLICKED, nullptr); + lv_obj_add_event_cb(scan_button, onPressScanCallback, LV_EVENT_SHORT_CLICKED, this); lv_obj_t* scan_button_label = lv_label_create(scan_button); lv_obj_align(scan_button_label, LV_ALIGN_CENTER, 0, 0); lv_label_set_text(scan_button_label, START_SCAN_TEXT); - data->scanButtonLabelWidget = scan_button_label; + scanButtonLabelWidget = scan_button_label; lv_obj_t* port_dropdown = lv_dropdown_create(wrapper); std::string dropdown_items = getPortNamesForDropdown(); lv_dropdown_set_options(port_dropdown, dropdown_items.c_str()); lv_obj_set_width(port_dropdown, LV_PCT(48)); lv_obj_align(port_dropdown, LV_ALIGN_TOP_RIGHT, 0, 0); - lv_obj_add_event_cb(port_dropdown, onSelectBus, LV_EVENT_VALUE_CHANGED, nullptr); + lv_obj_add_event_cb(port_dropdown, onSelectBusCallback, LV_EVENT_VALUE_CHANGED, this); lv_dropdown_set_selected(port_dropdown, 0); - data->portDropdownWidget = port_dropdown; + portDropdownWidget = port_dropdown; lv_obj_t* scan_list = lv_list_create(main_wrapper); lv_obj_set_style_margin_top(scan_list, 8, 0); lv_obj_set_width(scan_list, LV_PCT(100)); lv_obj_set_height(scan_list, LV_SIZE_CONTENT); lv_obj_add_flag(scan_list, LV_OBJ_FLAG_HIDDEN); - data->scanListWidget = scan_list; + scanListWidget = scan_list; } -static void onHide(AppContext& app) { - auto data = std::static_pointer_cast(app.getData()); - +void I2cScannerApp::onHide(AppContext& app) { bool isRunning = false; - if (data->mutex.acquire(250 / portTICK_PERIOD_MS) == TtStatusOk) { - auto* timer = data->scanTimer.get(); + if (mutex.acquire(250 / portTICK_PERIOD_MS) == TtStatusOk) { + auto* timer = scanTimer.get(); if (timer != nullptr) { isRunning = timer->isRunning(); } - data->mutex.release(); + mutex.release(); } else { return; } if (isRunning) { - stopScanning(data); + stopScanning(); } } -static void onStart(AppContext& app) { - auto data = std::make_shared(); - app.setData(data); +// endregion Lifecycle + +// region Callbacks + +void I2cScannerApp::onSelectBusCallback(lv_event_t* event) { + auto* app = (I2cScannerApp*)lv_event_get_user_data(event); + if (app != nullptr) { + app->onSelectBus(event); + } +} + +void I2cScannerApp::onPressScanCallback(lv_event_t* event) { + auto* app = (I2cScannerApp*)lv_event_get_user_data(event); + if (app != nullptr) { + app->onPressScan(event); + } +} + +void I2cScannerApp::onScanTimerCallback(TT_UNUSED std::shared_ptr context) { + auto app = optApp(); + if (app != nullptr) { + app->onScanTimer(); + } +} + +// endregion Callbacks + +bool I2cScannerApp::getPort(i2c_port_t* outPort) { + if (mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { + *outPort = this->port; + tt_assert(mutex.release() == TtStatusOk); + return true; + } else { + TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "getPort"); + return false; + } +} + +bool I2cScannerApp::addAddressToList(uint8_t address) { + if (mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { + scannedAddresses.push_back(address); + tt_assert(mutex.release() == TtStatusOk); + return true; + } else { + TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "addAddressToList"); + return false; + } +} + +bool I2cScannerApp::shouldStopScanTimer() { + if (mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { + bool is_scanning = scanState == ScanStateScanning; + tt_check(mutex.release() == TtStatusOk); + return !is_scanning; + } else { + return true; + } +} + +void I2cScannerApp::onScanTimer() { + TT_LOG_I(TAG, "Scan thread started"); + + for (uint8_t address = 0; address < 128; ++address) { + i2c_port_t safe_port; + if (getPort(&safe_port)) { + if (hal::i2c::masterHasDeviceAtAddress(port, address, 10 / portTICK_PERIOD_MS)) { + TT_LOG_I(TAG, "Found device at address %d", address); + if (!shouldStopScanTimer()) { + addAddressToList(address); + } else { + break; + } + } + } else { + TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "onScanTimer"); + break; + } + + if (shouldStopScanTimer()) { + break; + } + } + + TT_LOG_I(TAG, "Scan thread finalizing"); + + onScanTimerFinished(); + + TT_LOG_I(TAG, "Scan timer done"); +} + +bool I2cScannerApp::hasScanThread() { + bool has_thread; + if (mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { + has_thread = scanTimer != nullptr; + tt_check(mutex.release() == TtStatusOk); + return has_thread; + } else { + // Unsafe way + TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "hasScanTimer"); + return scanTimer != nullptr; + } +} + +void I2cScannerApp::startScanning() { + if (hasScanThread()) { + stopScanning(); + } + + if (mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { + scannedAddresses.clear(); + + lv_obj_add_flag(scanListWidget, LV_OBJ_FLAG_HIDDEN); + lv_obj_clean(scanListWidget); + + scanState = ScanStateScanning; + scanTimer = std::make_unique( + Timer::Type::Once, + onScanTimerCallback + ); + scanTimer->start(10); + tt_check(mutex.release() == TtStatusOk); + } else { + TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "startScanning"); + } +} +void I2cScannerApp::stopScanning() { + if (mutex.acquire(250 / portTICK_PERIOD_MS) == TtStatusOk) { + tt_assert(scanTimer != nullptr); + scanState = ScanStateStopped; + tt_check(mutex.release() == TtStatusOk); + } else { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED); + } +} + +void I2cScannerApp::onSelectBus(lv_event_t* event) { + auto* dropdown = static_cast(lv_event_get_target(event)); + uint32_t selected = lv_dropdown_get_selected(dropdown); + auto i2c_devices = tt::getConfiguration()->hardware->i2c; + assert(selected < i2c_devices.size()); + + if (mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { + scannedAddresses.clear(); + port = i2c_devices[selected].port; + scanState = ScanStateInitial; + tt_check(mutex.release() == TtStatusOk); + + updateViews(); + } + + TT_LOG_I(TAG, "Selected %ld", selected); +} + +void I2cScannerApp::onPressScan(TT_UNUSED lv_event_t* event) { + if (scanState == ScanStateScanning) { + stopScanning(); + } else { + startScanning(); + } + updateViews(); +} + +void I2cScannerApp::updateViews() { + if (mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { + if (scanState == ScanStateScanning) { + lv_label_set_text(scanButtonLabelWidget, STOP_SCAN_TEXT); + lv_obj_remove_flag(portDropdownWidget, LV_OBJ_FLAG_CLICKABLE); + } else { + lv_label_set_text(scanButtonLabelWidget, START_SCAN_TEXT); + lv_obj_add_flag(portDropdownWidget, LV_OBJ_FLAG_CLICKABLE); + } + + lv_obj_clean(scanListWidget); + if (scanState == ScanStateStopped) { + lv_obj_remove_flag(scanListWidget, LV_OBJ_FLAG_HIDDEN); + if (!scannedAddresses.empty()) { + for (auto address: scannedAddresses) { + std::string address_text = getAddressText(address); + lv_list_add_text(scanListWidget, address_text.c_str()); + } + } else { + lv_list_add_text(scanListWidget, "No devices found"); + } + } else { + lv_obj_add_flag(scanListWidget, LV_OBJ_FLAG_HIDDEN); + } + + tt_check(mutex.release() == TtStatusOk); + } else { + TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "updateViews"); + } +} + +void I2cScannerApp::updateViewsSafely() { + if (lvgl::lock(100 / portTICK_PERIOD_MS)) { + updateViews(); + lvgl::unlock(); + } else { + TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "updateViewsSafely"); + } +} + +void I2cScannerApp::onScanTimerFinished() { + if (mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { + if (scanState == ScanStateScanning) { + scanState = ScanStateStopped; + updateViewsSafely(); + } + tt_check(mutex.release() == TtStatusOk); + } else { + TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "onScanTimerFinished"); + } } extern const AppManifest manifest = { .id = "I2cScanner", .name = "I2C Scanner", .icon = TT_ASSETS_APP_ICON_I2C_SETTINGS, - .type = TypeSystem, - .onStart = onStart, - .onShow = onShow, - .onHide = onHide + .type = Type::System, + .createApp = create }; void start() { diff --git a/Tactility/Source/app/i2cscanner/I2cScannerThread.cpp b/Tactility/Source/app/i2cscanner/I2cScannerThread.cpp deleted file mode 100644 index 8ca7037c..00000000 --- a/Tactility/Source/app/i2cscanner/I2cScannerThread.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "app/i2cscanner/I2cScannerThread.h" -#include "lvgl.h" -#include "service/loader/Loader.h" - -namespace tt::app::i2cscanner { - -std::shared_ptr _Nullable optData(); - -static bool shouldStopScanTimer(std::shared_ptr data) { - if (data->mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { - bool is_scanning = data->scanState == ScanStateScanning; - tt_check(data->mutex.release() == TtStatusOk); - return !is_scanning; - } else { - return true; - } -} - -static bool getPort(std::shared_ptr data, i2c_port_t* port) { - if (data->mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { - *port = data->port; - tt_assert(data->mutex.release() == TtStatusOk); - return true; - } else { - TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "getPort"); - return false; - } -} - -static bool addAddressToList(std::shared_ptr data, uint8_t address) { - if (data->mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { - data->scannedAddresses.push_back(address); - tt_assert(data->mutex.release() == TtStatusOk); - return true; - } else { - TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "addAddressToList"); - return false; - } -} - -static void onScanTimer(TT_UNUSED std::shared_ptr context) { - auto data = optData(); - if (data == nullptr) { - return; - } - - TT_LOG_I(TAG, "Scan thread started"); - - for (uint8_t address = 0; address < 128; ++address) { - i2c_port_t port; - if (getPort(data, &port)) { - if (hal::i2c::masterHasDeviceAtAddress(port, address, 10 / portTICK_PERIOD_MS)) { - TT_LOG_I(TAG, "Found device at address %d", address); - if (!shouldStopScanTimer(data)) { - addAddressToList(data, address); - } else { - break; - } - } - } else { - TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "onScanTimer"); - break; - } - - if (shouldStopScanTimer(data)) { - break; - } - } - - TT_LOG_I(TAG, "Scan thread finalizing"); - - onScanTimerFinished(data); - - TT_LOG_I(TAG, "Scan timer done"); -} - -bool hasScanThread(std::shared_ptr data) { - bool has_thread; - if (data->mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { - has_thread = data->scanTimer != nullptr; - tt_check(data->mutex.release() == TtStatusOk); - return has_thread; - } else { - // Unsafe way - TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "hasScanTimer"); - return data->scanTimer != nullptr; - } -} - -void startScanning(std::shared_ptr data) { - if (hasScanThread(data)) { - stopScanning(data); - } - - if (data->mutex.acquire(100 / portTICK_PERIOD_MS) == TtStatusOk) { - data->scannedAddresses.clear(); - - lv_obj_add_flag(data->scanListWidget, LV_OBJ_FLAG_HIDDEN); - lv_obj_clean(data->scanListWidget); - - data->scanState = ScanStateScanning; - data->scanTimer = std::make_unique( - Timer::Type::Once, - onScanTimer, - data - ); - data->scanTimer->start(10); - tt_check(data->mutex.release() == TtStatusOk); - } else { - TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "startScanning"); - } -} - -void stopScanning(std::shared_ptr data) { - if (data->mutex.acquire(250 / portTICK_PERIOD_MS) == TtStatusOk) { - tt_assert(data->scanTimer != nullptr); - data->scanState = ScanStateStopped; - tt_check(data->mutex.release() == TtStatusOk); - } else { - TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED); - } -} - -} // namespace \ No newline at end of file diff --git a/Tactility/Source/app/i2csettings/I2cSettings.cpp b/Tactility/Source/app/i2csettings/I2cSettings.cpp index 5313c9b7..9fab4488 100644 --- a/Tactility/Source/app/i2csettings/I2cSettings.cpp +++ b/Tactility/Source/app/i2csettings/I2cSettings.cpp @@ -69,28 +69,31 @@ static void show(lv_obj_t* parent, const hal::i2c::Configuration& configuration) } } -static void onShow(AppContext& app, lv_obj_t* parent) { - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - lvgl::toolbar_create(parent, app); +class I2cSettingsApp : public App { - lv_obj_t* wrapper = lv_obj_create(parent); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_flex_grow(wrapper, 1); - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); - lvgl::obj_set_style_no_padding(wrapper); - lvgl::obj_set_style_bg_invisible(wrapper); + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lvgl::toolbar_create(parent, app); - for (const auto& configuration: getConfiguration()->hardware->i2c) { - show(wrapper, configuration); + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lvgl::obj_set_style_no_padding(wrapper); + lvgl::obj_set_style_bg_invisible(wrapper); + + for (const auto& configuration: getConfiguration()->hardware->i2c) { + show(wrapper, configuration); + } } -} +}; extern const AppManifest manifest = { .id = "I2cSettings", .name = "I2C", .icon = TT_ASSETS_APP_ICON_I2C_SETTINGS, - .type = TypeSettings, - .onShow = onShow + .type = Type::Settings, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/imageviewer/ImageViewer.cpp b/Tactility/Source/app/imageviewer/ImageViewer.cpp index 4dd38f67..b00ca9e1 100644 --- a/Tactility/Source/app/imageviewer/ImageViewer.cpp +++ b/Tactility/Source/app/imageviewer/ImageViewer.cpp @@ -12,50 +12,53 @@ extern const AppManifest manifest; #define TAG "image_viewer" #define IMAGE_VIEWER_FILE_ARGUMENT "file" -static void onShow(AppContext& app, lv_obj_t* parent) { - auto wrapper = lv_obj_create(parent); - lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100)); - lv_obj_set_style_border_width(wrapper, 0, 0); - lvgl::obj_set_style_no_padding(wrapper); +class ImageViewerApp : public App { - auto toolbar = lvgl::toolbar_create(wrapper, app); - lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + void onShow(AppContext& app, lv_obj_t* parent) override { + auto wrapper = lv_obj_create(parent); + lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_border_width(wrapper, 0, 0); + lvgl::obj_set_style_no_padding(wrapper); - auto* image_wrapper = lv_obj_create(wrapper); - lv_obj_align_to(image_wrapper, toolbar, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 0); - lv_obj_set_width(image_wrapper, LV_PCT(100)); - auto parent_height = lv_obj_get_height(wrapper); - lv_obj_set_height(image_wrapper, parent_height - TOOLBAR_HEIGHT); - lv_obj_set_flex_flow(image_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(image_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lvgl::obj_set_style_no_padding(image_wrapper); - lvgl::obj_set_style_bg_invisible(image_wrapper); + auto toolbar = lvgl::toolbar_create(wrapper, app); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); - auto* image = lv_image_create(image_wrapper); - lv_obj_align(image, LV_ALIGN_CENTER, 0, 0); + auto* image_wrapper = lv_obj_create(wrapper); + lv_obj_align_to(image_wrapper, toolbar, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 0); + lv_obj_set_width(image_wrapper, LV_PCT(100)); + auto parent_height = lv_obj_get_height(wrapper); + lv_obj_set_height(image_wrapper, parent_height - TOOLBAR_HEIGHT); + lv_obj_set_flex_flow(image_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(image_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lvgl::obj_set_style_no_padding(image_wrapper); + lvgl::obj_set_style_bg_invisible(image_wrapper); - auto* file_label = lv_label_create(wrapper); - lv_obj_align_to(file_label, wrapper, LV_ALIGN_BOTTOM_LEFT, 0, 0); + auto* image = lv_image_create(image_wrapper); + lv_obj_align(image, LV_ALIGN_CENTER, 0, 0); - std::shared_ptr bundle = app.getParameters(); - tt_check(bundle != nullptr, "Parameters not set"); - std::string file_argument; - if (bundle->optString(IMAGE_VIEWER_FILE_ARGUMENT, file_argument)) { - std::string prefixed_path = "A:" + file_argument; - TT_LOG_I(TAG, "Opening %s", prefixed_path.c_str()); - lv_img_set_src(image, prefixed_path.c_str()); - auto path = string::getLastPathSegment(file_argument); - lv_label_set_text(file_label, path.c_str()); - } else { - lv_label_set_text(file_label, "File not found"); + auto* file_label = lv_label_create(wrapper); + lv_obj_align_to(file_label, wrapper, LV_ALIGN_BOTTOM_LEFT, 0, 0); + + std::shared_ptr bundle = app.getParameters(); + tt_check(bundle != nullptr, "Parameters not set"); + std::string file_argument; + if (bundle->optString(IMAGE_VIEWER_FILE_ARGUMENT, file_argument)) { + std::string prefixed_path = "A:" + file_argument; + TT_LOG_I(TAG, "Opening %s", prefixed_path.c_str()); + lv_img_set_src(image, prefixed_path.c_str()); + auto path = string::getLastPathSegment(file_argument); + lv_label_set_text(file_label, path.c_str()); + } else { + lv_label_set_text(file_label, "File not found"); + } } -} +}; extern const AppManifest manifest = { .id = "ImageViewer", .name = "Image Viewer", - .type = TypeHidden, - .onShow = onShow + .type = Type::Hidden, + .createApp = create }; void start(const std::string& file) { diff --git a/Tactility/Source/app/inputdialog/InputDialog.cpp b/Tactility/Source/app/inputdialog/InputDialog.cpp index 413e0004..58453a60 100644 --- a/Tactility/Source/app/inputdialog/InputDialog.cpp +++ b/Tactility/Source/app/inputdialog/InputDialog.cpp @@ -18,6 +18,7 @@ namespace tt::app::inputdialog { #define TAG "input_dialog" extern const AppManifest manifest; +class InputDialogApp; void start(const std::string& title, const std::string& message, const std::string& prefilled) { auto bundle = std::make_shared(); @@ -33,10 +34,6 @@ std::string getResult(const Bundle& bundle) { return result; } -void setResult(const std::shared_ptr& bundle, const std::string& result) { - bundle->putString(RESULT_BUNDLE_KEY_RESULT, result); -} - static std::string getTitleParameter(const std::shared_ptr& bundle) { std::string result; if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) { @@ -46,75 +43,87 @@ static std::string getTitleParameter(const std::shared_ptr& bundle } } -static void onButtonClicked(lv_event_t* e) { - auto user_data = lv_event_get_user_data(e); - int index = (user_data != 0) ? 0 : 1; - TT_LOG_I(TAG, "Selected item at index %d", index); - tt::app::AppContext* app = service::loader::getCurrentApp(); - auto bundle = std::make_shared(); - if (index == 0) { - const char* text = lv_textarea_get_text((lv_obj_t*)user_data); - setResult(bundle, text); - app->setResult(app::ResultOk, bundle); - } else { - app->setResult(app::ResultCancelled, bundle); +class InputDialogApp : public App { - } - service::loader::stopApp(); -} +private: -static void createButton(lv_obj_t* parent, const std::string& text, void* callbackContext) { - lv_obj_t* button = lv_button_create(parent); - lv_obj_t* button_label = lv_label_create(button); - lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0); - lv_label_set_text(button_label, text.c_str()); - lv_obj_add_event_cb(button, &onButtonClicked, LV_EVENT_SHORT_CLICKED, callbackContext); -} - -static void onShow(AppContext& app, lv_obj_t* parent) { - auto parameters = app.getParameters(); - tt_check(parameters != nullptr, "Parameters missing"); - - std::string title = getTitleParameter(app.getParameters()); - auto* toolbar = lvgl::toolbar_create(parent, title); - lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); - - auto* message_label = lv_label_create(parent); - lv_obj_align(message_label, LV_ALIGN_CENTER, 0, -20); - lv_obj_set_width(message_label, LV_PCT(80)); - - std::string message; - if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) { - lv_label_set_text(message_label, message.c_str()); - lv_label_set_long_mode(message_label, LV_LABEL_LONG_WRAP); + static void createButton(lv_obj_t* parent, const std::string& text, void* callbackContext) { + lv_obj_t* button = lv_button_create(parent); + lv_obj_t* button_label = lv_label_create(button); + lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0); + lv_label_set_text(button_label, text.c_str()); + lv_obj_add_event_cb(button, onButtonClickedCallback, LV_EVENT_SHORT_CLICKED, callbackContext); } - auto* textarea = lv_textarea_create(parent); - lv_obj_align_to(textarea, message_label, LV_ALIGN_OUT_BOTTOM_MID, 0, 4); - lv_textarea_set_one_line(textarea, true); - std::string prefilled; - if (parameters->optString(PARAMETER_BUNDLE_KEY_PREFILLED, prefilled)) { - lv_textarea_set_text(textarea, prefilled.c_str()); + static void onButtonClickedCallback(lv_event_t* e) { + auto appContext = service::loader::getCurrentAppContext(); + tt_assert(appContext != nullptr); + auto app = std::static_pointer_cast(appContext->getApp()); + app->onButtonClicked(e); } - service::gui::keyboardAddTextArea(textarea); - auto* button_wrapper = lv_obj_create(parent); - lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW); - lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(button_wrapper, 0, 0); - lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_border_width(button_wrapper, 0, 0); - lv_obj_align(button_wrapper, LV_ALIGN_BOTTOM_MID, 0, -4); + void onButtonClicked(lv_event_t* e) { + auto user_data = lv_event_get_user_data(e); + int index = (user_data != 0) ? 0 : 1; + TT_LOG_I(TAG, "Selected item at index %d", index); + if (index == 0) { + auto bundle = std::make_unique(); + const char* text = lv_textarea_get_text((lv_obj_t*)user_data); + bundle->putString(RESULT_BUNDLE_KEY_RESULT, text); + setResult(app::Result::Ok, std::move(bundle)); + } else { + setResult(app::Result::Cancelled); - createButton(button_wrapper, "OK", textarea); - createButton(button_wrapper, "Cancel", nullptr); -} + } + service::loader::stopApp(); + } + +public: + void onShow(AppContext& app, lv_obj_t* parent) override { + auto parameters = app.getParameters(); + tt_check(parameters != nullptr, "Parameters missing"); + + std::string title = getTitleParameter(app.getParameters()); + auto* toolbar = lvgl::toolbar_create(parent, title); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + auto* message_label = lv_label_create(parent); + lv_obj_align(message_label, LV_ALIGN_CENTER, 0, -20); + lv_obj_set_width(message_label, LV_PCT(80)); + + std::string message; + if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) { + lv_label_set_text(message_label, message.c_str()); + lv_label_set_long_mode(message_label, LV_LABEL_LONG_WRAP); + } + + auto* textarea = lv_textarea_create(parent); + lv_obj_align_to(textarea, message_label, LV_ALIGN_OUT_BOTTOM_MID, 0, 4); + lv_textarea_set_one_line(textarea, true); + std::string prefilled; + if (parameters->optString(PARAMETER_BUNDLE_KEY_PREFILLED, prefilled)) { + lv_textarea_set_text(textarea, prefilled.c_str()); + } + service::gui::keyboardAddTextArea(textarea); + + auto* button_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(button_wrapper, 0, 0); + lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_border_width(button_wrapper, 0, 0); + lv_obj_align(button_wrapper, LV_ALIGN_BOTTOM_MID, 0, -4); + + createButton(button_wrapper, "OK", textarea); + createButton(button_wrapper, "Cancel", nullptr); + } +}; extern const AppManifest manifest = { - .id = "InputDialog", - .name = "Input Dialog", - .type = TypeHidden, - .onShow = onShow + .id = "InputDialog", + .name = "Input Dialog", + .type = Type::Hidden, + .createApp = create }; } diff --git a/Tactility/Source/app/inputdialog/InputDialog.h b/Tactility/Source/app/inputdialog/InputDialog.h index 94ce7785..563a88e7 100644 --- a/Tactility/Source/app/inputdialog/InputDialog.h +++ b/Tactility/Source/app/inputdialog/InputDialog.h @@ -11,10 +11,10 @@ */ namespace tt::app::inputdialog { - void start(const std::string& title, const std::string& message, const std::string& prefilled = ""); +void start(const std::string& title, const std::string& message, const std::string& prefilled = ""); - /** - * @return the text that was in the field when OK was pressed, or otherwise empty string - */ - std::string getResult(const Bundle& bundle); +/** + * @return the text that was in the field when OK was pressed, or otherwise empty string + */ +std::string getResult(const Bundle& bundle); } diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 329719f3..7a79808f 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -42,42 +42,45 @@ static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char return wrapper; } -static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { - auto* wrapper = lv_obj_create(parent); +class LauncherApp : public App { - lv_obj_align(wrapper, LV_ALIGN_CENTER, 0, 0); - lv_obj_set_style_pad_all(wrapper, 0, 0); - lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_style_border_width(wrapper, 0, 0); - lv_obj_set_flex_grow(wrapper, 1); + void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override { + auto* wrapper = lv_obj_create(parent); - auto* display = lv_obj_get_display(parent); - auto horizontal_px = lv_display_get_horizontal_resolution(display); - auto vertical_px = lv_display_get_vertical_resolution(display); - bool is_landscape_display = horizontal_px > vertical_px; - if (is_landscape_display) { - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); - } else { - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_align(wrapper, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_pad_all(wrapper, 0, 0); + lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_flex_grow(wrapper, 1); + + auto* display = lv_obj_get_display(parent); + auto horizontal_px = lv_display_get_horizontal_resolution(display); + auto vertical_px = lv_display_get_vertical_resolution(display); + bool is_landscape_display = horizontal_px > vertical_px; + if (is_landscape_display) { + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); + } else { + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + } + + int32_t available_width = lv_display_get_horizontal_resolution(display) - (3 * 80); + int32_t padding = is_landscape_display ? TT_MIN(available_width / 4, 64) : 0; + + auto paths = app.getPaths(); + auto apps_icon_path = paths->getSystemPathLvgl("icon_apps.png"); + auto files_icon_path = paths->getSystemPathLvgl("icon_files.png"); + auto settings_icon_path = paths->getSystemPathLvgl("icon_settings.png"); + createAppButton(wrapper, "Apps", apps_icon_path.c_str(), "AppList", 0); + createAppButton(wrapper, "Files", files_icon_path.c_str(), "Files", padding); + createAppButton(wrapper, "Settings", settings_icon_path.c_str(), "Settings", padding); } - - int32_t available_width = lv_display_get_horizontal_resolution(display) - (3 * 80); - int32_t padding = is_landscape_display ? TT_MIN(available_width / 4, 64) : 0; - - auto paths = app.getPaths(); - auto apps_icon_path = paths->getSystemPathLvgl("icon_apps.png"); - auto files_icon_path = paths->getSystemPathLvgl("icon_files.png"); - auto settings_icon_path = paths->getSystemPathLvgl("icon_settings.png"); - createAppButton(wrapper, "Apps", apps_icon_path.c_str(), "AppList", 0); - createAppButton(wrapper, "Files", files_icon_path.c_str(), "Files", padding); - createAppButton(wrapper, "Settings", settings_icon_path.c_str(), "Settings", padding); -} +}; extern const AppManifest manifest = { .id = "Launcher", .name = "Launcher", - .type = TypeLauncher, - .onShow = onShow, + .type = Type::Launcher, + .createApp = create }; void start() { diff --git a/Tactility/Source/app/log/Log.cpp b/Tactility/Source/app/log/Log.cpp index 3a0d28b8..e3217b8e 100644 --- a/Tactility/Source/app/log/Log.cpp +++ b/Tactility/Source/app/log/Log.cpp @@ -11,139 +11,121 @@ namespace tt::app::log { -struct LogAppData { +class LogApp : public App { + +private: + LogLevel filterLevel = LogLevel::Info; lv_obj_t* labelWidget = nullptr; -}; -static bool shouldShowLog(LogLevel filterLevel, LogLevel logLevel) { - if (filterLevel == LogLevel::None || logLevel == LogLevel::None) { - return false; - } else { - return filterLevel >= logLevel; - } -} - -static void setLogEntries(lv_obj_t* label) { - auto app = service::loader::getCurrentApp(); - if (app == nullptr) { - return; - } - auto data = std::static_pointer_cast(app->getData()); - auto filterLevel = data->filterLevel; - - unsigned int index; - auto* entries = copyLogEntries(index); - std::stringstream buffer; - if (entries != nullptr) { - for (unsigned int i = index; i < TT_LOG_ENTRY_COUNT; ++i) { - if (shouldShowLog(filterLevel, entries[i].level)) { - buffer << entries[i].message; - } + static bool shouldShowLog(LogLevel filterLevel, LogLevel logLevel) { + if (filterLevel == LogLevel::None || logLevel == LogLevel::None) { + return false; + } else { + return filterLevel >= logLevel; } - if (index != 0) { - for (unsigned int i = 0; i < index; ++i) { + } + + void updateLogEntries() { + unsigned int index; + auto* entries = copyLogEntries(index); + std::stringstream buffer; + if (entries != nullptr) { + for (unsigned int i = index; i < TT_LOG_ENTRY_COUNT; ++i) { if (shouldShowLog(filterLevel, entries[i].level)) { buffer << entries[i].message; } } - } - delete entries; - if (!buffer.str().empty()) { - lv_label_set_text(label, buffer.str().c_str()); + if (index != 0) { + for (unsigned int i = 0; i < index; ++i) { + if (shouldShowLog(filterLevel, entries[i].level)) { + buffer << entries[i].message; + } + } + } + delete entries; + if (!buffer.str().empty()) { + lv_label_set_text(labelWidget, buffer.str().c_str()); + } else { + lv_label_set_text(labelWidget, "No logs for the selected log level"); + } } else { - lv_label_set_text(label, "No logs for the selected log level"); - } - } else { - lv_label_set_text(label, "Failed to load log"); - } -} - -static void onLevelFilterPressed(TT_UNUSED lv_event_t* event) { - std::vector items = { - "Verbose", - "Debug", - "Info", - "Warning", - "Error", - }; - app::selectiondialog::start("Log Level", items); -} - -static void updateViews() { - auto app = service::loader::getCurrentApp(); - if (app == nullptr) { - return; - } - auto data = std::static_pointer_cast(app->getData()); - assert(data != nullptr); - - if (lvgl::lock(100 / portTICK_PERIOD_MS)) { - setLogEntries(data->labelWidget); - lvgl::unlock(); - } -} - -static void onShow(AppContext& app, lv_obj_t* parent) { - auto data = std::static_pointer_cast(app.getData()); - - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - auto* toolbar = lvgl::toolbar_create(parent, app); - lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_EDIT, onLevelFilterPressed, nullptr); - - auto* wrapper = lv_obj_create(parent); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_flex_grow(wrapper, 1); - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); - lvgl::obj_set_style_no_padding(wrapper); - lvgl::obj_set_style_bg_invisible(wrapper); - - data->labelWidget = lv_label_create(wrapper); - lv_obj_align(data->labelWidget, LV_ALIGN_CENTER, 0, 0); - setLogEntries(data->labelWidget); -} - -static void onStart(AppContext& app) { - auto data = std::make_shared(); - app.setData(data); -} - -static void onResult(AppContext& app, Result result, const Bundle& bundle) { - auto resultIndex = selectiondialog::getResultIndex(bundle); - auto data = std::static_pointer_cast(app.getData()); - if (result == ResultOk) { - switch (resultIndex) { - case 0: - data->filterLevel = LogLevel::Verbose; - break; - case 1: - data->filterLevel = LogLevel::Debug; - break; - case 2: - data->filterLevel = LogLevel::Info; - break; - case 3: - data->filterLevel = LogLevel::Warning; - break; - case 4: - data->filterLevel = LogLevel::Error; - break; - default: - break; + lv_label_set_text(labelWidget, "Failed to load log"); } } - updateViews(); -} + void updateViews() { + if (lvgl::lock(100 / portTICK_PERIOD_MS)) { + updateLogEntries(); + lvgl::unlock(); + } + } + + static void onLevelFilterPressedCallback(TT_UNUSED lv_event_t* event) { + std::vector items = { + "Verbose", + "Debug", + "Info", + "Warning", + "Error", + }; + app::selectiondialog::start("Log Level", items); + } + +public: + + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + auto* toolbar = lvgl::toolbar_create(parent, app); + lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_EDIT, onLevelFilterPressedCallback, this); + + auto* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lvgl::obj_set_style_no_padding(wrapper); + lvgl::obj_set_style_bg_invisible(wrapper); + + labelWidget = lv_label_create(wrapper); + lv_obj_align(labelWidget, LV_ALIGN_CENTER, 0, 0); + + updateLogEntries(); + } + + void onResult(AppContext& app, Result result, std::unique_ptr bundle) override { + auto resultIndex = selectiondialog::getResultIndex(*bundle); + if (result == Result::Ok) { + switch (resultIndex) { + case 0: + filterLevel = LogLevel::Verbose; + break; + case 1: + filterLevel = LogLevel::Debug; + break; + case 2: + filterLevel = LogLevel::Info; + break; + case 3: + filterLevel = LogLevel::Warning; + break; + case 4: + filterLevel = LogLevel::Error; + break; + default: + break; + } + } + + updateViews(); + } +}; extern const AppManifest manifest = { .id = "Log", .name = "Log", .icon = LV_SYMBOL_LIST, - .type = TypeSystem, - .onStart = onStart, - .onShow = onShow, - .onResult = onResult + .type = Type::System, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/power/Power.cpp b/Tactility/Source/app/power/Power.cpp index 655322ad..ae383b4d 100644 --- a/Tactility/Source/app/power/Power.cpp +++ b/Tactility/Source/app/power/Power.cpp @@ -15,174 +15,173 @@ namespace tt::app::power { extern const AppManifest manifest; static void onTimer(TT_UNUSED std::shared_ptr context); -struct Data { - Timer update_timer = Timer(Timer::Type::Periodic, &onTimer, nullptr); - std::shared_ptr power = getConfiguration()->hardware->power(); - lv_obj_t* enable_label = nullptr; - lv_obj_t* enable_switch = nullptr; - lv_obj_t* battery_voltage = nullptr; - lv_obj_t* charge_state = nullptr; - lv_obj_t* charge_level = nullptr; - lv_obj_t* current = nullptr; -}; +class PowerApp; /** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ -std::shared_ptr _Nullable optData() { - app::AppContext* app = service::loader::getCurrentApp(); - if (app->getManifest().id == manifest.id) { - return std::static_pointer_cast(app->getData()); +std::shared_ptr _Nullable optApp() { + auto appContext = service::loader::getCurrentAppContext(); + if (appContext != nullptr && appContext->getManifest().id == manifest.id) { + return std::static_pointer_cast(appContext->getApp()); } else { return nullptr; } } -static void updateUi(std::shared_ptr data) { - const char* charge_state; - hal::Power::MetricData metric_data; - if (data->power->getMetric(hal::Power::MetricType::IsCharging, metric_data)) { - charge_state = metric_data.valueAsBool ? "yes" : "no"; - } else { - charge_state = "N/A"; +class PowerApp : public App { + +private: + + Timer update_timer = Timer(Timer::Type::Periodic, &onTimer, nullptr); + std::shared_ptr power = getConfiguration()->hardware->power(); + lv_obj_t* enableLabel = nullptr; + lv_obj_t* enableSwitch = nullptr; + lv_obj_t* batteryVoltageLabel = nullptr; + lv_obj_t* chargeStateLabel = nullptr; + lv_obj_t* chargeLevelLabel = nullptr; + lv_obj_t* currentLabel = nullptr; + + static void onTimer(TT_UNUSED std::shared_ptr context) { + auto app = optApp(); + if (app != nullptr) { + app->updateUi(); + } } - uint8_t charge_level; - bool charge_level_scaled_set = false; - if (data->power->getMetric(hal::Power::MetricType::ChargeLevel, metric_data)) { - charge_level = metric_data.valueAsUint8; - charge_level_scaled_set = true; - } + void onPowerEnabledChanged(lv_event_t* event) { + lv_event_code_t code = lv_event_get_code(event); + auto* enable_switch = static_cast(lv_event_get_target(event)); + if (code == LV_EVENT_VALUE_CHANGED) { + bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED); - bool charging_enabled_set = data->power->supportsChargeControl(); - bool charging_enabled_and_allowed = data->power->supportsChargeControl() && data->power->isAllowedToCharge(); - - int32_t current; - bool current_set = false; - if (data->power->getMetric(hal::Power::MetricType::Current, metric_data)) { - current = metric_data.valueAsInt32; - current_set = true; - } - - uint32_t battery_voltage; - bool battery_voltage_set = false; - if (data->power->getMetric(hal::Power::MetricType::BatteryVoltage, metric_data)) { - battery_voltage = metric_data.valueAsUint32; - battery_voltage_set = true; - } - - lvgl::lock(kernel::millisToTicks(1000)); - - if (charging_enabled_set) { - lv_obj_set_state(data->enable_switch, LV_STATE_CHECKED, charging_enabled_and_allowed); - lv_obj_remove_flag(data->enable_switch, LV_OBJ_FLAG_HIDDEN); - lv_obj_remove_flag(data->enable_label, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(data->enable_switch, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(data->enable_label, LV_OBJ_FLAG_HIDDEN); - } - - lv_label_set_text_fmt(data->charge_state, "Charging: %s", charge_state); - - if (battery_voltage_set) { - lv_label_set_text_fmt(data->battery_voltage, "Battery voltage: %lu mV", battery_voltage); - } else { - lv_label_set_text_fmt(data->battery_voltage, "Battery voltage: N/A"); - } - - if (charge_level_scaled_set) { - lv_label_set_text_fmt(data->charge_level, "Charge level: %d%%", charge_level); - } else { - lv_label_set_text_fmt(data->charge_level, "Charge level: N/A"); - } - - if (current_set) { - lv_label_set_text_fmt(data->current, "Current: %ld mAh", current); - } else { - lv_label_set_text_fmt(data->current, "Current: N/A"); - } - - lvgl::unlock(); -} - -static void onTimer(TT_UNUSED std::shared_ptr context) { - auto data = optData(); - if (data != nullptr) { - updateUi(data); - } -} - -static void onPowerEnabledChanged(lv_event_t* event) { - lv_event_code_t code = lv_event_get_code(event); - auto* enable_switch = static_cast(lv_event_get_target(event)); - if (code == LV_EVENT_VALUE_CHANGED) { - bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED); - - auto data = optData(); - if (data != nullptr) { - if (data->power->isAllowedToCharge() != is_on) { - data->power->setAllowedToCharge(is_on); - updateUi(data); + if (power->isAllowedToCharge() != is_on) { + power->setAllowedToCharge(is_on); + updateUi(); } } } -} -static void onShow(AppContext& app, lv_obj_t* parent) { - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + static void onPowerEnabledChangedCallback(lv_event_t* event) { + auto* app = (PowerApp*)lv_event_get_user_data(event); + app->onPowerEnabledChanged(event); + } - lvgl::toolbar_create(parent, app); + void updateUi() { + const char* charge_state; + hal::Power::MetricData metric_data; + if (power->getMetric(hal::Power::MetricType::IsCharging, metric_data)) { + charge_state = metric_data.valueAsBool ? "yes" : "no"; + } else { + charge_state = "N/A"; + } - lv_obj_t* wrapper = lv_obj_create(parent); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_style_border_width(wrapper, 0, 0); - lv_obj_set_flex_grow(wrapper, 1); - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + uint8_t charge_level; + bool charge_level_scaled_set = false; + if (power->getMetric(hal::Power::MetricType::ChargeLevel, metric_data)) { + charge_level = metric_data.valueAsUint8; + charge_level_scaled_set = true; + } - auto data = std::static_pointer_cast(app.getData()); + bool charging_enabled_set = power->supportsChargeControl(); + bool charging_enabled_and_allowed = power->supportsChargeControl() && power->isAllowedToCharge(); - // Top row: enable/disable - lv_obj_t* switch_container = lv_obj_create(wrapper); - lv_obj_set_width(switch_container, LV_PCT(100)); - lv_obj_set_height(switch_container, LV_SIZE_CONTENT); - lvgl::obj_set_style_no_padding(switch_container); - lvgl::obj_set_style_bg_invisible(switch_container); + int32_t current; + bool current_set = false; + if (power->getMetric(hal::Power::MetricType::Current, metric_data)) { + current = metric_data.valueAsInt32; + current_set = true; + } - data->enable_label = lv_label_create(switch_container); - lv_label_set_text(data->enable_label, "Charging enabled"); - lv_obj_set_align(data->enable_label, LV_ALIGN_LEFT_MID); + uint32_t battery_voltage; + bool battery_voltage_set = false; + if (power->getMetric(hal::Power::MetricType::BatteryVoltage, metric_data)) { + battery_voltage = metric_data.valueAsUint32; + battery_voltage_set = true; + } - lv_obj_t* enable_switch = lv_switch_create(switch_container); - lv_obj_add_event_cb(enable_switch, onPowerEnabledChanged, LV_EVENT_VALUE_CHANGED, nullptr); - lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID); + lvgl::lock(kernel::millisToTicks(1000)); - data->enable_switch = enable_switch; - data->charge_state = lv_label_create(wrapper); - data->charge_level = lv_label_create(wrapper); - data->battery_voltage = lv_label_create(wrapper); - data->current = lv_label_create(wrapper); + if (charging_enabled_set) { + lv_obj_set_state(enableSwitch, LV_STATE_CHECKED, charging_enabled_and_allowed); + lv_obj_remove_flag(enableSwitch, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(enableLabel, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(enableSwitch, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(enableLabel, LV_OBJ_FLAG_HIDDEN); + } - updateUi(data); - data->update_timer.start(kernel::millisToTicks(1000)); -} + lv_label_set_text_fmt(chargeStateLabel, "Charging: %s", charge_state); -static void onHide(TT_UNUSED AppContext& app) { - auto data = std::static_pointer_cast(app.getData()); - data->update_timer.stop(); -} + if (battery_voltage_set) { + lv_label_set_text_fmt(batteryVoltageLabel, "Battery voltage: %lu mV", battery_voltage); + } else { + lv_label_set_text_fmt(batteryVoltageLabel, "Battery voltage: N/A"); + } -static void onStart(AppContext& app) { - auto data = std::make_shared(); - app.setData(data); - assert(data->power != nullptr); // The Power app only shows up on supported devices -} + if (charge_level_scaled_set) { + lv_label_set_text_fmt(chargeLevelLabel, "Charge level: %d%%", charge_level); + } else { + lv_label_set_text_fmt(chargeLevelLabel, "Charge level: N/A"); + } + + if (current_set) { + lv_label_set_text_fmt(currentLabel, "Current: %ld mAh", current); + } else { + lv_label_set_text_fmt(currentLabel, "Current: N/A"); + } + + lvgl::unlock(); + } + +public: + + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + lvgl::toolbar_create(parent, app); + + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + + // Top row: enable/disable + lv_obj_t* switch_container = lv_obj_create(wrapper); + lv_obj_set_width(switch_container, LV_PCT(100)); + lv_obj_set_height(switch_container, LV_SIZE_CONTENT); + lvgl::obj_set_style_no_padding(switch_container); + lvgl::obj_set_style_bg_invisible(switch_container); + + enableLabel = lv_label_create(switch_container); + lv_label_set_text(enableLabel, "Charging enabled"); + lv_obj_set_align(enableLabel, LV_ALIGN_LEFT_MID); + + lv_obj_t* enable_switch = lv_switch_create(switch_container); + lv_obj_add_event_cb(enable_switch, onPowerEnabledChangedCallback, LV_EVENT_VALUE_CHANGED, this); + lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID); + + enableSwitch = enable_switch; + chargeStateLabel = lv_label_create(wrapper); + chargeLevelLabel = lv_label_create(wrapper); + batteryVoltageLabel = lv_label_create(wrapper); + currentLabel = lv_label_create(wrapper); + + updateUi(); + + update_timer.start(kernel::millisToTicks(1000)); + } + + void onHide(TT_UNUSED AppContext& app) override { + update_timer.stop(); + } +}; extern const AppManifest manifest = { .id = "Power", .name = "Power", .icon = TT_ASSETS_APP_ICON_POWER_SETTINGS, - .type = TypeSettings, - .onStart = onStart, - .onShow = onShow, - .onHide = onHide + .type = Type::Settings, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/screenshot/Screenshot.cpp b/Tactility/Source/app/screenshot/Screenshot.cpp index 18689664..5b9fb261 100644 --- a/Tactility/Source/app/screenshot/Screenshot.cpp +++ b/Tactility/Source/app/screenshot/Screenshot.cpp @@ -1,29 +1,289 @@ #include "TactilityConfig.h" +#include +#include #if TT_FEATURE_SCREENSHOT_ENABLED -#include "app/screenshot/ScreenshotUi.h" -#include +#include "TactilityHeadless.h" +#include "app/App.h" +#include "app/AppManifest.h" +#include "lvgl/LvglSync.h" +#include "lvgl/Toolbar.h" +#include "service/gui/Gui.h" +#include "service/loader/Loader.h" +#include "service/screenshot/Screenshot.h" + +#define TAG "screenshot" namespace tt::app::screenshot { -static void onShow(AppContext& app, lv_obj_t* parent) { - auto ui = std::static_pointer_cast(app.getData()); - ui->createWidgets(app, parent); +extern const AppManifest manifest; + +class ScreenshotApp : public App { + + lv_obj_t* modeDropdown = nullptr; + lv_obj_t* pathTextArea = nullptr; + lv_obj_t* startStopButtonLabel = nullptr; + lv_obj_t* timerWrapper = nullptr; + lv_obj_t* delayTextArea = nullptr; + std::unique_ptr updateTimer; + + void createTimerSettingsWidgets(lv_obj_t* parent); + void createModeSettingWidgets(lv_obj_t* parent); + void createFilePathWidgets(lv_obj_t* parent); + + void updateScreenshotMode(); + +public: + + ScreenshotApp(); + ~ScreenshotApp(); + + void onShow(AppContext& app, lv_obj_t* parent) override; + void onStartPressed(); + void onModeSet(); + void onTimerTick(); +}; + + +/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ +std::shared_ptr _Nullable optApp() { + auto appContext = service::loader::getCurrentAppContext(); + if (appContext != nullptr && appContext->getManifest().id == manifest.id) { + return std::static_pointer_cast(appContext->getApp()); + } else { + return nullptr; + } } -static void onStart(AppContext& app) { - auto ui = std::make_shared(); - app.setData(ui); // Ensure data gets deleted when no more in use +static void onStartPressedCallback(TT_UNUSED lv_event_t* event) { + auto app = optApp(); + if (app != nullptr) { + app->onStartPressed(); + } +} + +static void onModeSetCallback(TT_UNUSED lv_event_t* event) { + auto app = optApp(); + if (app != nullptr) { + app->onModeSet(); + } +} + +static void onTimerCallback(TT_UNUSED std::shared_ptr context) { + auto app = optApp(); + if (app != nullptr) { + app->onTimerTick(); + } +} + +ScreenshotApp::ScreenshotApp() { + updateTimer = std::make_unique(Timer::Type::Periodic, onTimerCallback, nullptr); +} + +ScreenshotApp::~ScreenshotApp() { + if (updateTimer->isRunning()) { + updateTimer->stop(); + } +} + +void ScreenshotApp::onTimerTick() { + auto lvgl_lock = lvgl::getLvglSyncLockable()->scoped(); + if (lvgl_lock->lock(50 / portTICK_PERIOD_MS)) { + updateScreenshotMode(); + } +} + +void ScreenshotApp::onModeSet() { + updateScreenshotMode(); +} + +void ScreenshotApp::onStartPressed() { + auto service = service::screenshot::optScreenshotService(); + if (service == nullptr) { + TT_LOG_E(TAG, "Service not found/running"); + return; + } + + if (service->isTaskStarted()) { + TT_LOG_I(TAG, "Stop screenshot"); + service->stop(); + } else { + uint32_t selected = lv_dropdown_get_selected(modeDropdown); + const char* path = lv_textarea_get_text(pathTextArea); + if (selected == 0) { + TT_LOG_I(TAG, "Start timed screenshots"); + const char* delay_text = lv_textarea_get_text(delayTextArea); + int delay = atoi(delay_text); + if (delay > 0) { + service->startTimed(path, delay, 1); + } else { + TT_LOG_W(TAG, "Ignored screenshot start because delay was 0"); + } + } else { + TT_LOG_I(TAG, "Start app screenshots"); + service->startApps(path); + } + } + + updateScreenshotMode(); +} + +void ScreenshotApp::updateScreenshotMode() { + auto service = service::screenshot::optScreenshotService(); + if (service == nullptr) { + TT_LOG_E(TAG, "Service not found/running"); + return; + } + + lv_obj_t* label = startStopButtonLabel; + if (service->isTaskStarted()) { + lv_label_set_text(label, "Stop"); + } else { + lv_label_set_text(label, "Start"); + } + + uint32_t selected = lv_dropdown_get_selected(modeDropdown); + if (selected == 0) { // Timer + lv_obj_remove_flag(timerWrapper, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(timerWrapper, LV_OBJ_FLAG_HIDDEN); + } +} + + +void ScreenshotApp::createModeSettingWidgets(lv_obj_t* parent) { + auto service = service::screenshot::optScreenshotService(); + if (service == nullptr) { + TT_LOG_E(TAG, "Service not found/running"); + return; + } + + auto* mode_wrapper = lv_obj_create(parent); + lv_obj_set_size(mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(mode_wrapper, 0, 0); + lv_obj_set_style_border_width(mode_wrapper, 0, 0); + + auto* mode_label = lv_label_create(mode_wrapper); + lv_label_set_text(mode_label, "Mode:"); + lv_obj_align(mode_label, LV_ALIGN_LEFT_MID, 0, 0); + + modeDropdown = lv_dropdown_create(mode_wrapper); + lv_dropdown_set_options(modeDropdown, "Timer\nApp start"); + lv_obj_align_to(modeDropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0); + lv_obj_add_event_cb(modeDropdown, onModeSetCallback, LV_EVENT_VALUE_CHANGED, nullptr); + service::screenshot::Mode mode = service->getMode(); + if (mode == service::screenshot::Mode::Apps) { + lv_dropdown_set_selected(modeDropdown, 1); + } + + auto* button = lv_button_create(mode_wrapper); + lv_obj_align(button, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(button, &onStartPressedCallback, LV_EVENT_SHORT_CLICKED, nullptr); + startStopButtonLabel = lv_label_create(button); + lv_obj_align(startStopButtonLabel, LV_ALIGN_CENTER, 0, 0); +} + +void ScreenshotApp::createFilePathWidgets(lv_obj_t* parent) { + auto* path_wrapper = lv_obj_create(parent); + lv_obj_set_size(path_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(path_wrapper, 0, 0); + lv_obj_set_style_border_width(path_wrapper, 0, 0); + lv_obj_set_flex_flow(path_wrapper, LV_FLEX_FLOW_ROW); + + auto* label_wrapper = lv_obj_create(path_wrapper); + lv_obj_set_style_border_width(label_wrapper, 0, 0); + lv_obj_set_style_pad_all(label_wrapper, 0, 0); + lv_obj_set_size(label_wrapper, 44, 36); + auto* path_label = lv_label_create(label_wrapper); + lv_label_set_text(path_label, "Path:"); + lv_obj_align(path_label, LV_ALIGN_LEFT_MID, 0, 0); + + pathTextArea = lv_textarea_create(path_wrapper); + lv_textarea_set_one_line(pathTextArea, true); + lv_obj_set_flex_grow(pathTextArea, 1); + if (kernel::getPlatform() == kernel::PlatformEsp) { + auto sdcard = tt::hal::getConfiguration()->sdcard; + if (sdcard != nullptr && sdcard->getState() == hal::SdCard::State::Mounted) { + lv_textarea_set_text(pathTextArea, "A:/sdcard"); + } else { + lv_textarea_set_text(pathTextArea, "Error: no SD card"); + } + } else { // PC + lv_textarea_set_text(pathTextArea, "A:"); + } +} + +void ScreenshotApp::createTimerSettingsWidgets(lv_obj_t* parent) { + timerWrapper = lv_obj_create(parent); + lv_obj_set_size(timerWrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(timerWrapper, 0, 0); + lv_obj_set_style_border_width(timerWrapper, 0, 0); + + auto* delay_wrapper = lv_obj_create(timerWrapper); + lv_obj_set_size(delay_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(delay_wrapper, 0, 0); + lv_obj_set_style_border_width(delay_wrapper, 0, 0); + lv_obj_set_flex_flow(delay_wrapper, LV_FLEX_FLOW_ROW); + + auto* delay_label_wrapper = lv_obj_create(delay_wrapper); + lv_obj_set_style_border_width(delay_label_wrapper, 0, 0); + lv_obj_set_style_pad_all(delay_label_wrapper, 0, 0); + lv_obj_set_size(delay_label_wrapper, 44, 36); + auto* delay_label = lv_label_create(delay_label_wrapper); + lv_label_set_text(delay_label, "Delay:"); + lv_obj_align(delay_label, LV_ALIGN_LEFT_MID, 0, 0); + + delayTextArea = lv_textarea_create(delay_wrapper); + lv_textarea_set_one_line(delayTextArea, true); + lv_textarea_set_accepted_chars(delayTextArea, "0123456789"); + lv_textarea_set_text(delayTextArea, "10"); + lv_obj_set_flex_grow(delayTextArea, 1); + + auto* delay_unit_label_wrapper = lv_obj_create(delay_wrapper); + lv_obj_set_style_border_width(delay_unit_label_wrapper, 0, 0); + lv_obj_set_style_pad_all(delay_unit_label_wrapper, 0, 0); + lv_obj_set_size(delay_unit_label_wrapper, LV_SIZE_CONTENT, 36); + auto* delay_unit_label = lv_label_create(delay_unit_label_wrapper); + lv_obj_align(delay_unit_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_label_set_text(delay_unit_label, "seconds"); +} + +void ScreenshotApp::onShow(AppContext& appContext, lv_obj_t* parent) { + if (updateTimer->isRunning()) { + updateTimer->stop(); + } + + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + auto* toolbar = lvgl::toolbar_create(parent, appContext); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + auto* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + + createModeSettingWidgets(wrapper); + createFilePathWidgets(wrapper); + createTimerSettingsWidgets(wrapper); + + service::gui::keyboardAddTextArea(delayTextArea); + service::gui::keyboardAddTextArea(pathTextArea); + + updateScreenshotMode(); + + if (!updateTimer->isRunning()) { + updateTimer->start(500 / portTICK_PERIOD_MS); + } } extern const AppManifest manifest = { .id = "Screenshot", .name = "Screenshot", .icon = LV_SYMBOL_IMAGE, - .type = TypeSystem, - .onStart = onStart, - .onShow = onShow, + .type = Type::System, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/screenshot/ScreenshotUi.cpp b/Tactility/Source/app/screenshot/ScreenshotUi.cpp deleted file mode 100644 index f945ca20..00000000 --- a/Tactility/Source/app/screenshot/ScreenshotUi.cpp +++ /dev/null @@ -1,256 +0,0 @@ -#include "TactilityConfig.h" - -#if TT_FEATURE_SCREENSHOT_ENABLED - -#include "app/screenshot/ScreenshotUi.h" - -#include "TactilityCore.h" -#include "hal/SdCard.h" -#include "service/gui/Gui.h" -#include "service/loader/Loader.h" -#include "service/screenshot/Screenshot.h" -#include "lvgl/Toolbar.h" -#include "TactilityHeadless.h" -#include "lvgl/LvglSync.h" - -namespace tt::app::screenshot { - -#define TAG "screenshot_ui" - -extern AppManifest manifest; - -/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ -std::shared_ptr _Nullable optScreenshotUi() { - app::AppContext* app = service::loader::getCurrentApp(); - if (app->getManifest().id == manifest.id) { - return std::static_pointer_cast(app->getData()); - } else { - return nullptr; - } -} - -static void onStartPressedCallback(TT_UNUSED lv_event_t* event) { - auto ui = optScreenshotUi(); - if (ui != nullptr) { - ui->onStartPressed(); - } -} - -static void onModeSetCallback(TT_UNUSED lv_event_t* event) { - auto ui = optScreenshotUi(); - if (ui != nullptr) { - ui->onModeSet(); - } -} - -static void onTimerCallback(TT_UNUSED std::shared_ptr context) { - auto screenshot_ui = optScreenshotUi(); - if (screenshot_ui != nullptr) { - screenshot_ui->onTimerTick(); - } -} - -ScreenshotUi::ScreenshotUi() { - updateTimer = std::make_unique(Timer::Type::Periodic, onTimerCallback, nullptr); -} - -ScreenshotUi::~ScreenshotUi() { - if (updateTimer->isRunning()) { - updateTimer->stop(); - } -} - -void ScreenshotUi::onTimerTick() { - auto lvgl_lock = lvgl::getLvglSyncLockable()->scoped(); - if (lvgl_lock->lock(50 / portTICK_PERIOD_MS)) { - updateScreenshotMode(); - } -} - -void ScreenshotUi::onModeSet() { - updateScreenshotMode(); -} - -void ScreenshotUi::onStartPressed() { - auto service = service::screenshot::optScreenshotService(); - if (service == nullptr) { - TT_LOG_E(TAG, "Service not found/running"); - return; - } - - if (service->isTaskStarted()) { - TT_LOG_I(TAG, "Stop screenshot"); - service->stop(); - } else { - uint32_t selected = lv_dropdown_get_selected(modeDropdown); - const char* path = lv_textarea_get_text(pathTextArea); - if (selected == 0) { - TT_LOG_I(TAG, "Start timed screenshots"); - const char* delay_text = lv_textarea_get_text(delayTextArea); - int delay = atoi(delay_text); - if (delay > 0) { - service->startTimed(path, delay, 1); - } else { - TT_LOG_W(TAG, "Ignored screenshot start because delay was 0"); - } - } else { - TT_LOG_I(TAG, "Start app screenshots"); - service->startApps(path); - } - } - - updateScreenshotMode(); -} - -void ScreenshotUi::updateScreenshotMode() { - auto service = service::screenshot::optScreenshotService(); - if (service == nullptr) { - TT_LOG_E(TAG, "Service not found/running"); - return; - } - - lv_obj_t* label = startStopButtonLabel; - if (service->isTaskStarted()) { - lv_label_set_text(label, "Stop"); - } else { - lv_label_set_text(label, "Start"); - } - - uint32_t selected = lv_dropdown_get_selected(modeDropdown); - if (selected == 0) { // Timer - lv_obj_remove_flag(timerWrapper, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(timerWrapper, LV_OBJ_FLAG_HIDDEN); - } -} - - -void ScreenshotUi::createModeSettingWidgets(lv_obj_t* parent) { - auto service = service::screenshot::optScreenshotService(); - if (service == nullptr) { - TT_LOG_E(TAG, "Service not found/running"); - return; - } - - auto* mode_wrapper = lv_obj_create(parent); - lv_obj_set_size(mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(mode_wrapper, 0, 0); - lv_obj_set_style_border_width(mode_wrapper, 0, 0); - - auto* mode_label = lv_label_create(mode_wrapper); - lv_label_set_text(mode_label, "Mode:"); - lv_obj_align(mode_label, LV_ALIGN_LEFT_MID, 0, 0); - - modeDropdown = lv_dropdown_create(mode_wrapper); - lv_dropdown_set_options(modeDropdown, "Timer\nApp start"); - lv_obj_align_to(modeDropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0); - lv_obj_add_event_cb(modeDropdown, onModeSetCallback, LV_EVENT_VALUE_CHANGED, nullptr); - service::screenshot::Mode mode = service->getMode(); - if (mode == service::screenshot::Mode::Apps) { - lv_dropdown_set_selected(modeDropdown, 1); - } - - auto* button = lv_button_create(mode_wrapper); - lv_obj_align(button, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(button, &onStartPressedCallback, LV_EVENT_SHORT_CLICKED, nullptr); - startStopButtonLabel = lv_label_create(button); - lv_obj_align(startStopButtonLabel, LV_ALIGN_CENTER, 0, 0); -} - -void ScreenshotUi::createFilePathWidgets(lv_obj_t* parent) { - auto* path_wrapper = lv_obj_create(parent); - lv_obj_set_size(path_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(path_wrapper, 0, 0); - lv_obj_set_style_border_width(path_wrapper, 0, 0); - lv_obj_set_flex_flow(path_wrapper, LV_FLEX_FLOW_ROW); - - auto* label_wrapper = lv_obj_create(path_wrapper); - lv_obj_set_style_border_width(label_wrapper, 0, 0); - lv_obj_set_style_pad_all(label_wrapper, 0, 0); - lv_obj_set_size(label_wrapper, 44, 36); - auto* path_label = lv_label_create(label_wrapper); - lv_label_set_text(path_label, "Path:"); - lv_obj_align(path_label, LV_ALIGN_LEFT_MID, 0, 0); - - pathTextArea = lv_textarea_create(path_wrapper); - lv_textarea_set_one_line(pathTextArea, true); - lv_obj_set_flex_grow(pathTextArea, 1); - if (kernel::getPlatform() == kernel::PlatformEsp) { - auto sdcard = tt::hal::getConfiguration()->sdcard; - if (sdcard != nullptr && sdcard->getState() == hal::SdCard::State::Mounted) { - lv_textarea_set_text(pathTextArea, "A:/sdcard"); - } else { - lv_textarea_set_text(pathTextArea, "Error: no SD card"); - } - } else { // PC - lv_textarea_set_text(pathTextArea, "A:"); - } -} - -void ScreenshotUi::createTimerSettingsWidgets(lv_obj_t* parent) { - timerWrapper = lv_obj_create(parent); - lv_obj_set_size(timerWrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(timerWrapper, 0, 0); - lv_obj_set_style_border_width(timerWrapper, 0, 0); - - auto* delay_wrapper = lv_obj_create(timerWrapper); - lv_obj_set_size(delay_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(delay_wrapper, 0, 0); - lv_obj_set_style_border_width(delay_wrapper, 0, 0); - lv_obj_set_flex_flow(delay_wrapper, LV_FLEX_FLOW_ROW); - - auto* delay_label_wrapper = lv_obj_create(delay_wrapper); - lv_obj_set_style_border_width(delay_label_wrapper, 0, 0); - lv_obj_set_style_pad_all(delay_label_wrapper, 0, 0); - lv_obj_set_size(delay_label_wrapper, 44, 36); - auto* delay_label = lv_label_create(delay_label_wrapper); - lv_label_set_text(delay_label, "Delay:"); - lv_obj_align(delay_label, LV_ALIGN_LEFT_MID, 0, 0); - - delayTextArea = lv_textarea_create(delay_wrapper); - lv_textarea_set_one_line(delayTextArea, true); - lv_textarea_set_accepted_chars(delayTextArea, "0123456789"); - lv_textarea_set_text(delayTextArea, "10"); - lv_obj_set_flex_grow(delayTextArea, 1); - - auto* delay_unit_label_wrapper = lv_obj_create(delay_wrapper); - lv_obj_set_style_border_width(delay_unit_label_wrapper, 0, 0); - lv_obj_set_style_pad_all(delay_unit_label_wrapper, 0, 0); - lv_obj_set_size(delay_unit_label_wrapper, LV_SIZE_CONTENT, 36); - auto* delay_unit_label = lv_label_create(delay_unit_label_wrapper); - lv_obj_align(delay_unit_label, LV_ALIGN_LEFT_MID, 0, 0); - lv_label_set_text(delay_unit_label, "seconds"); -} - -void ScreenshotUi::createWidgets(const AppContext& app, lv_obj_t* parent) { - if (updateTimer->isRunning()) { - updateTimer->stop(); - } - - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - auto* toolbar = lvgl::toolbar_create(parent, app); - lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); - - auto* wrapper = lv_obj_create(parent); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_flex_grow(wrapper, 1); - lv_obj_set_style_border_width(wrapper, 0, 0); - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); - - createModeSettingWidgets(wrapper); - createFilePathWidgets(wrapper); - createTimerSettingsWidgets(wrapper); - - service::gui::keyboardAddTextArea(delayTextArea); - service::gui::keyboardAddTextArea(pathTextArea); - - updateScreenshotMode(); - - if (!updateTimer->isRunning()) { - updateTimer->start(500 / portTICK_PERIOD_MS); - } -} - -} // namespace - -#endif \ No newline at end of file diff --git a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp index 09034332..f3e0e8b7 100644 --- a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp +++ b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp @@ -32,10 +32,6 @@ int32_t getResultIndex(const Bundle& bundle) { return index; } -void setResultIndex(std::shared_ptr bundle, int32_t index) { - bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, index); -} - static std::string getTitleParameter(std::shared_ptr bundle) { std::string result; if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) { @@ -45,64 +41,77 @@ static std::string getTitleParameter(std::shared_ptr bundle) { } } -static void onListItemSelected(lv_event_t* e) { - size_t index = reinterpret_cast(lv_event_get_user_data(e)); - TT_LOG_I(TAG, "Selected item at index %d", index); - tt::app::AppContext* app = service::loader::getCurrentApp(); - auto bundle = std::make_shared(); - setResultIndex(bundle, (int32_t)index); - app->setResult(app::ResultOk, bundle); - service::loader::stopApp(); -} +class SelectionDialogApp : public App { -static void createChoiceItem(void* parent, const std::string& title, size_t index) { - auto* list = static_cast(parent); - lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str()); - lv_obj_add_event_cb(btn, &onListItemSelected, LV_EVENT_SHORT_CLICKED, (void*)index); -} +private: -static void onShow(AppContext& app, lv_obj_t* parent) { - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - std::string title = getTitleParameter(app.getParameters()); - lvgl::toolbar_create(parent, title); + static void onListItemSelectedCallback(lv_event_t* e) { + auto appContext = service::loader::getCurrentAppContext(); + tt_assert(appContext != nullptr); + auto app = std::static_pointer_cast(appContext->getApp()); + app->onListItemSelected(e); + } - lv_obj_t* list = lv_list_create(parent); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_set_flex_grow(list, 1); - - auto parameters = app.getParameters(); - tt_check(parameters != nullptr, "Parameters missing"); - std::string items_concatenated; - if (parameters->optString(PARAMETER_BUNDLE_KEY_ITEMS, items_concatenated)) { - std::vector items = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN); - if (items.empty() || items.front().empty()) { - TT_LOG_E(TAG, "No items provided"); - app.setResult(ResultError); - service::loader::stopApp(); - } else if (items.size() == 1) { - auto result_bundle = std::make_shared(); - setResultIndex(result_bundle, 0); - app.setResult(ResultOk, result_bundle); - service::loader::stopApp(); - TT_LOG_W(TAG, "Auto-selecting single item"); - } else { - size_t index = 0; - for (const auto& item: items) { - createChoiceItem(list, item, index++); - } - } - } else { - TT_LOG_E(TAG, "No items provided"); - app.setResult(ResultError); + void onListItemSelected(lv_event_t* e) { + size_t index = reinterpret_cast(lv_event_get_user_data(e)); + TT_LOG_I(TAG, "Selected item at index %d", index); + auto bundle = std::make_unique(); + bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index); + setResult(app::Result::Ok, std::move(bundle)); service::loader::stopApp(); } -} + + static void createChoiceItem(void* parent, const std::string& title, size_t index) { + auto* list = static_cast(parent); + lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str()); + lv_obj_add_event_cb(btn, onListItemSelectedCallback, LV_EVENT_SHORT_CLICKED, (void*)index); + } + +public: + + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + std::string title = getTitleParameter(app.getParameters()); + lvgl::toolbar_create(parent, title); + + lv_obj_t* list = lv_list_create(parent); + lv_obj_set_width(list, LV_PCT(100)); + lv_obj_set_flex_grow(list, 1); + + auto parameters = app.getParameters(); + tt_check(parameters != nullptr, "Parameters missing"); + std::string items_concatenated; + if (parameters->optString(PARAMETER_BUNDLE_KEY_ITEMS, items_concatenated)) { + std::vector items = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN); + if (items.empty() || items.front().empty()) { + TT_LOG_E(TAG, "No items provided"); + setResult(Result::Error); + service::loader::stopApp(); + } else if (items.size() == 1) { + auto result_bundle = std::make_unique(); + result_bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, 0); + setResult(Result::Ok, std::move(result_bundle)); + service::loader::stopApp(); + TT_LOG_W(TAG, "Auto-selecting single item"); + } else { + size_t index = 0; + for (const auto& item: items) { + createChoiceItem(list, item, index++); + } + } + } else { + TT_LOG_E(TAG, "No items provided"); + setResult(Result::Error); + service::loader::stopApp(); + } + } +}; extern const AppManifest manifest = { - .id = "SelectionDialog", - .name = "Selection Dialog", - .type = TypeHidden, - .onShow = onShow + .id = "SelectionDialog", + .name = "Selection Dialog", + .type = Type::Hidden, + .createApp = create }; } diff --git a/Tactility/Source/app/settings/Settings.cpp b/Tactility/Source/app/settings/Settings.cpp index c06c8df1..d09596bf 100644 --- a/Tactility/Source/app/settings/Settings.cpp +++ b/Tactility/Source/app/settings/Settings.cpp @@ -13,38 +13,41 @@ static void onAppPressed(lv_event_t* e) { service::loader::startApp(manifest->id); } -static void createWidget(const AppManifest* manifest, void* parent) { +static void createWidget(const std::shared_ptr& manifest, void* parent) { tt_check(parent); auto* list = (lv_obj_t*)parent; const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str()); - lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest); + lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest.get()); } -static void onShow(AppContext& app, lv_obj_t* parent) { - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); +class SettingsApp : public App { - lvgl::toolbar_create(parent, app); + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - lv_obj_t* list = lv_list_create(parent); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_set_flex_grow(list, 1); + lvgl::toolbar_create(parent, app); - auto manifests = getApps(); - std::sort(manifests.begin(), manifests.end(), SortAppManifestByName); - for (const auto& manifest: manifests) { - if (manifest->type == TypeSettings) { - createWidget(manifest, list); + lv_obj_t* list = lv_list_create(parent); + lv_obj_set_width(list, LV_PCT(100)); + lv_obj_set_flex_grow(list, 1); + + auto manifests = getApps(); + std::sort(manifests.begin(), manifests.end(), SortAppManifestByName); + for (const auto& manifest: manifests) { + if (manifest->type == Type::Settings) { + createWidget(manifest, list); + } } } -} +}; extern const AppManifest manifest = { .id = "Settings", .name = "Settings", .icon = TT_ASSETS_APP_ICON_SETTINGS, - .type = TypeHidden, - .onShow = onShow, + .type = Type::Hidden, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index e22ca17f..b74fa888 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -108,57 +108,58 @@ static void addRtosTasks(lv_obj_t* parent) { #endif -static void onShow(AppContext& app, lv_obj_t* parent) { - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - lvgl::toolbar_create(parent, app); +class SystemInfoApp : public App { - // This wrapper automatically has its children added vertically underneath eachother - lv_obj_t* wrapper = lv_obj_create(parent); - lv_obj_set_style_border_width(wrapper, 0, 0); - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_flex_grow(wrapper, 1); + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lvgl::toolbar_create(parent, app); - // Wrapper for the memory usage bars - lv_obj_t* memory_label = lv_label_create(wrapper); - lv_label_set_text(memory_label, "Memory usage"); - lv_obj_t* memory_wrapper = lv_obj_create(wrapper); - lv_obj_set_flex_flow(memory_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_size(memory_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + // This wrapper automatically has its children added vertically underneath eachother + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); - addMemoryBar(memory_wrapper, "Heap", getHeapTotal() - getHeapFree(), getHeapTotal()); - addMemoryBar(memory_wrapper, "SPI", getSpiTotal() - getSpiFree(), getSpiTotal()); + // Wrapper for the memory usage bars + lv_obj_t* memory_label = lv_label_create(wrapper); + lv_label_set_text(memory_label, "Memory usage"); + lv_obj_t* memory_wrapper = lv_obj_create(wrapper); + lv_obj_set_flex_flow(memory_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_size(memory_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + + addMemoryBar(memory_wrapper, "Heap", getHeapTotal() - getHeapFree(), getHeapTotal()); + addMemoryBar(memory_wrapper, "SPI", getSpiTotal() - getSpiFree(), getSpiTotal()); #if configUSE_TRACE_FACILITY - lv_obj_t* tasks_label = lv_label_create(wrapper); - lv_label_set_text(tasks_label, "Tasks"); - lv_obj_t* tasks_wrapper = lv_obj_create(wrapper); - lv_obj_set_flex_flow(tasks_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_size(tasks_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - addRtosTasks(tasks_wrapper); + lv_obj_t* tasks_label = lv_label_create(wrapper); + lv_label_set_text(tasks_label, "Tasks"); + lv_obj_t* tasks_wrapper = lv_obj_create(wrapper); + lv_obj_set_flex_flow(tasks_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_size(tasks_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + addRtosTasks(tasks_wrapper); #endif #ifdef ESP_PLATFORM - // Build info - lv_obj_t* build_info_label = lv_label_create(wrapper); - lv_label_set_text(build_info_label, "Build info"); - lv_obj_t* build_info_wrapper = lv_obj_create(wrapper); - lv_obj_set_flex_flow(build_info_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_size(build_info_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + // Build info + lv_obj_t* build_info_label = lv_label_create(wrapper); + lv_label_set_text(build_info_label, "Build info"); + lv_obj_t* build_info_wrapper = lv_obj_create(wrapper); + lv_obj_set_flex_flow(build_info_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_size(build_info_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_t* esp_idf_version = lv_label_create(build_info_wrapper); - lv_label_set_text_fmt(esp_idf_version, "IDF version: %d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH); + lv_obj_t* esp_idf_version = lv_label_create(build_info_wrapper); + lv_label_set_text_fmt(esp_idf_version, "IDF version: %d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH); #endif -} + } +}; extern const AppManifest manifest = { .id = "SystemInfo", .name = "System Info", .icon = TT_ASSETS_APP_ICON_SYSTEM_INFO, - .type = TypeSystem, - .onStart = nullptr, - .onStop = nullptr, - .onShow = onShow + .type = Type::System, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/textviewer/TextViewer.cpp b/Tactility/Source/app/textviewer/TextViewer.cpp index f4d50d40..f5f88212 100644 --- a/Tactility/Source/app/textviewer/TextViewer.cpp +++ b/Tactility/Source/app/textviewer/TextViewer.cpp @@ -11,40 +11,43 @@ namespace tt::app::textviewer { -static void onShow(AppContext& app, lv_obj_t* parent) { - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - lvgl::toolbar_create(parent, app); +class TextViewerApp : public App { - lv_obj_t* wrapper = lv_obj_create(parent); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_flex_grow(wrapper, 1); - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); - lvgl::obj_set_style_no_padding(wrapper); - lvgl::obj_set_style_bg_invisible(wrapper); + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lvgl::toolbar_create(parent, app); - lv_obj_t* label = lv_label_create(wrapper); - lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); - auto parameters = app.getParameters(); - tt_check(parameters != nullptr, "Parameters missing"); - bool success = false; - std::string file_argument; - if (parameters->optString(TEXT_VIEWER_FILE_ARGUMENT, file_argument)) { - TT_LOG_I(TAG, "Opening %s", file_argument.c_str()); - if (lvgl::label_set_text_file(label, file_argument.c_str())) { - success = true; + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lvgl::obj_set_style_no_padding(wrapper); + lvgl::obj_set_style_bg_invisible(wrapper); + + lv_obj_t* label = lv_label_create(wrapper); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + auto parameters = app.getParameters(); + tt_check(parameters != nullptr, "Parameters missing"); + bool success = false; + std::string file_argument; + if (parameters->optString(TEXT_VIEWER_FILE_ARGUMENT, file_argument)) { + TT_LOG_I(TAG, "Opening %s", file_argument.c_str()); + if (lvgl::label_set_text_file(label, file_argument.c_str())) { + success = true; + } + } + + if (!success) { + lv_label_set_text_fmt(label, "Failed to load %s", file_argument.c_str()); } } - - if (!success) { - lv_label_set_text_fmt(label, "Failed to load %s", file_argument.c_str()); - } -} +}; extern const AppManifest manifest = { .id = "TextViewer", .name = "Text Viewer", - .type = TypeHidden, - .onShow = onShow + .type = Type::Hidden, + .createApp = create }; void start(const std::string& file) { diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index d0d2f311..19668610 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -13,119 +13,104 @@ namespace tt::app::timedatesettings { extern const AppManifest manifest; -struct Data { +class TimeDateSettingsApp : public App { + +private: + Mutex mutex = Mutex(Mutex::Type::Recursive); lv_obj_t* regionLabelWidget = nullptr; -}; -/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ -std::shared_ptr _Nullable optData() { - app::AppContext* app = service::loader::getCurrentApp(); - if (app->getManifest().id == manifest.id) { - return std::static_pointer_cast(app->getData()); - } else { - return nullptr; + static void onConfigureTimeZonePressed(TT_UNUSED lv_event_t* event) { + timezone::start(); } -} -static void onConfigureTimeZonePressed(TT_UNUSED lv_event_t* event) { - timezone::start(); -} - -static void onTimeFormatChanged(lv_event_t* event) { - auto* widget = lv_event_get_target_obj(event); - bool show_24 = lv_obj_has_state(widget, LV_STATE_CHECKED); - time::setTimeFormat24Hour(show_24); -} - -static void onShow(AppContext& app, lv_obj_t* parent) { - auto data = std::static_pointer_cast(app.getData()); - - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - - lvgl::toolbar_create(parent, app); - - auto* main_wrapper = lv_obj_create(parent); - lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_width(main_wrapper, LV_PCT(100)); - lv_obj_set_flex_grow(main_wrapper, 1); - - auto* region_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_width(region_wrapper, LV_PCT(100)); - lv_obj_set_height(region_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(region_wrapper, 0, 0); - lv_obj_set_style_border_width(region_wrapper, 0, 0); - - auto* region_prefix_label = lv_label_create(region_wrapper); - lv_label_set_text(region_prefix_label, "Region: "); - lv_obj_align(region_prefix_label, LV_ALIGN_LEFT_MID, 0, 0); - - auto* region_label = lv_label_create(region_wrapper); - std::string timeZoneName = time::getTimeZoneName(); - if (timeZoneName.empty()) { - timeZoneName = "not set"; + static void onTimeFormatChanged(lv_event_t* event) { + auto* widget = lv_event_get_target_obj(event); + bool show_24 = lv_obj_has_state(widget, LV_STATE_CHECKED); + time::setTimeFormat24Hour(show_24); } - data->regionLabelWidget = region_label; - lv_label_set_text(region_label, timeZoneName.c_str()); - // TODO: Find out why Y offset is needed - lv_obj_align_to(region_label, region_prefix_label, LV_ALIGN_OUT_RIGHT_MID, 0, 8); - auto* region_button = lv_button_create(region_wrapper); - lv_obj_align(region_button, LV_ALIGN_TOP_RIGHT, 0, 0); - auto* region_button_image = lv_image_create(region_button); - lv_obj_add_event_cb(region_button, onConfigureTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr); - lv_image_set_src(region_button_image, LV_SYMBOL_SETTINGS); +public: - auto* time_format_wrapper= lv_obj_create(main_wrapper); - lv_obj_set_width(time_format_wrapper, LV_PCT(100)); - lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(time_format_wrapper, 0, 0); - lv_obj_set_style_border_width(time_format_wrapper, 0, 0); + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - auto* time_24h_label = lv_label_create(time_format_wrapper); - lv_label_set_text(time_24h_label, "24-hour clock"); - lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 0, 0); + lvgl::toolbar_create(parent, app); - auto* time_24h_switch = lv_switch_create(time_format_wrapper); - lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(time_24h_switch, onTimeFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr); - if (time::isTimeFormat24Hour()) { - lv_obj_add_state(time_24h_switch, LV_STATE_CHECKED); - } else { - lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED); + auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_width(main_wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(main_wrapper, 1); + + auto* region_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_width(region_wrapper, LV_PCT(100)); + lv_obj_set_height(region_wrapper, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(region_wrapper, 0, 0); + lv_obj_set_style_border_width(region_wrapper, 0, 0); + + auto* region_prefix_label = lv_label_create(region_wrapper); + lv_label_set_text(region_prefix_label, "Region: "); + lv_obj_align(region_prefix_label, LV_ALIGN_LEFT_MID, 0, 0); + + auto* region_label = lv_label_create(region_wrapper); + std::string timeZoneName = time::getTimeZoneName(); + if (timeZoneName.empty()) { + timeZoneName = "not set"; + } + regionLabelWidget = region_label; + lv_label_set_text(region_label, timeZoneName.c_str()); + // TODO: Find out why Y offset is needed + lv_obj_align_to(region_label, region_prefix_label, LV_ALIGN_OUT_RIGHT_MID, 0, 8); + + auto* region_button = lv_button_create(region_wrapper); + lv_obj_align(region_button, LV_ALIGN_TOP_RIGHT, 0, 0); + auto* region_button_image = lv_image_create(region_button); + lv_obj_add_event_cb(region_button, onConfigureTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr); + lv_image_set_src(region_button_image, LV_SYMBOL_SETTINGS); + + auto* time_format_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_width(time_format_wrapper, LV_PCT(100)); + lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(time_format_wrapper, 0, 0); + lv_obj_set_style_border_width(time_format_wrapper, 0, 0); + + auto* time_24h_label = lv_label_create(time_format_wrapper); + lv_label_set_text(time_24h_label, "24-hour clock"); + lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 0, 0); + + auto* time_24h_switch = lv_switch_create(time_format_wrapper); + lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(time_24h_switch, onTimeFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr); + if (time::isTimeFormat24Hour()) { + lv_obj_add_state(time_24h_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED); + } } -} -static void onStart(AppContext& app) { - auto data = std::make_shared(); - app.setData(data); -} + void onResult(AppContext& app, Result result, std::unique_ptr bundle) override { + if (result == Result::Ok) { + auto name = timezone::getResultName(*bundle); + auto code = timezone::getResultCode(*bundle); + TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str()); + time::setTimeZone(name, code); -static void onResult(AppContext& app, Result result, const Bundle& bundle) { - if (result == ResultOk) { - auto data = std::static_pointer_cast(app.getData()); - auto name = timezone::getResultName(bundle); - auto code = timezone::getResultCode(bundle); - TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str()); - time::setTimeZone(name, code); - - if (!name.empty()) { - if (lvgl::lock(100 / portTICK_PERIOD_MS)) { - lv_label_set_text(data->regionLabelWidget, name.c_str()); - lvgl::unlock(); + if (!name.empty()) { + if (lvgl::lock(100 / portTICK_PERIOD_MS)) { + lv_label_set_text(regionLabelWidget, name.c_str()); + lvgl::unlock(); + } } } } -} +}; extern const AppManifest manifest = { .id = "TimeDateSettings", .name = "Time & Date", .icon = TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS, - .type = TypeSettings, - .onStart = onStart, - .onShow = onShow, - .onResult = onResult + .type = Type::Settings, + .createApp = create }; void start() { diff --git a/Tactility/Source/app/timezone/TimeZone.cpp b/Tactility/Source/app/timezone/TimeZone.cpp index 1bd281e2..0a0a8c9c 100644 --- a/Tactility/Source/app/timezone/TimeZone.cpp +++ b/Tactility/Source/app/timezone/TimeZone.cpp @@ -26,16 +26,6 @@ struct TimeZoneEntry { std::string code; }; -struct Data { - Mutex mutex; - std::vector entries; - std::unique_ptr updateTimer; - lv_obj_t* listWidget = nullptr; - lv_obj_t* filterTextareaWidget = nullptr; -}; - -static void updateList(std::shared_ptr& data); - static bool parseEntry(const std::string& input, std::string& outName, std::string& outCode) { std::string partial_strip = input.substr(1, input.size() - 3); auto first_end_quote = partial_strip.find('"'); @@ -62,178 +52,195 @@ std::string getResultCode(const Bundle& bundle) { return result; } -void setResultName(std::shared_ptr& bundle, const std::string& name) { - bundle->putString(RESULT_BUNDLE_NAME_INDEX, name); +void setResultName(Bundle& bundle, const std::string& name) { + bundle.putString(RESULT_BUNDLE_NAME_INDEX, name); } -void setResultCode(std::shared_ptr& bundle, const std::string& code) { - bundle->putString(RESULT_BUNDLE_CODE_INDEX, code); +void setResultCode(Bundle& bundle, const std::string& code) { + bundle.putString(RESULT_BUNDLE_CODE_INDEX, code); } // endregion -static void onUpdateTimer(std::shared_ptr context) { - auto data = std::static_pointer_cast(context); - updateList(data); -} -static void onTextareaValueChanged(TT_UNUSED lv_event_t* e) { - auto* app = service::loader::getCurrentApp(); - auto app_data = app->getData(); - auto data = std::static_pointer_cast(app_data); +class TimeZoneApp : public App { - if (data->mutex.lock(100 / portTICK_PERIOD_MS)) { - if (data->updateTimer->isRunning()) { - data->updateTimer->stop(); - } +private: - data->updateTimer->start(500 / portTICK_PERIOD_MS); - - data->mutex.unlock(); - } -} - -static void onListItemSelected(lv_event_t* e) { - auto index = reinterpret_cast(lv_event_get_user_data(e)); - TT_LOG_I(TAG, "Selected item at index %zu", index); - auto* app = service::loader::getCurrentApp(); - auto data = std::static_pointer_cast(app->getData()); - - auto& entry = data->entries[index]; - - auto bundle = std::make_shared(); - setResultName(bundle, entry.name); - setResultCode(bundle, entry.code); - app->setResult(app::ResultOk, bundle); - - service::loader::stopApp(); -} - -static void createListItem(lv_obj_t* list, const std::string& title, size_t index) { - lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str()); - lv_obj_add_event_cb(btn, &onListItemSelected, LV_EVENT_SHORT_CLICKED, (void*)index); -} - -static void readTimeZones(const std::shared_ptr& data, std::string filter) { - auto path = std::string(MOUNT_POINT_SYSTEM) + "/timezones.csv"; - auto* file = fopen(path.c_str(), "rb"); - if (file == nullptr) { - TT_LOG_E(TAG, "Failed to open %s", path.c_str()); - return; - } - char line[96]; - std::string name; - std::string code; - uint32_t count = 0; + Mutex mutex; std::vector entries; - while (fgets(line, 96, file)) { - if (parseEntry(line, name, code)) { - if (tt::string::lowercase(name).find(filter) != std::string::npos) { - count++; - entries.push_back({ - .name = name, - .code = code - }); + std::unique_ptr updateTimer; + lv_obj_t* listWidget = nullptr; + lv_obj_t* filterTextareaWidget = nullptr; - // Safety guard - if (count > 50) { - // TODO: Show warning that we're not displaying a complete list - break; + static void onTextareaValueChangedCallback(TT_UNUSED lv_event_t* e) { + auto* app = (TimeZoneApp*)lv_event_get_user_data(e); + app->onTextareaValueChanged(e); + } + + void onTextareaValueChanged(TT_UNUSED lv_event_t* e) { + if (mutex.lock(100 / portTICK_PERIOD_MS)) { + if (updateTimer->isRunning()) { + updateTimer->stop(); + } + + updateTimer->start(500 / portTICK_PERIOD_MS); + + mutex.unlock(); + } + } + + static void onListItemSelectedCallback(lv_event_t* e) { + auto index = reinterpret_cast(lv_event_get_user_data(e)); + auto appContext = service::loader::getCurrentAppContext(); + if (appContext != nullptr && appContext->getManifest().id == manifest.id) { + auto app = std::static_pointer_cast(appContext->getApp()); + app->onListItemSelected(index); + } + } + + void onListItemSelected(std::size_t index) { + TT_LOG_I(TAG, "Selected item at index %zu", index); + + auto& entry = entries[index]; + + auto bundle = std::make_unique(); + setResultName(*bundle, entry.name); + setResultCode(*bundle, entry.code); + + setResult(app::Result::Ok, std::move(bundle)); + + service::loader::stopApp(); + } + + static void createListItem(lv_obj_t* list, const std::string& title, size_t index) { + lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str()); + lv_obj_add_event_cb(btn, &onListItemSelectedCallback, LV_EVENT_SHORT_CLICKED, (void*)index); + } + + static void updateTimerCallback(std::shared_ptr context) { + auto appContext = service::loader::getCurrentAppContext(); + if (appContext != nullptr && appContext->getManifest().id == manifest.id) { + auto app = std::static_pointer_cast(appContext->getApp()); + app->updateList(); + } + } + + void readTimeZones(std::string filter) { + auto path = std::string(MOUNT_POINT_SYSTEM) + "/timezones.csv"; + auto* file = fopen(path.c_str(), "rb"); + if (file == nullptr) { + TT_LOG_E(TAG, "Failed to open %s", path.c_str()); + return; + } + char line[96]; + std::string name; + std::string code; + uint32_t count = 0; + std::vector new_entries; + while (fgets(line, 96, file)) { + if (parseEntry(line, name, code)) { + if (tt::string::lowercase(name).find(filter) != std::string::npos) { + count++; + new_entries.push_back({.name = name, .code = code}); + + // Safety guard + if (count > 50) { + // TODO: Show warning that we're not displaying a complete list + break; + } } + } else { + TT_LOG_E(TAG, "Parse error at line %lu", count); } + } + + fclose(file); + + if (mutex.lock(100 / portTICK_PERIOD_MS)) { + entries = std::move(new_entries); + mutex.unlock(); } else { - TT_LOG_E(TAG, "Parse error at line %lu", count); + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED); } + + TT_LOG_I(TAG, "Processed %lu entries", count); } - fclose(file); + void updateList() { + if (lvgl::lock(100 / portTICK_PERIOD_MS)) { + std::string filter = tt::string::lowercase(std::string(lv_textarea_get_text(filterTextareaWidget))); + readTimeZones(filter); + lvgl::unlock(); + } else { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL"); + return; + } - if (data->mutex.lock(100 / portTICK_PERIOD_MS)) { - data->entries = std::move(entries); - data->mutex.unlock(); - } else { - TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED); - } + if (lvgl::lock(100 / portTICK_PERIOD_MS)) { + if (mutex.lock(100 / portTICK_PERIOD_MS)) { + lv_obj_clean(listWidget); - TT_LOG_I(TAG, "Processed %lu entries", count); -} + uint32_t index = 0; + for (auto& entry : entries) { + createListItem(listWidget, entry.name, index); + index++; + } -static void updateList(std::shared_ptr& data) { - if (lvgl::lock(100 / portTICK_PERIOD_MS)) { - std::string filter = tt::string::lowercase(std::string(lv_textarea_get_text(data->filterTextareaWidget))); - readTimeZones(data, filter); - lvgl::unlock(); - } else { - TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL"); - return; - } - - if (lvgl::lock(100 / portTICK_PERIOD_MS)) { - if (data->mutex.lock(100 / portTICK_PERIOD_MS)) { - lv_obj_clean(data->listWidget); - - uint32_t index = 0; - for (auto& entry : data->entries) { - createListItem(data->listWidget, entry.name, index); - index++; + mutex.unlock(); } - data->mutex.unlock(); + lvgl::unlock(); } - - lvgl::unlock(); } -} -static void onShow(AppContext& app, lv_obj_t* parent) { - auto data = std::static_pointer_cast(app.getData()); +public: - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - lvgl::toolbar_create(parent, app); + void onShow(AppContext& app, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lvgl::toolbar_create(parent, app); - auto* search_wrapper = lv_obj_create(parent); - lv_obj_set_size(search_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_flex_flow(search_wrapper, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(search_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); - lv_obj_set_style_pad_all(search_wrapper, 0, 0); - lv_obj_set_style_border_width(search_wrapper, 0, 0); + auto* search_wrapper = lv_obj_create(parent); + lv_obj_set_size(search_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(search_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(search_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_all(search_wrapper, 0, 0); + lv_obj_set_style_border_width(search_wrapper, 0, 0); - auto* icon = lv_image_create(search_wrapper); - lv_obj_set_style_margin_left(icon, 8, 0); - lv_obj_set_style_image_recolor_opa(icon, 255, 0); - lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0); + auto* icon = lv_image_create(search_wrapper); + lv_obj_set_style_margin_left(icon, 8, 0); + lv_obj_set_style_image_recolor_opa(icon, 255, 0); + lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0); - std::string icon_path = app.getPaths()->getSystemPathLvgl("search.png"); - lv_image_set_src(icon, icon_path.c_str()); - lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0); + std::string icon_path = app.getPaths()->getSystemPathLvgl("search.png"); + lv_image_set_src(icon, icon_path.c_str()); + lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0); - auto* textarea = lv_textarea_create(search_wrapper); - lv_textarea_set_placeholder_text(textarea, "e.g. Europe/Amsterdam"); - lv_textarea_set_one_line(textarea, true); - lv_obj_add_event_cb(textarea, onTextareaValueChanged, LV_EVENT_VALUE_CHANGED, nullptr); - data->filterTextareaWidget = textarea; - lv_obj_set_flex_grow(textarea, 1); - service::gui::keyboardAddTextArea(textarea); + auto* textarea = lv_textarea_create(search_wrapper); + lv_textarea_set_placeholder_text(textarea, "e.g. Europe/Amsterdam"); + lv_textarea_set_one_line(textarea, true); + lv_obj_add_event_cb(textarea, onTextareaValueChangedCallback, LV_EVENT_VALUE_CHANGED, this); + filterTextareaWidget = textarea; + lv_obj_set_flex_grow(textarea, 1); + service::gui::keyboardAddTextArea(textarea); - auto* list = lv_list_create(parent); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_set_flex_grow(list, 1); - lv_obj_set_style_border_width(list, 0, 0); - data->listWidget = list; -} + auto* list = lv_list_create(parent); + lv_obj_set_width(list, LV_PCT(100)); + lv_obj_set_flex_grow(list, 1); + lv_obj_set_style_border_width(list, 0, 0); + listWidget = list; + } -static void onStart(AppContext& app) { - auto data = std::make_shared(); - data->updateTimer = std::make_unique(Timer::Type::Once, onUpdateTimer, data); - app.setData(data); -} + void onStart(AppContext& app) override { + updateTimer = std::make_unique(Timer::Type::Once, updateTimerCallback, nullptr); + } +}; extern const AppManifest manifest = { .id = "TimeZone", .name = "Select timezone", - .type = TypeHidden, - .onStart = onStart, - .onShow = onShow, + .type = Type::Hidden, + .createApp = create }; void start() { diff --git a/Tactility/Source/app/usbsettings/UsbSettings.cpp b/Tactility/Source/app/usbsettings/UsbSettings.cpp index fa59148c..4b760234 100644 --- a/Tactility/Source/app/usbsettings/UsbSettings.cpp +++ b/Tactility/Source/app/usbsettings/UsbSettings.cpp @@ -1,6 +1,9 @@ +#include "CoreDefines.h" +#include "app/App.h" +#include "app/AppManifest.h" +#include "hal/usb/Usb.h" #include "lvgl.h" #include "lvgl/Toolbar.h" -#include "hal/usb/Usb.h" #define TAG "usb_settings" @@ -10,36 +13,38 @@ static void onRebootMassStorage(TT_UNUSED lv_event_t* event) { hal::usb::rebootIntoMassStorageSdmmc(); } -static void onShow(AppContext& app, lv_obj_t* parent) { - auto* toolbar = lvgl::toolbar_create(parent, app); - lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); +class UsbSettingsApp : public App { - if (hal::usb::canRebootIntoMassStorageSdmmc()) { - auto* button = lv_button_create(parent); - auto* label = lv_label_create(button); - lv_label_set_text(label, "Reboot as USB storage"); - lv_obj_align(button, LV_ALIGN_CENTER, 0, 0); - lv_obj_add_event_cb(button, onRebootMassStorage, LV_EVENT_SHORT_CLICKED, nullptr); - } else { - bool supported = hal::usb::isSupported(); - const char* first = supported ? "USB storage not available:" : "USB driver not supported"; - const char* second = supported ? "SD card not mounted" : "on this hardware"; - auto* label_a = lv_label_create(parent); - lv_label_set_text(label_a, first); - lv_obj_align(label_a, LV_ALIGN_CENTER, 0, 0); - auto* label_b = lv_label_create(parent); - lv_label_set_text(label_b, second); - lv_obj_align_to(label_b, label_a, LV_ALIGN_OUT_BOTTOM_MID, 0, 4); + void onShow(AppContext& app, lv_obj_t* parent) override { + auto* toolbar = lvgl::toolbar_create(parent, app); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + if (hal::usb::canRebootIntoMassStorageSdmmc()) { + auto* button = lv_button_create(parent); + auto* label = lv_label_create(button); + lv_label_set_text(label, "Reboot as USB storage"); + lv_obj_align(button, LV_ALIGN_CENTER, 0, 0); + lv_obj_add_event_cb(button, onRebootMassStorage, LV_EVENT_SHORT_CLICKED, nullptr); + } else { + bool supported = hal::usb::isSupported(); + const char* first = supported ? "USB storage not available:" : "USB driver not supported"; + const char* second = supported ? "SD card not mounted" : "on this hardware"; + auto* label_a = lv_label_create(parent); + lv_label_set_text(label_a, first); + lv_obj_align(label_a, LV_ALIGN_CENTER, 0, 0); + auto* label_b = lv_label_create(parent); + lv_label_set_text(label_b, second); + lv_obj_align_to(label_b, label_a, LV_ALIGN_OUT_BOTTOM_MID, 0, 4); + } } - -} +}; extern const AppManifest manifest = { .id = "UsbSettings", .name = "USB", .icon = LV_SYMBOL_USB, - .type = TypeSettings, - .onShow = onShow + .type = Type::Settings, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index 960bbf29..920ca972 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -15,9 +15,9 @@ namespace tt::app::wifiapsettings { extern const AppManifest manifest; /** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ -const AppContext* _Nullable optWifiApSettingsApp() { - app::AppContext* app = service::loader::getCurrentApp(); - if (app->getManifest().id == manifest.id) { +const std::shared_ptr _Nullable optWifiApSettingsApp() { + auto app = service::loader::getCurrentAppContext(); + if (app != nullptr && app->getManifest().id == manifest.id) { return app; } else { return nullptr; @@ -41,7 +41,7 @@ static void onPressForget(TT_UNUSED lv_event_t* event) { static void onToggleAutoConnect(lv_event_t* event) { lv_event_code_t code = lv_event_get_code(event); - auto* app = optWifiApSettingsApp(); + auto app = optWifiApSettingsApp(); if (app == nullptr) { return; } @@ -66,96 +66,98 @@ static void onToggleAutoConnect(lv_event_t* event) { } } -static void onShow(AppContext& app, lv_obj_t* parent) { - auto paremeters = app.getParameters(); - tt_check(paremeters != nullptr, "Parameters missing"); - std::string ssid = paremeters->getString("ssid"); +class WifiApSettings : public App { - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - lvgl::toolbar_create(parent, ssid); + void onShow(AppContext& app, lv_obj_t* parent) override { + auto paremeters = app.getParameters(); + tt_check(paremeters != nullptr, "Parameters missing"); + std::string ssid = paremeters->getString("ssid"); - // Wrappers + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lvgl::toolbar_create(parent, ssid); - lv_obj_t* wrapper = lv_obj_create(parent); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_flex_grow(wrapper, 1); - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); - lvgl::obj_set_style_bg_invisible(wrapper); + // Wrappers - // Auto-connect toggle + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lvgl::obj_set_style_bg_invisible(wrapper); - lv_obj_t* auto_connect_wrapper = lv_obj_create(wrapper); - lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lvgl::obj_set_style_no_padding(auto_connect_wrapper); - lv_obj_set_style_border_width(auto_connect_wrapper, 0, 0); + // Auto-connect toggle - lv_obj_t* auto_connect_label = lv_label_create(auto_connect_wrapper); - lv_label_set_text(auto_connect_label, "Auto-connect"); - lv_obj_align(auto_connect_label, LV_ALIGN_TOP_LEFT, 0, 6); + lv_obj_t* auto_connect_wrapper = lv_obj_create(wrapper); + lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lvgl::obj_set_style_no_padding(auto_connect_wrapper); + lv_obj_set_style_border_width(auto_connect_wrapper, 0, 0); - lv_obj_t* auto_connect_switch = lv_switch_create(auto_connect_wrapper); - lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, (void*)&paremeters); - lv_obj_align(auto_connect_switch, LV_ALIGN_TOP_RIGHT, 0, 0); + lv_obj_t* auto_connect_label = lv_label_create(auto_connect_wrapper); + lv_label_set_text(auto_connect_label, "Auto-connect"); + lv_obj_align(auto_connect_label, LV_ALIGN_TOP_LEFT, 0, 6); - lv_obj_t* forget_button = lv_button_create(wrapper); - lv_obj_set_width(forget_button, LV_PCT(100)); - lv_obj_align_to(forget_button, auto_connect_wrapper, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); - lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, nullptr); - lv_obj_t* forget_button_label = lv_label_create(forget_button); - lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0); - lv_label_set_text(forget_button_label, "Forget"); + lv_obj_t* auto_connect_switch = lv_switch_create(auto_connect_wrapper); + lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, (void*)&paremeters); + lv_obj_align(auto_connect_switch, LV_ALIGN_TOP_RIGHT, 0, 0); - service::wifi::settings::WifiApSettings settings {}; - if (service::wifi::settings::load(ssid.c_str(), &settings)) { - if (settings.auto_connect) { - lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED); + lv_obj_t* forget_button = lv_button_create(wrapper); + lv_obj_set_width(forget_button, LV_PCT(100)); + lv_obj_align_to(forget_button, auto_connect_wrapper, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); + lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, nullptr); + lv_obj_t* forget_button_label = lv_label_create(forget_button); + lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0); + lv_label_set_text(forget_button_label, "Forget"); + + service::wifi::settings::WifiApSettings settings {}; + if (service::wifi::settings::load(ssid.c_str(), &settings)) { + if (settings.auto_connect) { + lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED); + } } else { - lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED); + TT_LOG_W(TAG, "No settings found"); + lv_obj_add_flag(forget_button, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(auto_connect_wrapper, LV_OBJ_FLAG_HIDDEN); } - } else { - TT_LOG_W(TAG, "No settings found"); - lv_obj_add_flag(forget_button, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(auto_connect_wrapper, LV_OBJ_FLAG_HIDDEN); } -} -void onResult(TT_UNUSED AppContext& app, TT_UNUSED Result result, const Bundle& bundle) { - auto index = alertdialog::getResultIndex(bundle); - if (index == 0) {// Yes - auto* app = optWifiApSettingsApp(); - if (app == nullptr) { - return; - } - - auto parameters = app->getParameters(); - tt_check(parameters != nullptr, "Parameters missing"); - - std::string ssid = parameters->getString("ssid"); - if (service::wifi::settings::remove(ssid.c_str())) { - TT_LOG_I(TAG, "Removed SSID"); - - if ( - service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive && - service::wifi::getConnectionTarget() == ssid - ) { - service::wifi::disconnect(); + void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED Result result, std::unique_ptr bundle) override { + auto index = alertdialog::getResultIndex(*bundle); + if (index == 0) { // Yes + auto app = optWifiApSettingsApp(); + if (app == nullptr) { + return; } - // Stop self - service::loader::stopApp(); - } else { - TT_LOG_E(TAG, "Failed to remove SSID"); + auto parameters = app->getParameters(); + tt_check(parameters != nullptr, "Parameters missing"); + + std::string ssid = parameters->getString("ssid"); + if (service::wifi::settings::remove(ssid.c_str())) { + TT_LOG_I(TAG, "Removed SSID"); + + if ( + service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive && + service::wifi::getConnectionTarget() == ssid + ) { + service::wifi::disconnect(); + } + + // Stop self + service::loader::stopApp(); + } else { + TT_LOG_E(TAG, "Failed to remove SSID"); + } } } -} +}; extern const AppManifest manifest = { .id = "WifiApSettings", .name = "Wi-Fi AP Settings", .icon = LV_SYMBOL_WIFI, - .type = TypeHidden, - .onShow = onShow, - .onResult = onResult + .type = Type::Hidden, + .createApp = create }; } // namespace diff --git a/Tactility/Source/app/wificonnect/WifiConnect.cpp b/Tactility/Source/app/wificonnect/WifiConnect.cpp index af8b9b62..f2587796 100644 --- a/Tactility/Source/app/wificonnect/WifiConnect.cpp +++ b/Tactility/Source/app/wificonnect/WifiConnect.cpp @@ -16,9 +16,9 @@ extern const AppManifest manifest; /** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ std::shared_ptr _Nullable optWifiConnect() { - app::AppContext* app = service::loader::getCurrentApp(); - if (app->getManifest().id == manifest.id) { - return std::static_pointer_cast(app->getData()); + auto appContext = service::loader::getCurrentAppContext(); + if (appContext != nullptr && appContext->getManifest().id == manifest.id) { + return std::static_pointer_cast(appContext->getApp()); } else { return nullptr; } @@ -105,30 +105,12 @@ void WifiConnect::onHide(TT_UNUSED AppContext& app) { unlock(); } -static void onShow(AppContext& app, lv_obj_t* parent) { - auto wifi = std::static_pointer_cast(app.getData()); - wifi->onShow(app, parent); -} - -static void onHide(AppContext& app) { - auto wifi = std::static_pointer_cast(app.getData()); - wifi->onHide(app); -} - -static void onStart(AppContext& app) { - auto wifi = std::make_shared(); - app.setData(wifi); -} - - extern const AppManifest manifest = { .id = "WifiConnect", .name = "Wi-Fi Connect", .icon = LV_SYMBOL_WIFI, - .type = TypeHidden, - .onStart = &onStart, - .onShow = &onShow, - .onHide = &onHide + .type = Type::Hidden, + .createApp = create }; void start(const std::string& ssid, const std::string& password) { diff --git a/Tactility/Source/app/wifimanage/WifiManage.cpp b/Tactility/Source/app/wifimanage/WifiManage.cpp index a6bade85..0966cb58 100644 --- a/Tactility/Source/app/wifimanage/WifiManage.cpp +++ b/Tactility/Source/app/wifimanage/WifiManage.cpp @@ -18,9 +18,9 @@ extern const AppManifest manifest; /** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ std::shared_ptr _Nullable optWifiManage() { - app::AppContext* app = service::loader::getCurrentApp(); - if (app->getManifest().id == manifest.id) { - return std::static_pointer_cast(app->getData()); + auto appContext = service::loader::getCurrentAppContext(); + if (appContext != nullptr && appContext->getManifest().id == manifest.id) { + return std::static_pointer_cast(appContext->getApp()); } else { return nullptr; } @@ -146,33 +146,12 @@ void WifiManage::onHide(TT_UNUSED AppContext& app) { unlock(); } -// region Manifest methods - -static void onStart(AppContext& app) { - auto wifi = std::make_shared(); - app.setData(wifi); -} - -static void onShow(AppContext& app, lv_obj_t* parent) { - auto wifi = std::static_pointer_cast(app.getData()); - wifi->onShow(app, parent); -} - -static void onHide(AppContext& app) { - auto wifi = std::static_pointer_cast(app.getData()); - wifi->onHide(app); -} - -// endregion - extern const AppManifest manifest = { .id = "WifiManage", .name = "Wi-Fi", .icon = LV_SYMBOL_WIFI, - .type = TypeSettings, - .onStart = onStart, - .onShow = onShow, - .onHide = onHide + .type = Type::Settings, + .createApp = create }; void start() { diff --git a/Tactility/Source/service/gui/Gui.cpp b/Tactility/Source/service/gui/Gui.cpp index 4e4c2905..82139fc8 100644 --- a/Tactility/Source/service/gui/Gui.cpp +++ b/Tactility/Source/service/gui/Gui.cpp @@ -19,9 +19,8 @@ Gui* gui = nullptr; void onLoaderMessage(const void* message, TT_UNUSED void* context) { auto* event = static_cast(message); if (event->type == loader::LoaderEventTypeApplicationShowing) { - app::AppContext& app = event->app_showing.app; - const app::AppManifest& app_manifest = app.getManifest(); - showApp(app, app_manifest.onShow, app_manifest.onHide); + auto app_instance = service::loader::getCurrentAppContext(); + showApp(app_instance); } else if (event->type == loader::LoaderEventTypeApplicationHiding) { hideApp(); } @@ -94,27 +93,25 @@ void requestDraw() { thread_flags_set(thread_id, GUI_THREAD_FLAG_DRAW); } -void showApp(app::AppContext& app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide) { +void showApp(std::shared_ptr app) { lock(); - tt_check(gui->appViewPort == nullptr); - gui->appViewPort = view_port_alloc(app, on_show, on_hide); + tt_check(gui->appToRender == nullptr); + gui->appToRender = std::move(app); unlock(); requestDraw(); } void hideApp() { lock(); - ViewPort* view_port = gui->appViewPort; - tt_check(view_port != nullptr); + tt_check(gui->appToRender != nullptr); // We must lock the LVGL port, because the viewport hide callbacks // might call LVGL APIs (e.g. to remove the keyboard from the screen root) tt_check(lvgl::lock(configTICK_RATE_HZ)); - view_port_hide(view_port); + gui->appToRender->getApp()->onHide(*gui->appToRender); lvgl::unlock(); - view_port_free(view_port); - gui->appViewPort = nullptr; + gui->appToRender = nullptr; unlock(); } diff --git a/Tactility/Source/service/gui/Gui.h b/Tactility/Source/service/gui/Gui.h index 54b38fb3..0650d7a6 100644 --- a/Tactility/Source/service/gui/Gui.h +++ b/Tactility/Source/service/gui/Gui.h @@ -1,7 +1,7 @@ #pragma once +#include "app/AppInstance.h" #include "app/AppContext.h" -#include "ViewPort.h" namespace tt::service::gui { @@ -10,10 +10,8 @@ typedef struct Gui Gui; /** * Set the app viewport in the gui state and request the gui to draw it. * @param[in] app - * @param[in] onShow - * @param[in] onHide */ -void showApp(app::AppContext& app, ViewPortShowCallback onShow, ViewPortHideCallback onHide); +void showApp(std::shared_ptr app); /** * Hide the current app's viewport. diff --git a/Tactility/Source/service/gui/GuiDraw.cpp b/Tactility/Source/service/gui/GuiDraw.cpp index d448d747..c5302ec8 100644 --- a/Tactility/Source/service/gui/GuiDraw.cpp +++ b/Tactility/Source/service/gui/GuiDraw.cpp @@ -9,9 +9,10 @@ namespace tt::service::gui { #define TAG "gui" -static lv_obj_t* createAppViews(Gui* gui, lv_obj_t* parent, app::AppContext& app) { +static lv_obj_t* createAppViews(Gui* gui, lv_obj_t* parent) { lv_obj_send_event(gui->statusbarWidget, LV_EVENT_DRAW_MAIN, nullptr); lv_obj_t* child_container = lv_obj_create(parent); + lv_obj_set_style_pad_all(child_container, 0, 0); lv_obj_set_width(child_container, LV_PCT(100)); lv_obj_set_flex_grow(child_container, 1); @@ -34,19 +35,17 @@ void redraw(Gui* gui) { if (lvgl::lock(1000)) { lv_obj_clean(gui->appRootWidget); - ViewPort* view_port = gui->appViewPort; - if (view_port != nullptr) { - app::AppContext& app = view_port->app; + if (gui->appToRender != nullptr) { - app::Flags flags = app.getFlags(); + app::Flags flags = std::static_pointer_cast(gui->appToRender)->getFlags(); if (flags.showStatusbar) { lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN); } - lv_obj_t* container = createAppViews(gui, gui->appRootWidget, app); - view_port_show(view_port, container); + lv_obj_t* container = createAppViews(gui, gui->appRootWidget); + gui->appToRender->getApp()->onShow(*gui->appToRender, container); } else { TT_LOG_W(TAG, "nothing to draw"); } diff --git a/Tactility/Source/service/gui/ViewPort.cpp b/Tactility/Source/service/gui/ViewPort.cpp deleted file mode 100644 index c4e424c6..00000000 --- a/Tactility/Source/service/gui/ViewPort.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "ViewPort.h" - -#include "Check.h" -#include "service/gui/ViewPort_i.h" -#include "lvgl/Style.h" - -namespace tt::service::gui { - -#define TAG "viewport" - -ViewPort* view_port_alloc( - app::AppContext& app, - ViewPortShowCallback on_show, - ViewPortHideCallback on_hide -) { - return new ViewPort( - app, - on_show, - on_hide - ); -} - -void view_port_free(ViewPort* view_port) { - tt_assert(view_port); - delete view_port; -} - -void view_port_show(ViewPort* view_port, lv_obj_t* parent) { - tt_assert(view_port); - tt_assert(parent); - if (view_port->onShow) { - lvgl::obj_set_style_no_padding(parent); - view_port->onShow(view_port->app, parent); - } -} - -void view_port_hide(ViewPort* view_port) { - tt_assert(view_port); - if (view_port->onHide) { - view_port->onHide(view_port->app); - } -} - -} // namespace diff --git a/Tactility/Source/service/gui/ViewPort.h b/Tactility/Source/service/gui/ViewPort.h deleted file mode 100644 index 7721e0ea..00000000 --- a/Tactility/Source/service/gui/ViewPort.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "app/AppContext.h" -#include "lvgl.h" - -namespace tt::service::gui { - -/** ViewPort Draw callback - * @warning called from GUI thread - */ -typedef void (*ViewPortShowCallback)(app::AppContext& app, lv_obj_t* parent); -typedef void (*ViewPortHideCallback)(app::AppContext& app); - -// TODO: Move internally, use handle publicly - -typedef struct ViewPort { - app::AppContext& app; - ViewPortShowCallback onShow; - ViewPortHideCallback _Nullable onHide; - - ViewPort( - app::AppContext& app, - ViewPortShowCallback on_show, - ViewPortHideCallback _Nullable on_hide - ) : app(app), onShow(on_show), onHide(on_hide) {} -} ViewPort; - -/** ViewPort allocator - * always returns view_port or stops system if not enough memory. - * @param app - * @param onShow Called to create LVGL widgets - * @param onHide Called before clearing the LVGL widget parent - * @return ViewPort instance - */ -ViewPort* view_port_alloc( - app::AppContext& app, - ViewPortShowCallback onShow, - ViewPortHideCallback onHide -); - -/** ViewPort destruction - * Ensure that view_port was unregistered in GUI system before use. - * @param viewPort ViewPort instance - */ -void view_port_free(ViewPort* viewPort); - -} // namespace diff --git a/Tactility/Source/service/loader/Loader.cpp b/Tactility/Source/service/loader/Loader.cpp index 439a5290..c962d0c0 100644 --- a/Tactility/Source/service/loader/Loader.cpp +++ b/Tactility/Source/service/loader/Loader.cpp @@ -6,8 +6,9 @@ #include "RtosCompat.h" #ifdef ESP_PLATFORM -#include "esp_heap_caps.h" #include "TactilityHeadless.h" +#include "app/ElfApp.h" +#include "esp_heap_caps.h" #else #include "lvgl/LvglSync.h" @@ -61,17 +62,22 @@ void stopApp() { loader_singleton->dispatcherThread->dispatch(onStopAppMessage, nullptr); } -app::AppContext* _Nullable getCurrentApp() { +std::shared_ptr _Nullable getCurrentAppContext() { tt_assert(loader_singleton); if (loader_singleton->mutex.lock(10 / portTICK_PERIOD_MS)) { - app::AppInstance* app = loader_singleton->appStack.top(); + auto app = loader_singleton->appStack.top(); loader_singleton->mutex.unlock(); - return dynamic_cast(app); + return std::move(app); } else { return nullptr; } } +std::shared_ptr _Nullable getCurrentApp() { + auto app_context = getCurrentAppContext(); + return app_context != nullptr ? app_context->getApp() : nullptr; +} + std::shared_ptr getPubsub() { tt_assert(loader_singleton); // it's safe to return pubsub without locking @@ -97,9 +103,9 @@ static const char* appStateToString(app::State state) { } } -static void transitionAppToState(app::AppInstance& app, app::State state) { - const app::AppManifest& manifest = app.getManifest(); - const app::State old_state = app.getState(); +static void transitionAppToState(std::shared_ptr app, app::State state) { + const app::AppManifest& manifest = app->getManifest(); + const app::State old_state = app->getState(); TT_LOG_I( TAG, @@ -111,49 +117,35 @@ static void transitionAppToState(app::AppInstance& app, app::State state) { switch (state) { case app::StateInitial: - app.setState(app::StateInitial); + app->setState(app::StateInitial); break; case app::StateStarted: - if (manifest.onStart != nullptr) { - manifest.onStart(app); - } - app.setState(app::StateStarted); + app->getApp()->onStart(*app); + app->setState(app::StateStarted); break; case app::StateShowing: { - LoaderEvent event_showing = { - .type = LoaderEventTypeApplicationShowing, - .app_showing = { - .app = app - } - }; + LoaderEvent event_showing = { .type = LoaderEventTypeApplicationShowing }; tt_pubsub_publish(loader_singleton->pubsubExternal, &event_showing); - app.setState(app::StateShowing); + app->setState(app::StateShowing); break; } case app::StateHiding: { - LoaderEvent event_hiding = { - .type = LoaderEventTypeApplicationHiding, - .app_hiding = { - .app = app - } - }; + LoaderEvent event_hiding = { .type = LoaderEventTypeApplicationHiding }; tt_pubsub_publish(loader_singleton->pubsubExternal, &event_hiding); - app.setState(app::StateHiding); + app->setState(app::StateHiding); break; } case app::StateStopped: - if (manifest.onStop) { - manifest.onStop(app); - } - app.setData(nullptr); - app.setState(app::StateStopped); + // TODO: Verify manifest + app->getApp()->onStop(*app); + app->setState(app::StateStopped); break; } } static LoaderStatus startAppWithManifestInternal( - const app::AppManifest* manifest, - std::shared_ptr _Nullable parameters + const std::shared_ptr& manifest, + const std::shared_ptr _Nullable& parameters ) { tt_check(loader_singleton != nullptr); @@ -165,29 +157,23 @@ static LoaderStatus startAppWithManifestInternal( } auto previous_app = !loader_singleton->appStack.empty() ? loader_singleton->appStack.top() : nullptr; - auto new_app = new app::AppInstance(*manifest, parameters); - new_app->mutableFlags().showStatusbar = (manifest->type != app::TypeBoot); + + auto new_app = std::make_shared(manifest, parameters); + + new_app->mutableFlags().showStatusbar = (manifest->type != app::Type::Boot); loader_singleton->appStack.push(new_app); - transitionAppToState(*new_app, app::StateInitial); - transitionAppToState(*new_app, app::StateStarted); + transitionAppToState(new_app, app::StateInitial); + transitionAppToState(new_app, app::StateStarted); // We might have to hide the previous app first if (previous_app != nullptr) { - transitionAppToState(*previous_app, app::StateHiding); + transitionAppToState(previous_app, app::StateHiding); } - transitionAppToState(*new_app, app::StateShowing); + transitionAppToState(new_app, app::StateShowing); - LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStarted}; - tt_pubsub_publish(loader_singleton->pubsubInternal, &event_internal); - - LoaderEvent event_external = { - .type = LoaderEventTypeApplicationStarted, - .app_started = { - .app = *new_app - } - }; + LoaderEvent event_external = { .type = LoaderEventTypeApplicationStarted }; tt_pubsub_publish(loader_singleton->pubsubExternal, &event_external); return LoaderStatus::Ok; @@ -208,7 +194,7 @@ static LoaderStatus startAppInternal( ) { TT_LOG_I(TAG, "Start by id %s", id.c_str()); - const app::AppManifest* manifest = app::findAppById(id); + auto manifest = app::findAppById(id); if (manifest == nullptr) { TT_LOG_E(TAG, "App not found: %s", id.c_str()); return LoaderStatus::ErrorUnknownApp; @@ -233,75 +219,75 @@ static void stopAppInternal() { } // Stop current app - app::AppInstance* app_to_stop = loader_singleton->appStack.top(); + auto app_to_stop = loader_singleton->appStack.top(); - if (original_stack_size == 1 && app_to_stop->getManifest().type != app::TypeBoot) { + if (original_stack_size == 1 && app_to_stop->getManifest().type != app::Type::Boot) { TT_LOG_E(TAG, "Stop app: can't stop root app"); return; } - auto result_holder = std::move(app_to_stop->getResult()); + bool result_set = false; + app::Result result; + std::unique_ptr result_bundle; + if (app_to_stop->getApp()->moveResult(result, result_bundle)) { + result_set = true; + } - const app::AppManifest& manifest = app_to_stop->getManifest(); - transitionAppToState(*app_to_stop, app::StateHiding); - transitionAppToState(*app_to_stop, app::StateStopped); + transitionAppToState(app_to_stop, app::StateHiding); + transitionAppToState(app_to_stop, app::StateStopped); loader_singleton->appStack.pop(); - delete app_to_stop; + + // We only expect the app to be referenced within the current scope + if (app_to_stop.use_count() > 1) { + TT_LOG_W(TAG, "Memory leak: Stopped %s, but use count is %ld", app_to_stop->getManifest().id.c_str(), app_to_stop.use_count() - 1); + } + + // Refcount is expected to be 2: 1 within app_to_stop and 1 within the current scope + if (app_to_stop->getApp().use_count() > 2) { + TT_LOG_W(TAG, "Memory leak: Stopped %s, but use count is %ld", app_to_stop->getManifest().id.c_str(), app_to_stop->getApp().use_count() - 2); + } #ifdef ESP_PLATFORM TT_LOG_I(TAG, "Free heap: %zu", heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); #endif - app::AppOnResult on_result = nullptr; - app::AppInstance* app_to_resume = nullptr; + std::shared_ptr instance_to_resume; // If there's a previous app, resume it if (!loader_singleton->appStack.empty()) { - app_to_resume = loader_singleton->appStack.top(); - tt_assert(app_to_resume); - transitionAppToState(*app_to_resume, app::StateShowing); - - on_result = app_to_resume->getManifest().onResult; + instance_to_resume = loader_singleton->appStack.top(); + tt_assert(instance_to_resume); + transitionAppToState(instance_to_resume, app::StateShowing); } // Unlock so that we can send results to app and they can also start/stop new apps while processing these results scoped_lock->unlock(); // WARNING: After this point we cannot change the app states from this method directly anymore as we don't have a lock! - LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStopped}; - tt_pubsub_publish(loader_singleton->pubsubInternal, &event_internal); - - LoaderEvent event_external = { - .type = LoaderEventTypeApplicationStopped, - .app_stopped = { - .manifest = manifest - } - }; + LoaderEvent event_external = { .type = LoaderEventTypeApplicationStopped }; tt_pubsub_publish(loader_singleton->pubsubExternal, &event_external); - if (on_result != nullptr && app_to_resume != nullptr) { - if (result_holder != nullptr) { - auto result_bundle = result_holder->resultData.get(); + if (instance_to_resume != nullptr) { + if (result_set) { if (result_bundle != nullptr) { - on_result( - *app_to_resume, - result_holder->result, - *result_bundle + instance_to_resume->getApp()->onResult( + *instance_to_resume, + result, + std::move(result_bundle) ); } else { - const Bundle empty_bundle; - on_result( - *app_to_resume, - result_holder->result, - empty_bundle + instance_to_resume->getApp()->onResult( + *instance_to_resume, + result, + nullptr ); } } else { const Bundle empty_bundle; - on_result( - *app_to_resume, - app::ResultCancelled, - empty_bundle + instance_to_resume->getApp()->onResult( + *instance_to_resume, + app::Result::Cancelled, + nullptr ); } } diff --git a/Tactility/Source/service/loader/Loader.h b/Tactility/Source/service/loader/Loader.h index e770d058..d74387b3 100644 --- a/Tactility/Source/service/loader/Loader.h +++ b/Tactility/Source/service/loader/Loader.h @@ -21,8 +21,11 @@ void startApp(const std::string& id, const std::shared_ptr& _Nulla /** @brief Stop the currently showing app. Show the previous app if any app was still running. */ void stopApp(); +/** @return the currently running app context (it is only ever null before the splash screen is shown) */ +std::shared_ptr _Nullable getCurrentAppContext(); + /** @return the currently running app (it is only ever null before the splash screen is shown) */ -app::AppContext* _Nullable getCurrentApp(); +std::shared_ptr _Nullable getCurrentApp(); /** * @brief PubSub for LoaderEvent diff --git a/Tactility/Source/service/screenshot/ScreenshotTask.cpp b/Tactility/Source/service/screenshot/ScreenshotTask.cpp index d3e40d10..4258e8e1 100644 --- a/Tactility/Source/service/screenshot/ScreenshotTask.cpp +++ b/Tactility/Source/service/screenshot/ScreenshotTask.cpp @@ -87,9 +87,9 @@ void ScreenshotTask::taskMain() { } } } else if (work.type == TASK_WORK_TYPE_APPS) { - app::AppContext* _Nullable app = loader::getCurrentApp(); - if (app) { - const app::AppManifest& manifest = app->getManifest(); + auto appContext = loader::getCurrentAppContext(); + if (appContext != nullptr) { + const app::AppManifest& manifest = appContext->getManifest(); if (manifest.id != last_app_id) { kernel::delayMillis(100); last_app_id = manifest.id; diff --git a/TactilityC/Source/tt_app_context.cpp b/TactilityC/Source/tt_app_context.cpp index 01bb1961..16af1c27 100644 --- a/TactilityC/Source/tt_app_context.cpp +++ b/TactilityC/Source/tt_app_context.cpp @@ -1,4 +1,5 @@ #include "tt_app_context.h" +#include #include struct AppContextDataWrapper { @@ -9,28 +10,17 @@ extern "C" { #define HANDLE_AS_APP_CONTEXT(handle) ((tt::app::AppContext*)(handle)) -void* _Nullable tt_app_context_get_data(AppContextHandle handle) { - auto wrapper = std::reinterpret_pointer_cast(HANDLE_AS_APP_CONTEXT(handle)->getData()); - return wrapper ? wrapper->data : nullptr; -} - -void tt_app_context_set_data(AppContextHandle handle, void* _Nullable data) { - auto wrapper = std::make_shared(); - wrapper->data = data; - HANDLE_AS_APP_CONTEXT(handle)->setData(std::move(wrapper)); -} - BundleHandle _Nullable tt_app_context_get_parameters(AppContextHandle handle) { return (BundleHandle)HANDLE_AS_APP_CONTEXT(handle)->getParameters().get(); } void tt_app_context_set_result(AppContextHandle handle, Result result, BundleHandle _Nullable bundle) { - auto shared_bundle = std::shared_ptr((tt::Bundle*)bundle); - HANDLE_AS_APP_CONTEXT(handle)->setResult((tt::app::Result)result, std::move(shared_bundle)); + auto shared_bundle = std::unique_ptr((tt::Bundle*)bundle); + HANDLE_AS_APP_CONTEXT(handle)->getApp()->setResult((tt::app::Result)result, std::move(shared_bundle)); } bool tt_app_context_has_result(AppContextHandle handle) { - return HANDLE_AS_APP_CONTEXT(handle)->hasResult(); + return HANDLE_AS_APP_CONTEXT(handle)->getApp()->hasResult(); } } \ No newline at end of file diff --git a/TactilityC/Source/tt_app_context.h b/TactilityC/Source/tt_app_context.h index 32311c0e..9a9f0284 100644 --- a/TactilityC/Source/tt_app_context.h +++ b/TactilityC/Source/tt_app_context.h @@ -11,14 +11,6 @@ typedef void* AppContextHandle; /** @return the data that was attached to this app context */ void* _Nullable tt_app_context_get_data(AppContextHandle handle); -/** - * Attach data to an application context. - * Don't forget to manually delete allocated memory when onStopped() is called. - * @param[in] handle the app context handle - * @param[in] data the data to attach - */ -void tt_app_context_set_data(AppContextHandle handle, void* _Nullable data); - /** @return the bundle that belongs to this application, or null */ BundleHandle _Nullable tt_app_context_get_parameters(AppContextHandle handle); diff --git a/TactilityC/Source/tt_app_manifest.cpp b/TactilityC/Source/tt_app_manifest.cpp index 912b27dc..96fadbdf 100644 --- a/TactilityC/Source/tt_app_manifest.cpp +++ b/TactilityC/Source/tt_app_manifest.cpp @@ -1,105 +1,29 @@ #include "tt_app_manifest.h" #include -#include #include +#include #define TAG "tt_app" -AppOnStart elfOnStart = nullptr; -AppOnStop elfOnStop = nullptr; -AppOnShow elfOnShow = nullptr; -AppOnHide elfOnHide = nullptr; -AppOnResult elfOnResult = nullptr; - -static void onStartWrapper(tt::app::AppContext& context) { - if (elfOnStart != nullptr) { - TT_LOG_I(TAG, "onStartWrapper"); - elfOnStart(&context); - } else { - TT_LOG_W(TAG, "onStartWrapper not set"); - } -} - -static void onStopWrapper(tt::app::AppContext& context) { - if (elfOnStop != nullptr) { - TT_LOG_I(TAG, "onStopWrapper"); - elfOnStop(&context); - } else { - TT_LOG_W(TAG, "onStopWrapper not set"); - } -} - -static void onShowWrapper(tt::app::AppContext& context, lv_obj_t* parent) { - if (elfOnShow != nullptr) { - TT_LOG_I(TAG, "onShowWrapper"); - elfOnShow(&context, parent); - } else { - TT_LOG_W(TAG, "onShowWrapper not set"); - } -} - -static void onHideWrapper(tt::app::AppContext& context) { - if (elfOnHide != nullptr) { - TT_LOG_I(TAG, "onHideWrapper"); - elfOnHide(&context); - } else { - TT_LOG_W(TAG, "onHideWrapper not set"); - } -} - -static void onResultWrapper(tt::app::AppContext& context, tt::app::Result result, const tt::Bundle& resultData) { - if (elfOnResult != nullptr) { - TT_LOG_I(TAG, "onResultWrapper"); - Result convertedResult = AppResultError; - switch (result) { - case tt::app::ResultOk: - convertedResult = AppResultOk; - break; - case tt::app::ResultCancelled: - convertedResult = AppResultCancelled; - break; - case tt::app::ResultError: - convertedResult = AppResultError; - break; - } - elfOnResult(&context, convertedResult, (BundleHandle)&resultData); - } else { - TT_LOG_W(TAG, "onResultWrapper not set"); - } -} - -tt::app::AppManifest manifest = { - .id = "ElfWrapperInTactilityC", - .name = "", - .icon = "", - .onStart = onStartWrapper, - .onStop = onStopWrapper, - .onShow = onShowWrapper, - .onHide = onHideWrapper, - .onResult = onResultWrapper -}; - extern "C" { -void tt_set_app_manifest( - const char* name, - const char* _Nullable icon, - AppOnStart onStart, - AppOnStop _Nullable onStop, - AppOnShow _Nullable onShow, - AppOnHide _Nullable onHide, - AppOnResult _Nullable onResult +void tt_app_register( + const ExternalAppManifest* manifest ) { #ifdef ESP_PLATFORM - manifest.name = name; - manifest.icon = icon ? icon : ""; - elfOnStart = onStart; - elfOnStop = onStop; - elfOnShow = onShow; - elfOnHide = onHide; - elfOnResult = onResult; - tt::app::setElfAppManifest(manifest); + tt_assert((manifest->createData == nullptr) == (manifest->destroyData == nullptr)); + tt::app::setElfAppManifest( + manifest->name, + manifest->icon, + (tt::app::CreateData)manifest->createData, + (tt::app::DestroyData)manifest->destroyData, + (tt::app::OnStart)manifest->onStart, + (tt::app::OnStop)manifest->onStop, + (tt::app::OnShow)manifest->onShow, + (tt::app::OnHide)manifest->onHide, + (tt::app::OnResult)manifest->onResult + ); #else tt_crash("TactilityC is intended for PC/Simulator"); #endif diff --git a/TactilityC/Source/tt_app_manifest.h b/TactilityC/Source/tt_app_manifest.h index 9156bb27..959e5e2b 100644 --- a/TactilityC/Source/tt_app_manifest.h +++ b/TactilityC/Source/tt_app_manifest.h @@ -7,39 +7,47 @@ extern "C" { #endif +/** Important: These values must map to tt::app::Result values exactly */ typedef enum { - AppResultOk, - AppResultCancelled, - AppResultError + AppResultOk = 0, + AppResultCancelled = 1, + AppResultError = 2 } Result; typedef void* AppContextHandle; -typedef void (*AppOnStart)(AppContextHandle app); -typedef void (*AppOnStop)(AppContextHandle app); -typedef void (*AppOnShow)(AppContextHandle app, lv_obj_t* parent); -typedef void (*AppOnHide)(AppContextHandle app); -typedef void (*AppOnResult)(AppContextHandle app, Result result, BundleHandle resultData); +/** Important: These function types must map to t::app types exactly */ +typedef void* (*AppCreateData)(); +typedef void (*AppDestroyData)(void* data); +typedef void (*AppOnStart)(AppContextHandle app, void* _Nullable data); +typedef void (*AppOnStop)(AppContextHandle app, void* _Nullable data); +typedef void (*AppOnShow)(AppContextHandle app, void* _Nullable data, lv_obj_t* parent); +typedef void (*AppOnHide)(AppContextHandle app, void* _Nullable data); +typedef void (*AppOnResult)(AppContextHandle app, void* _Nullable data, Result result, BundleHandle resultData); -/** - * This is used to register the manifest of an external app. - * @param[in] name the application's human-readable name - * @param[in] icon the optional application icon (you can use LV_SYMBOL_* too) - * @param[in] onStart called when the app is launched (started) - * @param[in] onStop called when the app is exited (stopped) - * @param[in] onShow called when the app is about to be shown to the user (app becomes visible) - * @param[in] onHide called when the app is about to be invisible to the user (e.g. other app was launched by this app, and this app goes to the background) - * @param[in] onResult called when the app receives a result after launching another app - */ -void tt_set_app_manifest( - const char* name, - const char* _Nullable icon, - AppOnStart onStart, - AppOnStop _Nullable onStop, - AppOnShow _Nullable onShow, - AppOnHide _Nullable onHide, - AppOnResult _Nullable onResult -); +typedef struct { + /** The application's human-readable name */ + const char* name; + /** The application icon (you can use LV_SYMBOL_* too) */ + const char* _Nullable icon; + /** The application can allocate data to re-use later (e.g. struct with state) */ + AppCreateData _Nullable createData; + /** If createData is specified, this one must be specified too */ + AppDestroyData _Nullable destroyData; + /** Called when the app is launched (started) */ + AppOnStart _Nullable onStart; + /** Called when the app is exited (stopped) */ + AppOnStop _Nullable onStop; + /** Called when the app is about to be shown to the user (app becomes visible) */ + AppOnShow _Nullable onShow; + /** Called when the app is about to be invisible to the user (e.g. other app was launched by this app, and this app goes to the background) */ + AppOnHide _Nullable onHide; + /** Called when the app receives a result after launching another app */ + AppOnResult _Nullable onResult; +} ExternalAppManifest; + +/** This is used to register the manifest of an external app. */ +void tt_app_register(const ExternalAppManifest* manifest); #ifdef __cplusplus } diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index 5a7828da..efed1a5b 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -22,8 +22,7 @@ extern "C" { const struct esp_elfsym elf_symbols[] { // Tactility - ESP_ELFSYM_EXPORT(tt_app_context_get_data), - ESP_ELFSYM_EXPORT(tt_app_context_set_data), + ESP_ELFSYM_EXPORT(tt_app_register), ESP_ELFSYM_EXPORT(tt_app_context_get_parameters), ESP_ELFSYM_EXPORT(tt_app_context_set_result), ESP_ELFSYM_EXPORT(tt_app_context_has_result), @@ -39,7 +38,6 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(tt_bundle_put_bool), ESP_ELFSYM_EXPORT(tt_bundle_put_int32), ESP_ELFSYM_EXPORT(tt_bundle_put_string), - ESP_ELFSYM_EXPORT(tt_set_app_manifest), ESP_ELFSYM_EXPORT(tt_hal_i2c_start), ESP_ELFSYM_EXPORT(tt_hal_i2c_stop), ESP_ELFSYM_EXPORT(tt_hal_i2c_is_started), diff --git a/TactilityC/Source/tt_service_loader.cpp b/TactilityC/Source/tt_service_loader.cpp index 801765be..f3da55fc 100644 --- a/TactilityC/Source/tt_service_loader.cpp +++ b/TactilityC/Source/tt_service_loader.cpp @@ -14,7 +14,7 @@ void tt_service_loader_stop_app() { } AppContextHandle tt_service_loader_get_current_app() { - return tt::service::loader::getCurrentApp(); + return tt::service::loader::getCurrentAppContext().get(); } } diff --git a/TactilityCore/Source/Log.h b/TactilityCore/Source/Log.h index c6428d5b..19062ba3 100644 --- a/TactilityCore/Source/Log.h +++ b/TactilityCore/Source/Log.h @@ -2,7 +2,14 @@ #include "LogMessages.h" -#if CONFIG_SPIRAM_USE_MALLOC == 1 or not defined(ESP_PLATFORM) +#ifdef ESP_TARGET +#include +#else +#include +#include +#endif + +#if not defined(ESP_PLATFORM) or (defined(CONFIG_SPIRAM_USE_MALLOC) && CONFIG_SPIRAM_USE_MALLOC == 1) #define TT_LOG_ENTRY_COUNT 200 #define TT_LOG_MESSAGE_SIZE 128 #else @@ -35,12 +42,6 @@ LogEntry* copyLogEntries(unsigned int& outIndex); } // namespace tt -#ifdef ESP_TARGET -#include "esp_log.h" -#else -#include -#include -#endif #ifdef ESP_TARGET diff --git a/TactilityCore/Source/StringUtils.cpp b/TactilityCore/Source/StringUtils.cpp index c8168271..e5db7d0f 100644 --- a/TactilityCore/Source/StringUtils.cpp +++ b/TactilityCore/Source/StringUtils.cpp @@ -78,4 +78,13 @@ std::string join(const std::vector& input, const std::string& delim return stream.str(); } +std::string removeFileExtension(const std::string& input) { + auto index = input.find('.'); + if (index != std::string::npos) { + return input.substr(0, index); + } else { + return input; + } +} + } // namespace diff --git a/TactilityCore/Source/StringUtils.h b/TactilityCore/Source/StringUtils.h index a11be649..7c229698 100644 --- a/TactilityCore/Source/StringUtils.h +++ b/TactilityCore/Source/StringUtils.h @@ -66,5 +66,9 @@ std::basic_string lowercase(const std::basic_string& input) { return std::move(output); } +/** + * @return the first part of a file name right up (and excluding) the first period character. + */ +std::string removeFileExtension(const std::string& input); } // namespace diff --git a/TactilityCore/Source/Timer.h b/TactilityCore/Source/Timer.h index 3b2baf16..e1429a47 100644 --- a/TactilityCore/Source/Timer.h +++ b/TactilityCore/Source/Timer.h @@ -34,7 +34,7 @@ public: * @param[in] callback The callback function * @param callbackContext The callback context */ - Timer(Type type, Callback callback, std::shared_ptr callbackContext); + Timer(Type type, Callback callback, std::shared_ptr callbackContext = nullptr); ~Timer(); diff --git a/TactilityHeadless/Source/service/sdcard/Sdcard.cpp b/TactilityHeadless/Source/service/sdcard/Sdcard.cpp index 8e16235d..c61abba1 100644 --- a/TactilityHeadless/Source/service/sdcard/Sdcard.cpp +++ b/TactilityHeadless/Source/service/sdcard/Sdcard.cpp @@ -2,12 +2,9 @@ #include "Timer.h" #include "service/ServiceContext.h" -#include "TactilityCore.h" #include "TactilityHeadless.h" #include "service/ServiceRegistry.h" -#include - #define TAG "sdcard_service" namespace tt::service::sdcard { @@ -28,7 +25,6 @@ struct ServiceData { } }; - static void onUpdate(std::shared_ptr context) { auto sdcard = tt::hal::getConfiguration()->sdcard; if (sdcard == nullptr) {