Refactor app launching (#174)

- Refactor the way apps work: Instead of a C interface, they are now C++ classes. The main reasoning is that attaching data to an app was cumbersome. Having different implementations for different kinds of apps was cumbersome too. (3 or 4 layers of manifest nesting for the TactilityC project)
- External apps are still written in C, but they get a createData/destroyData in their manifest, so:
- External apps now have their own manifest.
- All functions in the original AppManifest are removed and replaced by a single `createApp` function
- External apps now automatically register (each app individually!) when they run the first time. As a side-effect they become visible in the `AppList` app!
- Adapted all apps for the new interface.
- Adapted all internal logic for these changes (Gui, ViewPort, Loader, AppContext, AppInstance, etc.)
- Rewrote parts of Loader to use std::shared_ptr to make the code much safer.
- Added a refcount check for the `AppInstance` and `App` at the end of their lifecycle. Show warning if refcount is too high.
This commit is contained in:
Ken Van Hoeylandt 2025-01-21 17:48:32 +01:00 committed by GitHub
parent 2bbd44a8b5
commit c3bcf93698
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 2561 additions and 2581 deletions

View File

@ -1,17 +1,23 @@
#include "app/AppManifest.h"
#include "lvgl.h" #include "lvgl.h"
#include "lvgl/Toolbar.h" #include "lvgl/Toolbar.h"
static void onShow(tt::app::AppContext& context, lv_obj_t* parent) { using namespace tt::app;
class HelloWorldApp : public App {
void onShow(AppContext& context, lv_obj_t* parent) override {
lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context); lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* label = lv_label_create(parent); lv_obj_t* label = lv_label_create(parent);
lv_label_set_text(label, "Hello, world!"); lv_label_set_text(label, "Hello, world!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
} }
};
extern const tt::app::AppManifest hello_world_app = { extern const AppManifest hello_world_app = {
.id = "HelloWorld", .id = "HelloWorld",
.name = "Hello World", .name = "Hello World",
.onShow = onShow, .createApp = create<HelloWorldApp>
}; };

View File

@ -3,12 +3,15 @@
#define TAG "bq24295" #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 { namespace registers {
static const uint8_t WATCHDOG = 0x05U; // Charge end/timer cntrl static const uint8_t WATCHDOG = 0x05U; // Datasheet page 35: Charge end/timer cntrl
static const uint8_t OPERATION_CONTROL = 0x07U; // Misc operation control static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control
static const uint8_t STATUS = 0x08U; // System status static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status
static const uint8_t VERSION = 0x0AU; // Vendor/part/revision status static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status
} // namespace registers } // namespace registers
// region Watchdog // region Watchdog

View File

@ -8,4 +8,4 @@ Please open an [Issue](https://github.com/ByteWelder/Tactility/issues/new) on Gi
# Code Style # Code Style
See [this document](CODING_STYLE.md). See [this document](CODING_STYLE.md) and [.clang-format](.clang-format).

View File

@ -9,33 +9,33 @@
- Add statusbar icon for memory pressure. - 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) - 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. - 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) - 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 - 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. - 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 - 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 - 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 - 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 # 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. - 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 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 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<const Bundle> parameters) {
- Boot hooks instead of a single boot method in config. Define different boot phases/levels in enum. - 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 - Add toggle to Display app for sysmon overlay: https://docs.lvgl.io/master/API/others/sysmon/index.html
- CrashHandler: use "corrupted" flag - CrashHandler: use "corrupted" flag
- CrashHandler: process other types of crashes (WDT?) - CrashHandler: process other types of crashes (WDT?)
- Call tt::lvgl::isSyncSet after HAL init and show error (and crash?) when it is not set. - 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) - 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: 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) - 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) - 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 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. - 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 - Support hot-plugging SD card
# Nice-to-haves # 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 player app
- Audio recording app - Audio recording app
- OTA updates - 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) - On crash, try to save current log to flash or SD card? (this is risky, though, so ask in Discord first)
# App Ideas # 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. - USB implementation to make device act as mass storage device.
- System logger - System logger
- BlueTooth keyboard app - BlueTooth keyboard app

View File

@ -5,7 +5,7 @@
* Note: LVGL and Tactility methods need to be exposed manually from TactilityC/Source/tt_init.cpp * 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) * 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_t* toolbar = tt_lvgl_toolbar_create(parent, context);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); 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); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
} }
ExternalAppManifest manifest = {
.name = "Hello World",
.onShow = onShow
};
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
tt_set_app_manifest( tt_app_register(&manifest);
"Hello World",
NULL,
NULL,
NULL,
onShow,
NULL,
NULL
);
return 0; return 0;
} }

View File

@ -1,9 +1,10 @@
#pragma once #pragma once
#include "app/AppContext.h"
#include "app/AppManifest.h"
#include "Bundle.h" #include "Bundle.h"
#include "Mutex.h" #include "Mutex.h"
#include "app/AppContext.h"
#include "app/AppManifest.h"
#include "app/ElfApp.h"
#include <memory> #include <memory>
#include <utility> #include <utility>
@ -17,19 +18,6 @@ typedef enum {
StateStopped // App is not in memory StateStopped // App is not in memory
} State; } State;
struct ResultHolder {
Result result;
std::shared_ptr<const Bundle> resultData;
explicit ResultHolder(Result result) : result(result), resultData(nullptr) {}
ResultHolder(Result result, std::shared_ptr<const Bundle> resultData) :
result(result),
resultData(std::move(resultData))
{}
};
/** /**
* Thread-safe app instance. * Thread-safe app instance.
*/ */
@ -38,7 +26,7 @@ class AppInstance : public AppContext {
private: private:
Mutex mutex = Mutex(Mutex::Type::Normal); Mutex mutex = Mutex(Mutex::Type::Normal);
const AppManifest& manifest; const std::shared_ptr<AppManifest> manifest;
State state = StateInitial; State state = StateInitial;
Flags flags = { .showStatusbar = true }; Flags flags = { .showStatusbar = true };
/** @brief Optional parameters to start the app with /** @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. * These manifest methods can optionally allocate/free data that is attached here.
*/ */
std::shared_ptr<void> _Nullable data; std::shared_ptr<void> _Nullable data;
std::unique_ptr<ResultHolder> _Nullable resultHolder;
std::shared_ptr<App> app;
static std::shared_ptr<app::App> createApp(
const std::shared_ptr<app::AppManifest>& 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: public:
explicit AppInstance(const AppManifest& manifest) : explicit AppInstance(const std::shared_ptr<AppManifest>& manifest) :
manifest(manifest) {}
AppInstance(const AppManifest& manifest, std::shared_ptr<const Bundle> parameters) :
manifest(manifest), manifest(manifest),
parameters(std::move(parameters)) {} app(createApp(manifest))
{}
AppInstance(const std::shared_ptr<AppManifest>& manifest, std::shared_ptr<const Bundle> parameters) :
manifest(manifest),
parameters(std::move(parameters)),
app(createApp(manifest)) {}
~AppInstance() override = default; ~AppInstance() override = default;
@ -70,22 +82,15 @@ public:
const AppManifest& getManifest() const override; const AppManifest& getManifest() const override;
Flags getFlags() const override; Flags getFlags() const;
void setFlags(Flags flags); void setFlags(Flags flags);
Flags& mutableFlags() { return flags; } // TODO: locking mechanism Flags& mutableFlags() { return flags; } // TODO: locking mechanism
std::shared_ptr<void> _Nullable getData() const override;
void setData(std::shared_ptr<void> data) override;
std::shared_ptr<const Bundle> getParameters() const override; std::shared_ptr<const Bundle> getParameters() const override;
void setResult(Result result) override;
void setResult(Result result, std::shared_ptr<const Bundle> bundle) override;
bool hasResult() const override;
std::unique_ptr<Paths> getPaths() const override; std::unique_ptr<Paths> getPaths() const override;
std::unique_ptr<ResultHolder>& getResult() { return resultHolder; } std::shared_ptr<App> getApp() const override { return app; }
}; };
} // namespace } // namespace

View File

@ -1,34 +0,0 @@
#pragma once
#include "./View.h"
#include "./State.h"
#include "app/AppManifest.h"
#include <lvgl.h>
#include <dirent.h>
#include <memory>
namespace tt::app::files {
class Files {
std::unique_ptr<View> view;
std::shared_ptr<State> state;
public:
Files() {
state = std::make_shared<State>();
view = std::make_unique<View>(state);
}
void onShow(lv_obj_t* parent) {
view->init(parent);
}
void onResult(Result result, const Bundle& bundle) {
view->onResult(result, bundle);
}
};
} // namespace

View File

@ -35,7 +35,7 @@ public:
void onRenamePressed(); void onRenamePressed();
void onDeletePressed(); void onDeletePressed();
void onDirEntryListScrollBegin(); void onDirEntryListScrollBegin();
void onResult(Result result, const Bundle& bundle); void onResult(Result result, std::unique_ptr<Bundle> bundle);
}; };
} }

View File

@ -19,17 +19,7 @@ enum ScanState {
}; };
struct Data { struct Data {
// Core
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::unique_ptr<Timer> scanTimer = nullptr;
// State
ScanState scanState;
i2c_port_t port = I2C_NUM_0;
std::vector<uint8_t> scannedAddresses;
// Widgets
lv_obj_t* scanButtonLabelWidget = nullptr;
lv_obj_t* portDropdownWidget = nullptr;
lv_obj_t* scanListWidget = nullptr;
}; };
void onScanTimerFinished(std::shared_ptr<Data> data); void onScanTimerFinished(std::shared_ptr<Data> data);

View File

@ -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<Timer> 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

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "app/App.h"
#include "app/wificonnect/Bindings.h" #include "app/wificonnect/Bindings.h"
#include "app/wificonnect/State.h" #include "app/wificonnect/State.h"
#include "app/wificonnect/View.h" #include "app/wificonnect/View.h"
@ -9,7 +10,10 @@
namespace tt::app::wificonnect { namespace tt::app::wificonnect {
class WifiConnect { class WifiConnect : public App {
private:
Mutex mutex; Mutex mutex;
State state; State state;
Bindings bindings = { Bindings bindings = {
@ -28,8 +32,8 @@ public:
void lock(); void lock();
void unlock(); void unlock();
void onShow(AppContext& app, lv_obj_t* parent); void onShow(AppContext& app, lv_obj_t* parent) override;
void onHide(AppContext& app); void onHide(AppContext& app) override;
State& getState() { return state; } State& getState() { return state; }
Bindings& getBindings() { return bindings; } Bindings& getBindings() { return bindings; }

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "app/App.h"
#include "Mutex.h" #include "Mutex.h"
#include "./View.h" #include "./View.h"
#include "./State.h" #include "./State.h"
@ -7,7 +8,9 @@
namespace tt::app::wifimanage { namespace tt::app::wifimanage {
class WifiManage { class WifiManage : public App {
private:
PubSubSubscription* wifiSubscription = nullptr; PubSubSubscription* wifiSubscription = nullptr;
Mutex mutex; Mutex mutex;
@ -23,8 +26,8 @@ public:
void lock(); void lock();
void unlock(); void unlock();
void onShow(AppContext& app, lv_obj_t* parent); void onShow(AppContext& app, lv_obj_t* parent) override;
void onHide(AppContext& app); void onHide(AppContext& app) override;
Bindings& getBindings() { return bindings; } Bindings& getBindings() { return bindings; }
State& getState() { return state; } State& getState() { return state; }

View File

@ -4,9 +4,8 @@
#include "Mutex.h" #include "Mutex.h"
#include "Pubsub.h" #include "Pubsub.h"
#include "service/gui/Gui.h" #include "service/gui/Gui.h"
#include "service/gui/ViewPort.h"
#include "service/gui/ViewPort_i.h"
#include <cstdio> #include <cstdio>
#include <lvgl.h>
namespace tt::service::gui { namespace tt::service::gui {
@ -27,7 +26,7 @@ struct Gui {
lv_obj_t* statusbarWidget = nullptr; lv_obj_t* statusbarWidget = nullptr;
// App-specific // App-specific
ViewPort* appViewPort = nullptr; std::shared_ptr<app::AppContext> appToRender = nullptr;
lv_obj_t* _Nullable keyboard = nullptr; lv_obj_t* _Nullable keyboard = nullptr;
lv_group_t* keyboardGroup = nullptr; lv_group_t* keyboardGroup = nullptr;

View File

@ -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

View File

@ -6,7 +6,6 @@
#include "MessageQueue.h" #include "MessageQueue.h"
#include "Pubsub.h" #include "Pubsub.h"
#include "Thread.h" #include "Thread.h"
#include "service/gui/ViewPort.h"
#include "service/loader/Loader.h" #include "service/loader/Loader.h"
#include "RtosCompatSemaphore.h" #include "RtosCompatSemaphore.h"
#include <stack> #include <stack>
@ -25,31 +24,9 @@ typedef enum {
LoaderEventTypeApplicationStopped LoaderEventTypeApplicationStopped
} LoaderEventType; } LoaderEventType;
typedef struct { struct LoaderEvent {
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 {
LoaderEventType type; LoaderEventType type;
union { };
LoaderEventAppStarted app_started;
LoaderEventAppShowing app_showing;
LoaderEventAppHiding app_hiding;
LoaderEventAppStopped app_stopped;
};
} LoaderEvent;
// endregion LoaderEvent // endregion LoaderEvent
@ -77,10 +54,9 @@ public:
// endregion LoaderMessage // endregion LoaderMessage
struct Loader { struct Loader {
std::shared_ptr<PubSub> pubsubInternal = std::make_shared<PubSub>();
std::shared_ptr<PubSub> pubsubExternal = std::make_shared<PubSub>(); std::shared_ptr<PubSub> pubsubExternal = std::make_shared<PubSub>();
Mutex mutex = Mutex(Mutex::Type::Recursive); Mutex mutex = Mutex(Mutex::Type::Recursive);
std::stack<app::AppInstance*> appStack; std::stack<std::shared_ptr<app::AppInstance>> appStack;
/** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods. /** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods.
* This includes full LVGL redraw via Gui::redraw() * This includes full LVGL redraw via Gui::redraw()
*/ */

View File

@ -64,7 +64,6 @@ namespace app {
namespace screenshot { extern const AppManifest manifest; } namespace screenshot { extern const AppManifest manifest; }
#endif #endif
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
extern const AppManifest elfWrapperManifest;
namespace crashdiagnostics { extern const AppManifest manifest; } namespace crashdiagnostics { extern const AppManifest manifest; }
#endif #endif
} }
@ -99,8 +98,7 @@ static const std::vector<const app::AppManifest*> system_apps = {
&app::screenshot::manifest, &app::screenshot::manifest,
#endif #endif
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
&app::crashdiagnostics::manifest, &app::crashdiagnostics::manifest
&app::elfWrapperManifest, // For hot-loading ELF apps
#endif #endif
}; };
@ -109,11 +107,11 @@ static const std::vector<const app::AppManifest*> system_apps = {
static void register_system_apps() { static void register_system_apps() {
TT_LOG_I(TAG, "Registering default apps"); TT_LOG_I(TAG, "Registering default apps");
for (const auto* app_manifest: system_apps) { for (const auto* app_manifest: system_apps) {
addApp(app_manifest); addApp(*app_manifest);
} }
if (getConfiguration()->hardware->power != nullptr) { 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<const app::AppManifest*>& apps)
TT_LOG_I(TAG, "Registering user apps"); TT_LOG_I(TAG, "Registering user apps");
for (auto* manifest : apps) { for (auto* manifest : apps) {
assert(manifest != nullptr); assert(manifest != nullptr);
addApp(manifest); addApp(*manifest);
} }
} }

View File

@ -0,0 +1,78 @@
#pragma once
#include "AppContext.h"
#include "Bundle.h"
#include <Mutex.h>
// 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<Bundle> resultData;
explicit ResultHolder(Result result) : result(result), resultData(nullptr) {}
ResultHolder(Result result, std::unique_ptr<Bundle> resultData) :
result(result),
resultData(std::move(resultData)) {}
};
std::unique_ptr<ResultHolder> 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<Bundle> _Nullable resultData) {}
Mutex& getMutex() { return mutex; }
bool hasResult() const { return resultHolder != nullptr; }
void setResult(Result result, std::unique_ptr<Bundle> resultData = nullptr) {
auto lockable = getMutex().scoped();
lockable->lock(portMAX_DELAY);
resultHolder = std::make_unique<ResultHolder>(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<Bundle>& 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<typename T>
std::shared_ptr<App> create() { return std::shared_ptr<T>(new T); }
}

View File

@ -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<Bundle> 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<Bundle> _Nullable resultData) override {
if (onResultCallback != nullptr) {
onResultCallback(appContext, data, result, std::move(resultData));
}
}
};
template<typename T>
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
);
}
}

View File

@ -1,12 +1,15 @@
#pragma once #pragma once
#include "AppManifest.h"
#include "Bundle.h" #include "Bundle.h"
#include <memory> #include <memory>
namespace tt::app { namespace tt::app {
// Forward declarations
class App;
class Paths; class Paths;
struct AppManifest;
enum class Result;
typedef union { typedef union {
struct { struct {
@ -28,14 +31,10 @@ protected:
public: public:
virtual const AppManifest& getManifest() const = 0; virtual const AppManifest& getManifest() const = 0;
virtual std::shared_ptr<void> _Nullable getData() const = 0;
virtual void setData(std::shared_ptr<void> data) = 0;
virtual std::shared_ptr<const Bundle> getParameters() const = 0; virtual std::shared_ptr<const Bundle> getParameters() const = 0;
virtual Flags getFlags() const = 0;
virtual void setResult(Result result) = 0;
virtual void setResult(Result result, std::shared_ptr<const Bundle> bundle)= 0;
virtual bool hasResult() const = 0;
virtual std::unique_ptr<Paths> getPaths() const = 0; virtual std::unique_ptr<Paths> getPaths() const = 0;
virtual std::shared_ptr<App> getApp() const = 0;
}; };
class Paths { class Paths {

View File

@ -25,7 +25,8 @@ State AppInstance::getState() const {
* Consider not exposing bundle, but expose `app_get_bundle_int(key)` methods with locking in it. * Consider not exposing bundle, but expose `app_get_bundle_int(key)` methods with locking in it.
*/ */
const AppManifest& AppInstance::getManifest() const { const AppManifest& AppInstance::getManifest() const {
return manifest; tt_assert(manifest != nullptr);
return *manifest;
} }
Flags AppInstance::getFlags() const { Flags AppInstance::getFlags() const {
@ -41,19 +42,6 @@ void AppInstance::setFlags(Flags newFlags) {
mutex.release(); mutex.release();
} }
std::shared_ptr<void> _Nullable AppInstance::getData() const {
mutex.acquire(TtWaitForever);
auto result = data;
mutex.release();
return result;
}
void AppInstance::setData(std::shared_ptr<void> newData) {
mutex.acquire(TtWaitForever);
data = newData;
mutex.release();
}
std::shared_ptr<const Bundle> AppInstance::getParameters() const { std::shared_ptr<const Bundle> AppInstance::getParameters() const {
mutex.acquire(TtWaitForever); mutex.acquire(TtWaitForever);
std::shared_ptr<const Bundle> result = parameters; std::shared_ptr<const Bundle> result = parameters;
@ -61,29 +49,9 @@ std::shared_ptr<const Bundle> AppInstance::getParameters() const {
return result; return result;
} }
void AppInstance::setResult(Result result) {
std::unique_ptr<ResultHolder> new_holder(new ResultHolder(result));
mutex.acquire(TtWaitForever);
resultHolder = std::move(new_holder);
mutex.release();
}
void AppInstance::setResult(Result result, std::shared_ptr<const Bundle> bundle) {
std::unique_ptr<ResultHolder> 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<Paths> AppInstance::getPaths() const { std::unique_ptr<Paths> AppInstance::getPaths() const {
return std::make_unique<AppInstancePaths>(manifest); tt_assert(manifest != nullptr);
return std::make_unique<AppInstancePaths>(*manifest);
} }
} // namespace } // namespace

View File

@ -1,44 +1,63 @@
#pragma once #pragma once
#include <string>
#include <Bundle.h>
#include "CoreDefines.h" #include "CoreDefines.h"
#include "ManifestRegistry.h"
#include <Bundle.h>
#include <string>
// Forward declarations // Forward declarations
typedef struct _lv_obj_t lv_obj_t; typedef struct _lv_obj_t lv_obj_t;
namespace tt::app { namespace tt::app {
class App;
class AppContext; class AppContext;
/** Application types */ /** Application types */
enum Type { enum class Type {
/** Boot screen, shown before desktop is launched. */ /** 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 */ /** 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) */ /** Apps that generally aren't started from the desktop (e.g. image viewer) */
TypeHidden, Hidden,
/** Standard apps, provided by the system. */ /** 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. */ /** The apps that are launched/shown by the Settings app. The Settings app itself is of type AppTypeSystem. */
TypeSettings, Settings,
/** User-provided apps. */ /** User-provided apps. */
TypeUser User
}; };
/** Result status code for application result callback. */ /** Result status code for application result callback. */
typedef enum { enum class Result {
ResultOk, Ok = 0U,
ResultCancelled, Cancelled = 1U,
ResultError Error = 2U
} Result; };
typedef void (*AppOnStart)(AppContext& app); class Location {
typedef void (*AppOnStop)(AppContext& app);
typedef void (*AppOnShow)(AppContext& app, lv_obj_t* parent); private:
typedef void (*AppOnHide)(AppContext& app);
typedef void (*AppOnResult)(AppContext& app, Result result, const Bundle& resultData); 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<App>(*CreateApp)();
struct AppManifest { struct AppManifest {
/** The identifier by which the app is launched by the system and other apps. */ /** The identifier by which the app is launched by the system and other apps. */
@ -51,26 +70,17 @@ struct AppManifest {
std::string icon = {}; std::string icon = {};
/** App type affects launch behaviour. */ /** App type affects launch behaviour. */
Type type = TypeUser; Type type = Type::User;
/** Non-blocking method to call when app is started. */ /** Where the app is located */
AppOnStart onStart = nullptr; Location location = Location::internal();
/** Non-blocking method to call when app is stopped. */ /** Create the instance of the app */
AppOnStop _Nullable onStop = nullptr; CreateApp createApp = 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;
}; };
struct { struct {
bool operator()(const AppManifest* left, const AppManifest* right) const { return left->name < right->name; } bool operator()(const std::shared_ptr<AppManifest>& left, const std::shared_ptr<AppManifest>& right) const { return left->name < right->name; }
} SortAppManifestByName; } SortAppManifestByName;
} // namespace } // namespace

View File

@ -1,24 +1,49 @@
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include "file/File.h"
#include "ElfApp.h" #include "ElfApp.h"
#include "Log.h"
#include "StringUtils.h"
#include "TactilityCore.h" #include "TactilityCore.h"
#include "esp_elf.h" #include "esp_elf.h"
#include "file/File.h"
#include "service/loader/Loader.h" #include "service/loader/Loader.h"
#include <string>
namespace tt::app { namespace tt::app {
#define TAG "elf_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; static size_t elfManifestSetCount = 0;
std::unique_ptr<uint8_t[]> elfFileData; static ElfManifest elfManifest;
esp_elf_t elf;
bool startElfApp(const std::string& filePath) { class ElfApp : public App {
private:
const std::string filePath;
std::unique_ptr<uint8_t[]> elfFileData;
esp_elf_t elf;
bool shouldCleanupElf = false; // Whether we have to clean up the above "elf" object
std::unique_ptr<ElfManifest> manifest;
void* data = nullptr;
bool startElf() {
TT_LOG_I(TAG, "Starting ELF %s", filePath.c_str()); TT_LOG_I(TAG, "Starting ELF %s", filePath.c_str());
assert(elfFileData == nullptr); assert(elfFileData == nullptr);
size_t size = 0; size_t size = 0;
@ -29,6 +54,7 @@ bool startElfApp(const std::string& filePath) {
if (esp_elf_init(&elf) < 0) { if (esp_elf_init(&elf) < 0) {
TT_LOG_E(TAG, "Failed to initialize"); TT_LOG_E(TAG, "Failed to initialize");
shouldCleanupElf = true;
return false; return false;
} }
@ -40,87 +66,133 @@ bool startElfApp(const std::string& filePath) {
int argc = 0; int argc = 0;
char* argv[] = {}; char* argv[] = {};
size_t manifest_set_count = elfManifestSetCount;
if (esp_elf_request(&elf, 0, argc, argv) < 0) { if (esp_elf_request(&elf, 0, argc, argv) < 0) {
TT_LOG_W(TAG, "Executable returned error code"); TT_LOG_W(TAG, "Executable returned error code");
return false; return false;
} }
if (elfManifestSetCount > manifest_set_count) { return true;
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;
} }
return true; void stopElf() {
} TT_LOG_I(TAG, "Cleaning up ELF");
static void onStart(AppContext& app) {} if (shouldCleanupElf) {
static void onStop(AppContext& app) {} esp_elf_deinit(&elf);
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 = { if (elfFileData != nullptr) {
.id = "", elfFileData = nullptr;
.name = "", }
.type = TypeHidden, }
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>(elfManifest);
if (manifest->createData != nullptr) {
data = manifest->createData();
}
if (manifest->onStart != nullptr) {
manifest->onStart(appContext, data);
}
}
} else {
service::loader::stopApp();
}
}
void onStop(AppContext& appContext) override {
TT_LOG_I(TAG, "Cleaning up app");
if (manifest != nullptr) {
if (manifest->onStop != nullptr) {
manifest->onStop(appContext, data);
}
if (manifest->destroyData != nullptr && data != nullptr) {
manifest->destroyData(data);
}
this->manifest = nullptr;
}
stopElf();
}
void onShow(AppContext& appContext, lv_obj_t* parent) override {
if (manifest != nullptr && manifest->onShow != nullptr) {
manifest->onShow(appContext, data, parent);
}
}
void onHide(AppContext& appContext) override {
if (manifest != nullptr && manifest->onHide != nullptr) {
manifest->onHide(appContext, data);
}
}
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> resultBundle) override {
if (manifest != nullptr && manifest->onResult != nullptr) {
manifest->onResult(appContext, data, result, std::move(resultBundle));
}
}
};
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, .onStart = onStart,
.onStop = onStop, .onStop = onStop,
.onShow = onShow, .onShow = onShow,
.onHide = onHide, .onHide = onHide,
.onResult = onResult .onResult = onResult
}; };
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;
elfManifestSetCount++; 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<App> createElfApp(const std::shared_ptr<AppManifest>& manifest) {
TT_LOG_I(TAG, "createElfApp");
tt_assert(manifest != nullptr);
tt_assert(manifest->location.isExternal());
return std::make_shared<ElfApp>(manifest->location.getPath());
}
} // namespace } // namespace
#endif // ESP_PLATFORM #endif // ESP_PLATFORM

View File

@ -1,16 +1,35 @@
#pragma once #pragma once
#include "AppCompatC.h"
#include "AppManifest.h" #include "AppManifest.h"
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
namespace tt::app { 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<App> createElfApp(const std::shared_ptr<AppManifest>& manifest);
} }
#endif // ESP_PLATFORM #endif // ESP_PLATFORM

View File

@ -1,40 +1,44 @@
#include "ManifestRegistry.h" #include "ManifestRegistry.h"
#include "Mutex.h" #include "Mutex.h"
#include "TactilityCore.h" #include "AppManifest.h"
#include <unordered_map> #include <unordered_map>
#define TAG "app" #define TAG "app"
namespace tt::app { namespace tt::app {
typedef std::unordered_map<std::string, const AppManifest*> AppManifestMap; typedef std::unordered_map<std::string, std::shared_ptr<AppManifest>> AppManifestMap;
static AppManifestMap app_manifest_map; static AppManifestMap app_manifest_map;
static Mutex hash_mutex(Mutex::Type::Normal); static Mutex hash_mutex(Mutex::Type::Normal);
void addApp(const AppManifest* manifest) { void addApp(const AppManifest& manifest) {
TT_LOG_I(TAG, "Registering manifest %s", manifest->id.c_str()); TT_LOG_I(TAG, "Registering manifest %s", manifest.id.c_str());
hash_mutex.acquire(TtWaitForever); hash_mutex.acquire(TtWaitForever);
if (app_manifest_map[manifest->id] == nullptr) { if (!app_manifest_map.contains(manifest.id)) {
app_manifest_map[manifest->id] = manifest; app_manifest_map[manifest.id] = std::make_shared<AppManifest>(manifest);
} else { } 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(); hash_mutex.release();
} }
_Nullable const AppManifest * findAppById(const std::string& id) { _Nullable std::shared_ptr<AppManifest> findAppById(const std::string& id) {
hash_mutex.acquire(TtWaitForever); hash_mutex.acquire(TtWaitForever);
_Nullable const AppManifest* result = app_manifest_map[id.c_str()]; auto result = app_manifest_map.find(id);
hash_mutex.release(); hash_mutex.release();
return result; if (result != app_manifest_map.end()) {
return result->second;
} else {
return nullptr;
}
} }
std::vector<const AppManifest*> getApps() { std::vector<std::shared_ptr<AppManifest>> getApps() {
std::vector<const AppManifest*> manifests; std::vector<std::shared_ptr<AppManifest>> manifests;
hash_mutex.acquire(TtWaitForever); hash_mutex.acquire(TtWaitForever);
for (const auto& item: app_manifest_map) { for (const auto& item: app_manifest_map) {
manifests.push_back(item.second); manifests.push_back(item.second);

View File

@ -1,21 +1,23 @@
#pragma once #pragma once
#include "AppManifest.h" #include "App.h"
#include <string> #include <string>
#include <vector> #include <vector>
namespace tt::app { namespace tt::app {
struct AppManifest;
/** Register an application with its manifest */ /** Register an application with its manifest */
void addApp(const AppManifest* manifest); void addApp(const AppManifest& manifest);
/** Find an application manifest by its id /** Find an application manifest by its id
* @param[in] id the manifest id * @param[in] id the manifest id
* @return the application manifest if it was found * @return the application manifest if it was found
*/ */
const AppManifest _Nullable* findAppById(const std::string& id); _Nullable std::shared_ptr<AppManifest> findAppById(const std::string& id);
/** @return a list of all registered apps. This includes user and system apps. */ /** @return a list of all registered apps. This includes user and system apps. */
std::vector<const AppManifest*> getApps(); std::vector<std::shared_ptr<AppManifest>> getApps();
} // namespace } // namespace

View File

@ -34,10 +34,6 @@ int32_t getResultIndex(const Bundle& bundle) {
return index; return index;
} }
void setResultIndex(std::shared_ptr<Bundle> bundle, int32_t index) {
bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, index);
}
static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) { static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
std::string result; std::string result;
if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) { if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) {
@ -47,26 +43,40 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
} }
} }
static void onButtonClicked(lv_event_t* e) {
class AlertDialogApp : public App {
private:
static void onButtonClickedCallback(lv_event_t* e) {
auto appContext = service::loader::getCurrentAppContext();
tt_assert(appContext != nullptr);
auto app = std::static_pointer_cast<AlertDialogApp>(appContext->getApp());
app->onButtonClicked(e);
}
void onButtonClicked(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e); lv_event_code_t code = lv_event_get_code(e);
auto index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e)); auto index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e));
TT_LOG_I(TAG, "Selected item at index %d", index); TT_LOG_I(TAG, "Selected item at index %d", index);
tt::app::AppContext* app = service::loader::getCurrentApp();
auto bundle = std::make_shared<Bundle>();
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) { auto bundle = std::make_unique<Bundle>();
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 = lv_button_create(parent);
lv_obj_t* button_label = lv_label_create(button); lv_obj_t* button_label = lv_label_create(button);
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(button_label, text.c_str()); lv_label_set_text(button_label, text.c_str());
lv_obj_add_event_cb(button, &onButtonClicked, LV_EVENT_SHORT_CLICKED, (void*)index); lv_obj_add_event_cb(button, onButtonClickedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
} }
public:
static void onShow(AppContext& app, lv_obj_t* parent) { void onShow(AppContext& app, lv_obj_t* parent) override {
auto parameters = app.getParameters(); auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing"); tt_check(parameters != nullptr, "Parameters missing");
@ -97,12 +107,12 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
std::vector<std::string> labels = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN); std::vector<std::string> labels = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN);
if (labels.empty() || labels.front().empty()) { if (labels.empty() || labels.front().empty()) {
TT_LOG_E(TAG, "No items provided"); TT_LOG_E(TAG, "No items provided");
app.setResult(ResultError); setResult(Result::Error);
service::loader::stopApp(); service::loader::stopApp();
} else if (labels.size() == 1) { } else if (labels.size() == 1) {
auto result_bundle = std::make_shared<Bundle>(); auto result_bundle = std::make_unique<Bundle>();
setResultIndex(result_bundle, 0); result_bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, 0);
app.setResult(ResultOk, result_bundle); setResult(Result::Ok, std::move(result_bundle));
service::loader::stopApp(); service::loader::stopApp();
TT_LOG_W(TAG, "Auto-selecting single item"); TT_LOG_W(TAG, "Auto-selecting single item");
} else { } else {
@ -112,13 +122,14 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
} }
} }
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "AlertDialog", .id = "AlertDialog",
.name = "Alert Dialog", .name = "Alert Dialog",
.type = TypeHidden, .type = Type::Hidden,
.onShow = onShow .createApp = create<AlertDialogApp>
}; };
} }

View File

@ -8,20 +8,25 @@
namespace tt::app::applist { namespace tt::app::applist {
static void onAppPressed(lv_event_t* e) {
class AppListApp : public App {
private:
static void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e)); const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
service::loader::startApp(manifest->id); service::loader::startApp(manifest->id);
} }
static void createAppWidget(const AppManifest* manifest, void* parent) { static void createAppWidget(const std::shared_ptr<AppManifest>& manifest, lv_obj_t* list) {
tt_check(parent);
auto* list = reinterpret_cast<lv_obj_t*>(parent);
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; 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_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(TT_UNUSED AppContext& app, lv_obj_t* parent) { public:
void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
auto* toolbar = lvgl::toolbar_create(parent, app); auto* toolbar = lvgl::toolbar_create(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
@ -38,24 +43,26 @@ static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
lv_list_add_text(list, "User"); lv_list_add_text(list, "User");
for (const auto& manifest: manifests) { for (const auto& manifest: manifests) {
if (manifest->type == TypeUser) { if (manifest->type == Type::User) {
createAppWidget(manifest, list); createAppWidget(manifest, list);
} }
} }
lv_list_add_text(list, "System"); lv_list_add_text(list, "System");
for (const auto& manifest: manifests) { for (const auto& manifest: manifests) {
if (manifest->type == TypeSystem) { if (manifest->type == Type::System) {
createAppWidget(manifest, list); createAppWidget(manifest, list);
} }
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "AppList", .id = "AppList",
.name = "Apps", .name = "Apps",
.type = TypeHidden, .type = Type::Hidden,
.onShow = onShow, .createApp = create<AppListApp>,
}; };
} // namespace } // namespace

View File

@ -24,16 +24,13 @@
namespace tt::app::boot { namespace tt::app::boot {
static int32_t bootThreadCallback(void* context); class BootApp : public App {
static void startNextApp();
struct Data { private:
Data() : thread("boot", 4096, bootThreadCallback, this) {}
Thread thread; Thread thread = Thread("boot", 4096, bootThreadCallback, this);
};
static int32_t bootThreadCallback(TT_UNUSED void* context) { static int32_t bootThreadCallback(TT_UNUSED void* context) {
TickType_t start_time = kernel::getTicks(); TickType_t start_time = kernel::getTicks();
kernel::systemEventPublish(kernel::SystemEvent::BootSplash); kernel::systemEventPublish(kernel::SystemEvent::BootSplash);
@ -64,10 +61,9 @@ static int32_t bootThreadCallback(TT_UNUSED void* context) {
} }
return 0; return 0;
} }
static void startNextApp() {
static void startNextApp() {
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
esp_reset_reason_t reason = esp_reset_reason(); esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_PANIC) { if (reason == ESP_RST_PANIC) {
@ -83,11 +79,11 @@ static void startNextApp() {
} else { } else {
app::launcher::start(); app::launcher::start();
} }
} }
static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { public:
auto data = std::static_pointer_cast<Data>(app.getData());
void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
auto* image = lv_image_create(parent); auto* image = lv_image_create(parent);
lv_obj_set_size(image, LV_PCT(100), LV_PCT(100)); lv_obj_set_size(image, LV_PCT(100), LV_PCT(100));
@ -99,26 +95,22 @@ static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
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) { void onStop(AppContext& app) override {
auto data = std::make_shared<Data>(); thread.join();
app.setData(data); }
} };
static void onStop(AppContext& app) {
auto data = std::static_pointer_cast<Data>(app.getData());
data->thread.join();
}
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Boot", .id = "Boot",
.name = "Boot", .name = "Boot",
.type = TypeBoot, .type = Type::Boot,
.onStart = onStart, .createApp = create<BootApp>
.onStop = onStop,
.onShow = onShow,
}; };
} // namespace } // namespace

View File

@ -17,7 +17,11 @@ void onContinuePressed(TT_UNUSED lv_event_t* event) {
tt::app::launcher::start(); tt::app::launcher::start();
} }
static void onShow(AppContext& app, lv_obj_t* parent) { class CrashDiagnosticsApp : public App {
public:
void onShow(AppContext& app, lv_obj_t* parent) override {
auto* display = lv_obj_get_display(parent); auto* display = lv_obj_get_display(parent);
int32_t parent_height = lv_display_get_vertical_resolution(display) - STATUSBAR_HEIGHT; int32_t parent_height = lv_display_get_vertical_resolution(display) - STATUSBAR_HEIGHT;
@ -107,13 +111,14 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
} }
} }
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "CrashDiagnostics", .id = "CrashDiagnostics",
.name = "Crash Diagnostics", .name = "Crash Diagnostics",
.type = TypeHidden, .type = Type::Hidden,
.onShow = onShow .createApp = create<CrashDiagnosticsApp>
}; };
void start() { void start() {

View File

@ -98,7 +98,9 @@ static void onOrientationSet(lv_event_t* event) {
} }
} }
static void onShow(AppContext& app, lv_obj_t* parent) { class DisplayApp : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); lvgl::toolbar_create(parent, app);
@ -167,23 +169,21 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lv_display_get_rotation(lv_display_get_default()) lv_display_get_rotation(lv_display_get_default())
); );
lv_dropdown_set_selected(orientation_dropdown, orientation_selected); lv_dropdown_set_selected(orientation_dropdown, orientation_selected);
} }
static void onHide(TT_UNUSED AppContext& app) { void onHide(TT_UNUSED AppContext& app) override {
if (backlight_duty_set) { if (backlight_duty_set) {
setBacklightDuty(backlight_duty); setBacklightDuty(backlight_duty);
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Display", .id = "Display",
.name = "Display", .name = "Display",
.icon = TT_ASSETS_APP_ICON_DISPLAY_SETTINGS, .icon = TT_ASSETS_APP_ICON_DISPLAY_SETTINGS,
.type = TypeSettings, .type = Type::Settings,
.onStart = nullptr, .createApp = create<DisplayApp>
.onStop = nullptr,
.onShow = onShow,
.onHide = onHide
}; };
} // namespace } // namespace

View File

@ -1,4 +1,5 @@
#include "app/files/FilesPrivate.h" #include "app/files/View.h"
#include "app/files/State.h"
#include "app/AppContext.h" #include "app/AppContext.h"
#include "Assets.h" #include "Assets.h"
@ -12,31 +13,31 @@ namespace tt::app::files {
extern const AppManifest manifest; 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> view;
std::shared_ptr<State> state;
static void onShow(AppContext& app, lv_obj_t* parent) { public:
auto files = std::static_pointer_cast<Files>(app.getData()); FilesApp() {
files->onShow(parent); state = std::make_shared<State>();
} view = std::make_unique<View>(state);
}
static void onStart(AppContext& app) { void onShow(AppContext& appContext, lv_obj_t* parent) override {
auto files = std::make_shared<Files>(); view->init(parent);
app.setData(files); }
}
static void onResult(AppContext& app, Result result, const Bundle& bundle) { void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> bundle) override {
auto files = std::static_pointer_cast<Files>(app.getData()); view->onResult(result, std::move(bundle));
files->onResult(result, bundle); }
} };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Files", .id = "Files",
.name = "Files", .name = "Files",
.icon = TT_ASSETS_APP_ICON_FILES, .icon = TT_ASSETS_APP_ICON_FILES,
.type = TypeHidden, .type = Type::Hidden,
.onStart = onStart, .createApp = create<FilesApp>
.onShow = onShow,
.onResult = onResult
}; };
void start() { void start() {

View File

@ -8,12 +8,15 @@
#include "app/ElfApp.h" #include "app/ElfApp.h"
#include "lvgl/Toolbar.h" #include "lvgl/Toolbar.h"
#include "lvgl/LvglSync.h" #include "lvgl/LvglSync.h"
#include "service/loader/Loader.h"
#include "Tactility.h" #include "Tactility.h"
#include "StringUtils.h" #include "StringUtils.h"
#include <cstring> #include <cstring>
#include <unistd.h> #include <unistd.h>
#ifdef ESP_PLATFORM
#include "service/loader/Loader.h"
#endif
#define TAG "files_app" #define TAG "files_app"
namespace tt::app::files { namespace tt::app::files {
@ -81,7 +84,9 @@ void View::viewFile(const std::string& path, const std::string& filename) {
if (isSupportedExecutableFile(filename)) { if (isSupportedExecutableFile(filename)) {
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
app::startElfApp(processed_filepath); app::registerElfApp(processed_filepath);
auto app_id = app::getElfAppId(processed_filepath);
service::loader::startApp(app_id);
#endif #endif
} else if (isSupportedImageFile(filename)) { } else if (isSupportedImageFile(filename)) {
app::imageviewer::start(processed_filepath); app::imageviewer::start(processed_filepath);
@ -282,8 +287,8 @@ void View::onNavigate() {
} }
} }
void View::onResult(Result result, const Bundle& bundle) { void View::onResult(Result result, std::unique_ptr<Bundle> bundle) {
if (result != ResultOk) { if (result != Result::Ok || bundle == nullptr) {
return; return;
} }
@ -292,7 +297,7 @@ void View::onResult(Result result, const Bundle& bundle) {
switch (state->getPendingAction()) { switch (state->getPendingAction()) {
case State::ActionDelete: { case State::ActionDelete: {
if (alertdialog::getResultIndex(bundle) == 0) { if (alertdialog::getResultIndex(*bundle) == 0) {
int delete_count = (int)remove(filepath.c_str()); int delete_count = (int)remove(filepath.c_str());
if (delete_count > 0) { if (delete_count > 0) {
TT_LOG_I(TAG, "Deleted %d items", delete_count); TT_LOG_I(TAG, "Deleted %d items", delete_count);
@ -305,7 +310,7 @@ void View::onResult(Result result, const Bundle& bundle) {
break; break;
} }
case State::ActionRename: { 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()) { if (!new_name.empty() && new_name != state->getSelectedChildEntry()) {
std::string rename_to = getChildPath(state->getCurrentPath(), new_name); std::string rename_to = getChildPath(state->getCurrentPath(), new_name);
if (rename(filepath.c_str(), rename_to.c_str())) { if (rename(filepath.c_str(), rename_to.c_str())) {

View File

@ -1,6 +1,5 @@
#include "Mutex.h" #include "Mutex.h"
#include "Thread.h" #include "Thread.h"
#include "Tactility.h"
#include "service/loader/Loader.h" #include "service/loader/Loader.h"
#include "lvgl/Toolbar.h" #include "lvgl/Toolbar.h"
@ -10,7 +9,9 @@
namespace tt::app::gpio { namespace tt::app::gpio {
class Gpio { extern const AppManifest manifest;
class GpioApp : public App {
private: private:
@ -19,6 +20,9 @@ private:
std::unique_ptr<Timer> timer; std::unique_ptr<Timer> timer;
Mutex mutex; Mutex mutex;
static lv_obj_t* createGpioRowWrapper(lv_obj_t* parent);
static void onTimer(TT_UNUSED std::shared_ptr<void> context);
public: public:
void lock() const { void lock() const {
@ -29,18 +33,17 @@ public:
tt_check(mutex.release() == TtStatusOk); tt_check(mutex.release() == TtStatusOk);
} }
void onShow(AppContext& app, lv_obj_t* parent); void onShow(AppContext& app, lv_obj_t* parent) override;
void onHide(AppContext& app); void onHide(AppContext& app) override;
void startTask(std::shared_ptr<Gpio> ptr); void startTask();
void stopTask(); void stopTask();
void updatePinStates(); void updatePinStates();
void updatePinWidgets(); void updatePinWidgets();
}; };
void GpioApp::updatePinStates() {
void Gpio::updatePinStates() {
lock(); lock();
// Update pin states // Update pin states
for (int i = 0; i < GPIO_NUM_MAX; ++i) { for (int i = 0; i < GPIO_NUM_MAX; ++i) {
@ -53,7 +56,7 @@ void Gpio::updatePinStates() {
unlock(); unlock();
} }
void Gpio::updatePinWidgets() { void GpioApp::updatePinWidgets() {
if (lvgl::lock(100)) { if (lvgl::lock(100)) {
lock(); lock();
for (int j = 0; j < GPIO_NUM_MAX; ++j) { 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_t* wrapper = lv_obj_create(parent);
lv_obj_set_style_pad_all(wrapper, 0, 0); lv_obj_set_style_pad_all(wrapper, 0, 0);
lv_obj_set_style_border_width(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 // region Task
static void onTimer(std::shared_ptr<void> context) { void GpioApp::onTimer(TT_UNUSED std::shared_ptr<void> context) {
auto gpio = std::static_pointer_cast<Gpio>(context); auto appContext = service::loader::getCurrentAppContext();
if (appContext->getManifest().id == manifest.id) {
gpio->updatePinStates(); auto app = std::static_pointer_cast<GpioApp>(appContext->getApp());
gpio->updatePinWidgets(); if (app != nullptr) {
app->updatePinStates();
app->updatePinWidgets();
}
}
} }
void Gpio::startTask(std::shared_ptr<Gpio> ptr) { void GpioApp::startTask() {
lock(); lock();
tt_assert(timer == nullptr); tt_assert(timer == nullptr);
timer = std::make_unique<Timer>( timer = std::make_unique<Timer>(
Timer::Type::Periodic, Timer::Type::Periodic,
&onTimer, &onTimer
ptr
); );
timer->start(100 / portTICK_PERIOD_MS); timer->start(100 / portTICK_PERIOD_MS);
unlock(); unlock();
} }
void Gpio::stopTask() { void GpioApp::stopTask() {
tt_assert(timer); tt_assert(timer);
timer->stop(); timer->stop();
@ -114,9 +120,7 @@ void Gpio::stopTask() {
// endregion Task // endregion Task
void Gpio::onShow(AppContext& app, lv_obj_t* parent) { void GpioApp::onShow(AppContext& app, lv_obj_t* parent) {
auto gpio = std::static_pointer_cast<Gpio>(app.getData());
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app); lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); 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_t* row_wrapper = createGpioRowWrapper(wrapper);
lv_obj_align(row_wrapper, LV_ALIGN_TOP_MID, 0, 0); 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) { for (int i = GPIO_NUM_MIN; i < GPIO_NUM_MAX; ++i) {
// Add the GPIO number before the first item on a row // 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_t* status_label = lv_label_create(row_wrapper);
lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing), 0); lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing), 0);
lv_label_set_text_fmt(status_label, "%s", LV_SYMBOL_STOP); lv_label_set_text_fmt(status_label, "%s", LV_SYMBOL_STOP);
gpio->lvPins[i] = status_label; lvPins[i] = status_label;
column++; column++;
@ -170,42 +174,20 @@ void Gpio::onShow(AppContext& app, lv_obj_t* parent) {
column = 0; column = 0;
} }
} }
gpio->unlock(); unlock();
gpio->startTask(gpio); startTask();
} }
void Gpio::onHide(AppContext& app) { void GpioApp::onHide(AppContext& app) {
auto gpio = std::static_pointer_cast<Gpio>(app.getData()); stopTask();
gpio->stopTask();
} }
// region App lifecycle
static void onShow(AppContext& app, lv_obj_t* parent) {
auto gpio = std::static_pointer_cast<Gpio>(app.getData());
gpio->onShow(app, parent);
}
static void onHide(AppContext& app) {
auto gpio = std::static_pointer_cast<Gpio>(app.getData());
gpio->onHide(app);
}
static void onStart(AppContext& app) {
auto gpio = std::make_shared<Gpio>();
app.setData(gpio);
}
// endregion App lifecycle
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Gpio", .id = "Gpio",
.name = "GPIO", .name = "GPIO",
.type = TypeSystem, .type = Type::System,
.onStart = onStart, .createApp = create<GpioApp>
.onShow = onShow,
.onHide = onHide
}; };
} // namespace } // namespace

View File

@ -14,108 +14,64 @@
namespace tt::app::i2cscanner { namespace tt::app::i2cscanner {
static void updateViews(std::shared_ptr<Data> data);
extern const AppManifest manifest; extern const AppManifest manifest;
class I2cScannerApp : public App {
private:
// Core
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::unique_ptr<Timer> scanTimer = nullptr;
// State
ScanState scanState = ScanStateInitial;
i2c_port_t port = I2C_NUM_0;
std::vector<uint8_t> 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<void> 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. */ /** 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<Data> _Nullable optData() { std::shared_ptr<I2cScannerApp> _Nullable optApp() {
app::AppContext* app = service::loader::getCurrentApp(); auto appContext = service::loader::getCurrentAppContext();
if (app->getManifest().id == manifest.id) { if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
return std::static_pointer_cast<Data>(app->getData()); return std::static_pointer_cast<I2cScannerApp>(appContext->getApp());
} else { } else {
return nullptr; return nullptr;
} }
} }
static void onSelectBus(lv_event_t* event) { // region Lifecycle
auto data = optData();
if (data == nullptr) {
return;
}
auto* dropdown = static_cast<lv_obj_t*>(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) { void I2cScannerApp::onShow(AppContext& app, lv_obj_t* parent) {
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> 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> 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> 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<Data>(app.getData());
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); 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_t* scan_button = lv_button_create(wrapper);
lv_obj_set_width(scan_button, LV_PCT(48)); 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_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_t* scan_button_label = lv_label_create(scan_button);
lv_obj_align(scan_button_label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(scan_button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(scan_button_label, START_SCAN_TEXT); 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); lv_obj_t* port_dropdown = lv_dropdown_create(wrapper);
std::string dropdown_items = getPortNamesForDropdown(); std::string dropdown_items = getPortNamesForDropdown();
lv_dropdown_set_options(port_dropdown, dropdown_items.c_str()); lv_dropdown_set_options(port_dropdown, dropdown_items.c_str());
lv_obj_set_width(port_dropdown, LV_PCT(48)); lv_obj_set_width(port_dropdown, LV_PCT(48));
lv_obj_align(port_dropdown, LV_ALIGN_TOP_RIGHT, 0, 0); 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); 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_t* scan_list = lv_list_create(main_wrapper);
lv_obj_set_style_margin_top(scan_list, 8, 0); lv_obj_set_style_margin_top(scan_list, 8, 0);
lv_obj_set_width(scan_list, LV_PCT(100)); lv_obj_set_width(scan_list, LV_PCT(100));
lv_obj_set_height(scan_list, LV_SIZE_CONTENT); lv_obj_set_height(scan_list, LV_SIZE_CONTENT);
lv_obj_add_flag(scan_list, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(scan_list, LV_OBJ_FLAG_HIDDEN);
data->scanListWidget = scan_list; scanListWidget = scan_list;
} }
static void onHide(AppContext& app) { void I2cScannerApp::onHide(AppContext& app) {
auto data = std::static_pointer_cast<Data>(app.getData());
bool isRunning = false; bool isRunning = false;
if (data->mutex.acquire(250 / portTICK_PERIOD_MS) == TtStatusOk) { if (mutex.acquire(250 / portTICK_PERIOD_MS) == TtStatusOk) {
auto* timer = data->scanTimer.get(); auto* timer = scanTimer.get();
if (timer != nullptr) { if (timer != nullptr) {
isRunning = timer->isRunning(); isRunning = timer->isRunning();
} }
data->mutex.release(); mutex.release();
} else { } else {
return; return;
} }
if (isRunning) { if (isRunning) {
stopScanning(data); stopScanning();
} }
} }
static void onStart(AppContext& app) { // endregion Lifecycle
auto data = std::make_shared<Data>();
app.setData(data); // 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<void> 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>(
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_obj_t*>(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 = { extern const AppManifest manifest = {
.id = "I2cScanner", .id = "I2cScanner",
.name = "I2C Scanner", .name = "I2C Scanner",
.icon = TT_ASSETS_APP_ICON_I2C_SETTINGS, .icon = TT_ASSETS_APP_ICON_I2C_SETTINGS,
.type = TypeSystem, .type = Type::System,
.onStart = onStart, .createApp = create<I2cScannerApp>
.onShow = onShow,
.onHide = onHide
}; };
void start() { void start() {

View File

@ -1,124 +0,0 @@
#include "app/i2cscanner/I2cScannerThread.h"
#include "lvgl.h"
#include "service/loader/Loader.h"
namespace tt::app::i2cscanner {
std::shared_ptr<Data> _Nullable optData();
static bool shouldStopScanTimer(std::shared_ptr<Data> 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> 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> 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<void> 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> 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> 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>(
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> 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

View File

@ -69,7 +69,9 @@ static void show(lv_obj_t* parent, const hal::i2c::Configuration& configuration)
} }
} }
static void onShow(AppContext& app, lv_obj_t* parent) { class I2cSettingsApp : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); lvgl::toolbar_create(parent, app);
@ -83,14 +85,15 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
for (const auto& configuration: getConfiguration()->hardware->i2c) { for (const auto& configuration: getConfiguration()->hardware->i2c) {
show(wrapper, configuration); show(wrapper, configuration);
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "I2cSettings", .id = "I2cSettings",
.name = "I2C", .name = "I2C",
.icon = TT_ASSETS_APP_ICON_I2C_SETTINGS, .icon = TT_ASSETS_APP_ICON_I2C_SETTINGS,
.type = TypeSettings, .type = Type::Settings,
.onShow = onShow .createApp = create<I2cSettingsApp>
}; };
} // namespace } // namespace

View File

@ -12,7 +12,9 @@ extern const AppManifest manifest;
#define TAG "image_viewer" #define TAG "image_viewer"
#define IMAGE_VIEWER_FILE_ARGUMENT "file" #define IMAGE_VIEWER_FILE_ARGUMENT "file"
static void onShow(AppContext& app, lv_obj_t* parent) { class ImageViewerApp : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
auto wrapper = lv_obj_create(parent); auto wrapper = lv_obj_create(parent);
lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100)); lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_border_width(wrapper, 0, 0); lv_obj_set_style_border_width(wrapper, 0, 0);
@ -49,13 +51,14 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
} else { } else {
lv_label_set_text(file_label, "File not found"); lv_label_set_text(file_label, "File not found");
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "ImageViewer", .id = "ImageViewer",
.name = "Image Viewer", .name = "Image Viewer",
.type = TypeHidden, .type = Type::Hidden,
.onShow = onShow .createApp = create<ImageViewerApp>
}; };
void start(const std::string& file) { void start(const std::string& file) {

View File

@ -18,6 +18,7 @@ namespace tt::app::inputdialog {
#define TAG "input_dialog" #define TAG "input_dialog"
extern const AppManifest manifest; extern const AppManifest manifest;
class InputDialogApp;
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) {
auto bundle = std::make_shared<Bundle>(); auto bundle = std::make_shared<Bundle>();
@ -33,10 +34,6 @@ std::string getResult(const Bundle& bundle) {
return result; return result;
} }
void setResult(const std::shared_ptr<Bundle>& bundle, const std::string& result) {
bundle->putString(RESULT_BUNDLE_KEY_RESULT, result);
}
static std::string getTitleParameter(const std::shared_ptr<const Bundle>& bundle) { static std::string getTitleParameter(const std::shared_ptr<const Bundle>& bundle) {
std::string result; std::string result;
if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) { if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) {
@ -46,32 +43,43 @@ static std::string getTitleParameter(const std::shared_ptr<const Bundle>& bundle
} }
} }
static void onButtonClicked(lv_event_t* e) { class InputDialogApp : public App {
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<Bundle>();
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);
} private:
service::loader::stopApp();
}
static void createButton(lv_obj_t* parent, const std::string& text, void* callbackContext) { 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 = lv_button_create(parent);
lv_obj_t* button_label = lv_label_create(button); lv_obj_t* button_label = lv_label_create(button);
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(button_label, text.c_str()); lv_label_set_text(button_label, text.c_str());
lv_obj_add_event_cb(button, &onButtonClicked, LV_EVENT_SHORT_CLICKED, callbackContext); lv_obj_add_event_cb(button, onButtonClickedCallback, LV_EVENT_SHORT_CLICKED, callbackContext);
} }
static void onShow(AppContext& app, lv_obj_t* parent) { static void onButtonClickedCallback(lv_event_t* e) {
auto appContext = service::loader::getCurrentAppContext();
tt_assert(appContext != nullptr);
auto app = std::static_pointer_cast<InputDialogApp>(appContext->getApp());
app->onButtonClicked(e);
}
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<Bundle>();
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);
}
service::loader::stopApp();
}
public:
void onShow(AppContext& app, lv_obj_t* parent) override {
auto parameters = app.getParameters(); auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing"); tt_check(parameters != nullptr, "Parameters missing");
@ -108,13 +116,14 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
createButton(button_wrapper, "OK", textarea); createButton(button_wrapper, "OK", textarea);
createButton(button_wrapper, "Cancel", nullptr); createButton(button_wrapper, "Cancel", nullptr);
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "InputDialog", .id = "InputDialog",
.name = "Input Dialog", .name = "Input Dialog",
.type = TypeHidden, .type = Type::Hidden,
.onShow = onShow .createApp = create<InputDialogApp>
}; };
} }

View File

@ -11,10 +11,10 @@
*/ */
namespace tt::app::inputdialog { 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 * @return the text that was in the field when OK was pressed, or otherwise empty string
*/ */
std::string getResult(const Bundle& bundle); std::string getResult(const Bundle& bundle);
} }

View File

@ -42,7 +42,9 @@ static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char
return wrapper; return wrapper;
} }
static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { class LauncherApp : public App {
void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
auto* wrapper = lv_obj_create(parent); auto* wrapper = lv_obj_create(parent);
lv_obj_align(wrapper, LV_ALIGN_CENTER, 0, 0); lv_obj_align(wrapper, LV_ALIGN_CENTER, 0, 0);
@ -71,13 +73,14 @@ static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
createAppButton(wrapper, "Apps", apps_icon_path.c_str(), "AppList", 0); createAppButton(wrapper, "Apps", apps_icon_path.c_str(), "AppList", 0);
createAppButton(wrapper, "Files", files_icon_path.c_str(), "Files", padding); createAppButton(wrapper, "Files", files_icon_path.c_str(), "Files", padding);
createAppButton(wrapper, "Settings", settings_icon_path.c_str(), "Settings", padding); createAppButton(wrapper, "Settings", settings_icon_path.c_str(), "Settings", padding);
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Launcher", .id = "Launcher",
.name = "Launcher", .name = "Launcher",
.type = TypeLauncher, .type = Type::Launcher,
.onShow = onShow, .createApp = create<LauncherApp>
}; };
void start() { void start() {

View File

@ -11,27 +11,22 @@
namespace tt::app::log { namespace tt::app::log {
struct LogAppData { class LogApp : public App {
private:
LogLevel filterLevel = LogLevel::Info; LogLevel filterLevel = LogLevel::Info;
lv_obj_t* labelWidget = nullptr; lv_obj_t* labelWidget = nullptr;
};
static bool shouldShowLog(LogLevel filterLevel, LogLevel logLevel) { static bool shouldShowLog(LogLevel filterLevel, LogLevel logLevel) {
if (filterLevel == LogLevel::None || logLevel == LogLevel::None) { if (filterLevel == LogLevel::None || logLevel == LogLevel::None) {
return false; return false;
} else { } else {
return filterLevel >= logLevel; 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<LogAppData>(app->getData());
auto filterLevel = data->filterLevel;
void updateLogEntries() {
unsigned int index; unsigned int index;
auto* entries = copyLogEntries(index); auto* entries = copyLogEntries(index);
std::stringstream buffer; std::stringstream buffer;
@ -50,16 +45,23 @@ static void setLogEntries(lv_obj_t* label) {
} }
delete entries; delete entries;
if (!buffer.str().empty()) { if (!buffer.str().empty()) {
lv_label_set_text(label, buffer.str().c_str()); lv_label_set_text(labelWidget, buffer.str().c_str());
} else { } else {
lv_label_set_text(label, "No logs for the selected log level"); lv_label_set_text(labelWidget, "No logs for the selected log level");
} }
} else { } else {
lv_label_set_text(label, "Failed to load log"); lv_label_set_text(labelWidget, "Failed to load log");
}
} }
}
static void onLevelFilterPressed(TT_UNUSED lv_event_t* event) { void updateViews() {
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
updateLogEntries();
lvgl::unlock();
}
}
static void onLevelFilterPressedCallback(TT_UNUSED lv_event_t* event) {
std::vector<std::string> items = { std::vector<std::string> items = {
"Verbose", "Verbose",
"Debug", "Debug",
@ -68,28 +70,14 @@ static void onLevelFilterPressed(TT_UNUSED lv_event_t* event) {
"Error", "Error",
}; };
app::selectiondialog::start("Log Level", items); app::selectiondialog::start("Log Level", items);
}
static void updateViews() {
auto app = service::loader::getCurrentApp();
if (app == nullptr) {
return;
} }
auto data = std::static_pointer_cast<LogAppData>(app->getData());
assert(data != nullptr);
if (lvgl::lock(100 / portTICK_PERIOD_MS)) { public:
setLogEntries(data->labelWidget);
lvgl::unlock();
}
}
static void onShow(AppContext& app, lv_obj_t* parent) {
auto data = std::static_pointer_cast<LogAppData>(app.getData());
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
auto* toolbar = lvgl::toolbar_create(parent, app); auto* toolbar = lvgl::toolbar_create(parent, app);
lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_EDIT, onLevelFilterPressed, nullptr); lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_EDIT, onLevelFilterPressedCallback, this);
auto* wrapper = lv_obj_create(parent); auto* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_width(wrapper, LV_PCT(100));
@ -98,35 +86,30 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lvgl::obj_set_style_no_padding(wrapper); lvgl::obj_set_style_no_padding(wrapper);
lvgl::obj_set_style_bg_invisible(wrapper); lvgl::obj_set_style_bg_invisible(wrapper);
data->labelWidget = lv_label_create(wrapper); labelWidget = lv_label_create(wrapper);
lv_obj_align(data->labelWidget, LV_ALIGN_CENTER, 0, 0); lv_obj_align(labelWidget, LV_ALIGN_CENTER, 0, 0);
setLogEntries(data->labelWidget);
}
static void onStart(AppContext& app) { updateLogEntries();
auto data = std::make_shared<LogAppData>(); }
app.setData(data);
}
static void onResult(AppContext& app, Result result, const Bundle& bundle) { void onResult(AppContext& app, Result result, std::unique_ptr<Bundle> bundle) override {
auto resultIndex = selectiondialog::getResultIndex(bundle); auto resultIndex = selectiondialog::getResultIndex(*bundle);
auto data = std::static_pointer_cast<LogAppData>(app.getData()); if (result == Result::Ok) {
if (result == ResultOk) {
switch (resultIndex) { switch (resultIndex) {
case 0: case 0:
data->filterLevel = LogLevel::Verbose; filterLevel = LogLevel::Verbose;
break; break;
case 1: case 1:
data->filterLevel = LogLevel::Debug; filterLevel = LogLevel::Debug;
break; break;
case 2: case 2:
data->filterLevel = LogLevel::Info; filterLevel = LogLevel::Info;
break; break;
case 3: case 3:
data->filterLevel = LogLevel::Warning; filterLevel = LogLevel::Warning;
break; break;
case 4: case 4:
data->filterLevel = LogLevel::Error; filterLevel = LogLevel::Error;
break; break;
default: default:
break; break;
@ -134,16 +117,15 @@ static void onResult(AppContext& app, Result result, const Bundle& bundle) {
} }
updateViews(); updateViews();
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Log", .id = "Log",
.name = "Log", .name = "Log",
.icon = LV_SYMBOL_LIST, .icon = LV_SYMBOL_LIST,
.type = TypeSystem, .type = Type::System,
.onStart = onStart, .createApp = create<LogApp>
.onShow = onShow,
.onResult = onResult
}; };
} // namespace } // namespace

View File

@ -15,31 +15,60 @@ namespace tt::app::power {
extern const AppManifest manifest; extern const AppManifest manifest;
static void onTimer(TT_UNUSED std::shared_ptr<void> context); static void onTimer(TT_UNUSED std::shared_ptr<void> context);
struct Data { class PowerApp;
Timer update_timer = Timer(Timer::Type::Periodic, &onTimer, nullptr);
std::shared_ptr<tt::hal::Power> 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;
};
/** 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. */ /** 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<Data> _Nullable optData() { std::shared_ptr<PowerApp> _Nullable optApp() {
app::AppContext* app = service::loader::getCurrentApp(); auto appContext = service::loader::getCurrentAppContext();
if (app->getManifest().id == manifest.id) { if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
return std::static_pointer_cast<Data>(app->getData()); return std::static_pointer_cast<PowerApp>(appContext->getApp());
} else { } else {
return nullptr; return nullptr;
} }
} }
static void updateUi(std::shared_ptr<Data> data) { class PowerApp : public App {
private:
Timer update_timer = Timer(Timer::Type::Periodic, &onTimer, nullptr);
std::shared_ptr<tt::hal::Power> 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<void> context) {
auto app = optApp();
if (app != nullptr) {
app->updateUi();
}
}
void onPowerEnabledChanged(lv_event_t* event) {
lv_event_code_t code = lv_event_get_code(event);
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
if (code == LV_EVENT_VALUE_CHANGED) {
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
if (power->isAllowedToCharge() != is_on) {
power->setAllowedToCharge(is_on);
updateUi();
}
}
}
static void onPowerEnabledChangedCallback(lv_event_t* event) {
auto* app = (PowerApp*)lv_event_get_user_data(event);
app->onPowerEnabledChanged(event);
}
void updateUi() {
const char* charge_state; const char* charge_state;
hal::Power::MetricData metric_data; hal::Power::MetricData metric_data;
if (data->power->getMetric(hal::Power::MetricType::IsCharging, metric_data)) { if (power->getMetric(hal::Power::MetricType::IsCharging, metric_data)) {
charge_state = metric_data.valueAsBool ? "yes" : "no"; charge_state = metric_data.valueAsBool ? "yes" : "no";
} else { } else {
charge_state = "N/A"; charge_state = "N/A";
@ -47,24 +76,24 @@ static void updateUi(std::shared_ptr<Data> data) {
uint8_t charge_level; uint8_t charge_level;
bool charge_level_scaled_set = false; bool charge_level_scaled_set = false;
if (data->power->getMetric(hal::Power::MetricType::ChargeLevel, metric_data)) { if (power->getMetric(hal::Power::MetricType::ChargeLevel, metric_data)) {
charge_level = metric_data.valueAsUint8; charge_level = metric_data.valueAsUint8;
charge_level_scaled_set = true; charge_level_scaled_set = true;
} }
bool charging_enabled_set = data->power->supportsChargeControl(); bool charging_enabled_set = power->supportsChargeControl();
bool charging_enabled_and_allowed = data->power->supportsChargeControl() && data->power->isAllowedToCharge(); bool charging_enabled_and_allowed = power->supportsChargeControl() && power->isAllowedToCharge();
int32_t current; int32_t current;
bool current_set = false; bool current_set = false;
if (data->power->getMetric(hal::Power::MetricType::Current, metric_data)) { if (power->getMetric(hal::Power::MetricType::Current, metric_data)) {
current = metric_data.valueAsInt32; current = metric_data.valueAsInt32;
current_set = true; current_set = true;
} }
uint32_t battery_voltage; uint32_t battery_voltage;
bool battery_voltage_set = false; bool battery_voltage_set = false;
if (data->power->getMetric(hal::Power::MetricType::BatteryVoltage, metric_data)) { if (power->getMetric(hal::Power::MetricType::BatteryVoltage, metric_data)) {
battery_voltage = metric_data.valueAsUint32; battery_voltage = metric_data.valueAsUint32;
battery_voltage_set = true; battery_voltage_set = true;
} }
@ -72,61 +101,40 @@ static void updateUi(std::shared_ptr<Data> data) {
lvgl::lock(kernel::millisToTicks(1000)); lvgl::lock(kernel::millisToTicks(1000));
if (charging_enabled_set) { if (charging_enabled_set) {
lv_obj_set_state(data->enable_switch, LV_STATE_CHECKED, charging_enabled_and_allowed); lv_obj_set_state(enableSwitch, LV_STATE_CHECKED, charging_enabled_and_allowed);
lv_obj_remove_flag(data->enable_switch, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(enableSwitch, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(data->enable_label, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(enableLabel, LV_OBJ_FLAG_HIDDEN);
} else { } else {
lv_obj_add_flag(data->enable_switch, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(enableSwitch, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(data->enable_label, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(enableLabel, LV_OBJ_FLAG_HIDDEN);
} }
lv_label_set_text_fmt(data->charge_state, "Charging: %s", charge_state); lv_label_set_text_fmt(chargeStateLabel, "Charging: %s", charge_state);
if (battery_voltage_set) { if (battery_voltage_set) {
lv_label_set_text_fmt(data->battery_voltage, "Battery voltage: %lu mV", battery_voltage); lv_label_set_text_fmt(batteryVoltageLabel, "Battery voltage: %lu mV", battery_voltage);
} else { } else {
lv_label_set_text_fmt(data->battery_voltage, "Battery voltage: N/A"); lv_label_set_text_fmt(batteryVoltageLabel, "Battery voltage: N/A");
} }
if (charge_level_scaled_set) { if (charge_level_scaled_set) {
lv_label_set_text_fmt(data->charge_level, "Charge level: %d%%", charge_level); lv_label_set_text_fmt(chargeLevelLabel, "Charge level: %d%%", charge_level);
} else { } else {
lv_label_set_text_fmt(data->charge_level, "Charge level: N/A"); lv_label_set_text_fmt(chargeLevelLabel, "Charge level: N/A");
} }
if (current_set) { if (current_set) {
lv_label_set_text_fmt(data->current, "Current: %ld mAh", current); lv_label_set_text_fmt(currentLabel, "Current: %ld mAh", current);
} else { } else {
lv_label_set_text_fmt(data->current, "Current: N/A"); lv_label_set_text_fmt(currentLabel, "Current: N/A");
} }
lvgl::unlock(); lvgl::unlock();
}
static void onTimer(TT_UNUSED std::shared_ptr<void> context) {
auto data = optData();
if (data != nullptr) {
updateUi(data);
} }
}
static void onPowerEnabledChanged(lv_event_t* event) { public:
lv_event_code_t code = lv_event_get_code(event);
auto* enable_switch = static_cast<lv_obj_t*>(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(); void onShow(AppContext& app, lv_obj_t* parent) override {
if (data != nullptr) {
if (data->power->isAllowedToCharge() != is_on) {
data->power->setAllowedToCharge(is_on);
updateUi(data);
}
}
}
}
static void onShow(AppContext& app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); lvgl::toolbar_create(parent, app);
@ -137,8 +145,6 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lv_obj_set_flex_grow(wrapper, 1); lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
auto data = std::static_pointer_cast<Data>(app.getData());
// Top row: enable/disable // Top row: enable/disable
lv_obj_t* switch_container = lv_obj_create(wrapper); lv_obj_t* switch_container = lv_obj_create(wrapper);
lv_obj_set_width(switch_container, LV_PCT(100)); lv_obj_set_width(switch_container, LV_PCT(100));
@ -146,43 +152,36 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lvgl::obj_set_style_no_padding(switch_container); lvgl::obj_set_style_no_padding(switch_container);
lvgl::obj_set_style_bg_invisible(switch_container); lvgl::obj_set_style_bg_invisible(switch_container);
data->enable_label = lv_label_create(switch_container); enableLabel = lv_label_create(switch_container);
lv_label_set_text(data->enable_label, "Charging enabled"); lv_label_set_text(enableLabel, "Charging enabled");
lv_obj_set_align(data->enable_label, LV_ALIGN_LEFT_MID); lv_obj_set_align(enableLabel, LV_ALIGN_LEFT_MID);
lv_obj_t* enable_switch = lv_switch_create(switch_container); 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_add_event_cb(enable_switch, onPowerEnabledChangedCallback, LV_EVENT_VALUE_CHANGED, this);
lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID); lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID);
data->enable_switch = enable_switch; enableSwitch = enable_switch;
data->charge_state = lv_label_create(wrapper); chargeStateLabel = lv_label_create(wrapper);
data->charge_level = lv_label_create(wrapper); chargeLevelLabel = lv_label_create(wrapper);
data->battery_voltage = lv_label_create(wrapper); batteryVoltageLabel = lv_label_create(wrapper);
data->current = lv_label_create(wrapper); currentLabel = lv_label_create(wrapper);
updateUi(data); updateUi();
data->update_timer.start(kernel::millisToTicks(1000));
}
static void onHide(TT_UNUSED AppContext& app) { update_timer.start(kernel::millisToTicks(1000));
auto data = std::static_pointer_cast<Data>(app.getData()); }
data->update_timer.stop();
}
static void onStart(AppContext& app) { void onHide(TT_UNUSED AppContext& app) override {
auto data = std::make_shared<Data>(); update_timer.stop();
app.setData(data); }
assert(data->power != nullptr); // The Power app only shows up on supported devices };
}
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Power", .id = "Power",
.name = "Power", .name = "Power",
.icon = TT_ASSETS_APP_ICON_POWER_SETTINGS, .icon = TT_ASSETS_APP_ICON_POWER_SETTINGS,
.type = TypeSettings, .type = Type::Settings,
.onStart = onStart, .createApp = create<PowerApp>
.onShow = onShow,
.onHide = onHide
}; };
} // namespace } // namespace

View File

@ -1,29 +1,289 @@
#include "TactilityConfig.h" #include "TactilityConfig.h"
#include <Timer.h>
#include <kernel/Kernel.h>
#if TT_FEATURE_SCREENSHOT_ENABLED #if TT_FEATURE_SCREENSHOT_ENABLED
#include "app/screenshot/ScreenshotUi.h" #include "TactilityHeadless.h"
#include <memory> #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 { namespace tt::app::screenshot {
static void onShow(AppContext& app, lv_obj_t* parent) { extern const AppManifest manifest;
auto ui = std::static_pointer_cast<ScreenshotUi>(app.getData());
ui->createWidgets(app, parent); 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<Timer> 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<ScreenshotApp> _Nullable optApp() {
auto appContext = service::loader::getCurrentAppContext();
if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
return std::static_pointer_cast<ScreenshotApp>(appContext->getApp());
} else {
return nullptr;
}
} }
static void onStart(AppContext& app) { static void onStartPressedCallback(TT_UNUSED lv_event_t* event) {
auto ui = std::make_shared<ScreenshotUi>(); auto app = optApp();
app.setData(ui); // Ensure data gets deleted when no more in use 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<void> context) {
auto app = optApp();
if (app != nullptr) {
app->onTimerTick();
}
}
ScreenshotApp::ScreenshotApp() {
updateTimer = std::make_unique<Timer>(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 = { extern const AppManifest manifest = {
.id = "Screenshot", .id = "Screenshot",
.name = "Screenshot", .name = "Screenshot",
.icon = LV_SYMBOL_IMAGE, .icon = LV_SYMBOL_IMAGE,
.type = TypeSystem, .type = Type::System,
.onStart = onStart, .createApp = create<ScreenshotApp>
.onShow = onShow,
}; };
} // namespace } // namespace

View File

@ -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<ScreenshotUi> _Nullable optScreenshotUi() {
app::AppContext* app = service::loader::getCurrentApp();
if (app->getManifest().id == manifest.id) {
return std::static_pointer_cast<ScreenshotUi>(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<void> context) {
auto screenshot_ui = optScreenshotUi();
if (screenshot_ui != nullptr) {
screenshot_ui->onTimerTick();
}
}
ScreenshotUi::ScreenshotUi() {
updateTimer = std::make_unique<Timer>(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

View File

@ -32,10 +32,6 @@ int32_t getResultIndex(const Bundle& bundle) {
return index; return index;
} }
void setResultIndex(std::shared_ptr<Bundle> bundle, int32_t index) {
bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, index);
}
static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) { static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
std::string result; std::string result;
if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) { if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) {
@ -45,23 +41,35 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
} }
} }
static void onListItemSelected(lv_event_t* e) { class SelectionDialogApp : public App {
private:
static void onListItemSelectedCallback(lv_event_t* e) {
auto appContext = service::loader::getCurrentAppContext();
tt_assert(appContext != nullptr);
auto app = std::static_pointer_cast<SelectionDialogApp>(appContext->getApp());
app->onListItemSelected(e);
}
void onListItemSelected(lv_event_t* e) {
size_t index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e)); size_t index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e));
TT_LOG_I(TAG, "Selected item at index %d", index); TT_LOG_I(TAG, "Selected item at index %d", index);
tt::app::AppContext* app = service::loader::getCurrentApp(); auto bundle = std::make_unique<Bundle>();
auto bundle = std::make_shared<Bundle>(); bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index);
setResultIndex(bundle, (int32_t)index); setResult(app::Result::Ok, std::move(bundle));
app->setResult(app::ResultOk, bundle);
service::loader::stopApp(); service::loader::stopApp();
} }
static void createChoiceItem(void* parent, const std::string& title, size_t index) { static void createChoiceItem(void* parent, const std::string& title, size_t index) {
auto* list = static_cast<lv_obj_t*>(parent); auto* list = static_cast<lv_obj_t*>(parent);
lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str()); 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); lv_obj_add_event_cb(btn, onListItemSelectedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
} }
static void onShow(AppContext& app, lv_obj_t* parent) { public:
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
std::string title = getTitleParameter(app.getParameters()); std::string title = getTitleParameter(app.getParameters());
lvgl::toolbar_create(parent, title); lvgl::toolbar_create(parent, title);
@ -77,12 +85,12 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
std::vector<std::string> items = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN); std::vector<std::string> items = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN);
if (items.empty() || items.front().empty()) { if (items.empty() || items.front().empty()) {
TT_LOG_E(TAG, "No items provided"); TT_LOG_E(TAG, "No items provided");
app.setResult(ResultError); setResult(Result::Error);
service::loader::stopApp(); service::loader::stopApp();
} else if (items.size() == 1) { } else if (items.size() == 1) {
auto result_bundle = std::make_shared<Bundle>(); auto result_bundle = std::make_unique<Bundle>();
setResultIndex(result_bundle, 0); result_bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, 0);
app.setResult(ResultOk, result_bundle); setResult(Result::Ok, std::move(result_bundle));
service::loader::stopApp(); service::loader::stopApp();
TT_LOG_W(TAG, "Auto-selecting single item"); TT_LOG_W(TAG, "Auto-selecting single item");
} else { } else {
@ -93,16 +101,17 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
} }
} else { } else {
TT_LOG_E(TAG, "No items provided"); TT_LOG_E(TAG, "No items provided");
app.setResult(ResultError); setResult(Result::Error);
service::loader::stopApp(); service::loader::stopApp();
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "SelectionDialog", .id = "SelectionDialog",
.name = "Selection Dialog", .name = "Selection Dialog",
.type = TypeHidden, .type = Type::Hidden,
.onShow = onShow .createApp = create<SelectionDialogApp>
}; };
} }

View File

@ -13,15 +13,17 @@ static void onAppPressed(lv_event_t* e) {
service::loader::startApp(manifest->id); service::loader::startApp(manifest->id);
} }
static void createWidget(const AppManifest* manifest, void* parent) { static void createWidget(const std::shared_ptr<AppManifest>& manifest, void* parent) {
tt_check(parent); tt_check(parent);
auto* list = (lv_obj_t*)parent; auto* list = (lv_obj_t*)parent;
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; 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_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) { class SettingsApp : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); lvgl::toolbar_create(parent, app);
@ -33,18 +35,19 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
auto manifests = getApps(); auto manifests = getApps();
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName); std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
for (const auto& manifest: manifests) { for (const auto& manifest: manifests) {
if (manifest->type == TypeSettings) { if (manifest->type == Type::Settings) {
createWidget(manifest, list); createWidget(manifest, list);
} }
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Settings", .id = "Settings",
.name = "Settings", .name = "Settings",
.icon = TT_ASSETS_APP_ICON_SETTINGS, .icon = TT_ASSETS_APP_ICON_SETTINGS,
.type = TypeHidden, .type = Type::Hidden,
.onShow = onShow, .createApp = create<SettingsApp>
}; };
} // namespace } // namespace

View File

@ -108,7 +108,9 @@ static void addRtosTasks(lv_obj_t* parent) {
#endif #endif
static void onShow(AppContext& app, lv_obj_t* parent) { class SystemInfoApp : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); lvgl::toolbar_create(parent, app);
@ -149,16 +151,15 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lv_obj_t* esp_idf_version = lv_label_create(build_info_wrapper); 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_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 #endif
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "SystemInfo", .id = "SystemInfo",
.name = "System Info", .name = "System Info",
.icon = TT_ASSETS_APP_ICON_SYSTEM_INFO, .icon = TT_ASSETS_APP_ICON_SYSTEM_INFO,
.type = TypeSystem, .type = Type::System,
.onStart = nullptr, .createApp = create<SystemInfoApp>
.onStop = nullptr,
.onShow = onShow
}; };
} // namespace } // namespace

View File

@ -11,7 +11,9 @@
namespace tt::app::textviewer { namespace tt::app::textviewer {
static void onShow(AppContext& app, lv_obj_t* parent) { class TextViewerApp : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); lvgl::toolbar_create(parent, app);
@ -38,13 +40,14 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
if (!success) { if (!success) {
lv_label_set_text_fmt(label, "Failed to load %s", file_argument.c_str()); lv_label_set_text_fmt(label, "Failed to load %s", file_argument.c_str());
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "TextViewer", .id = "TextViewer",
.name = "Text Viewer", .name = "Text Viewer",
.type = TypeHidden, .type = Type::Hidden,
.onShow = onShow .createApp = create<TextViewerApp>
}; };
void start(const std::string& file) { void start(const std::string& file) {

View File

@ -13,34 +13,26 @@ namespace tt::app::timedatesettings {
extern const AppManifest manifest; extern const AppManifest manifest;
struct Data { class TimeDateSettingsApp : public App {
private:
Mutex mutex = Mutex(Mutex::Type::Recursive); Mutex mutex = Mutex(Mutex::Type::Recursive);
lv_obj_t* regionLabelWidget = nullptr; 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. */ static void onConfigureTimeZonePressed(TT_UNUSED lv_event_t* event) {
std::shared_ptr<Data> _Nullable optData() {
app::AppContext* app = service::loader::getCurrentApp();
if (app->getManifest().id == manifest.id) {
return std::static_pointer_cast<Data>(app->getData());
} else {
return nullptr;
}
}
static void onConfigureTimeZonePressed(TT_UNUSED lv_event_t* event) {
timezone::start(); timezone::start();
} }
static void onTimeFormatChanged(lv_event_t* event) { static void onTimeFormatChanged(lv_event_t* event) {
auto* widget = lv_event_get_target_obj(event); auto* widget = lv_event_get_target_obj(event);
bool show_24 = lv_obj_has_state(widget, LV_STATE_CHECKED); bool show_24 = lv_obj_has_state(widget, LV_STATE_CHECKED);
time::setTimeFormat24Hour(show_24); time::setTimeFormat24Hour(show_24);
} }
static void onShow(AppContext& app, lv_obj_t* parent) { public:
auto data = std::static_pointer_cast<Data>(app.getData());
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); lvgl::toolbar_create(parent, app);
@ -65,7 +57,7 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
if (timeZoneName.empty()) { if (timeZoneName.empty()) {
timeZoneName = "not set"; timeZoneName = "not set";
} }
data->regionLabelWidget = region_label; regionLabelWidget = region_label;
lv_label_set_text(region_label, timeZoneName.c_str()); lv_label_set_text(region_label, timeZoneName.c_str());
// TODO: Find out why Y offset is needed // TODO: Find out why Y offset is needed
lv_obj_align_to(region_label, region_prefix_label, LV_ALIGN_OUT_RIGHT_MID, 0, 8); lv_obj_align_to(region_label, region_prefix_label, LV_ALIGN_OUT_RIGHT_MID, 0, 8);
@ -76,7 +68,7 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lv_obj_add_event_cb(region_button, onConfigureTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr); lv_obj_add_event_cb(region_button, onConfigureTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr);
lv_image_set_src(region_button_image, LV_SYMBOL_SETTINGS); lv_image_set_src(region_button_image, LV_SYMBOL_SETTINGS);
auto* time_format_wrapper= lv_obj_create(main_wrapper); auto* time_format_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_width(time_format_wrapper, LV_PCT(100)); lv_obj_set_width(time_format_wrapper, LV_PCT(100));
lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT); 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_pad_all(time_format_wrapper, 0, 0);
@ -94,38 +86,31 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
} else { } else {
lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED); lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED);
} }
} }
static void onStart(AppContext& app) { void onResult(AppContext& app, Result result, std::unique_ptr<Bundle> bundle) override {
auto data = std::make_shared<Data>(); if (result == Result::Ok) {
app.setData(data); auto name = timezone::getResultName(*bundle);
} auto code = timezone::getResultCode(*bundle);
static void onResult(AppContext& app, Result result, const Bundle& bundle) {
if (result == ResultOk) {
auto data = std::static_pointer_cast<Data>(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()); TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str());
time::setTimeZone(name, code); time::setTimeZone(name, code);
if (!name.empty()) { if (!name.empty()) {
if (lvgl::lock(100 / portTICK_PERIOD_MS)) { if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
lv_label_set_text(data->regionLabelWidget, name.c_str()); lv_label_set_text(regionLabelWidget, name.c_str());
lvgl::unlock(); lvgl::unlock();
} }
} }
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "TimeDateSettings", .id = "TimeDateSettings",
.name = "Time & Date", .name = "Time & Date",
.icon = TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS, .icon = TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS,
.type = TypeSettings, .type = Type::Settings,
.onStart = onStart, .createApp = create<TimeDateSettingsApp>
.onShow = onShow,
.onResult = onResult
}; };
void start() { void start() {

View File

@ -26,16 +26,6 @@ struct TimeZoneEntry {
std::string code; std::string code;
}; };
struct Data {
Mutex mutex;
std::vector<TimeZoneEntry> entries;
std::unique_ptr<Timer> updateTimer;
lv_obj_t* listWidget = nullptr;
lv_obj_t* filterTextareaWidget = nullptr;
};
static void updateList(std::shared_ptr<Data>& data);
static bool parseEntry(const std::string& input, std::string& outName, std::string& outCode) { static bool parseEntry(const std::string& input, std::string& outName, std::string& outCode) {
std::string partial_strip = input.substr(1, input.size() - 3); std::string partial_strip = input.substr(1, input.size() - 3);
auto first_end_quote = partial_strip.find('"'); auto first_end_quote = partial_strip.find('"');
@ -62,59 +52,81 @@ std::string getResultCode(const Bundle& bundle) {
return result; return result;
} }
void setResultName(std::shared_ptr<Bundle>& bundle, const std::string& name) { void setResultName(Bundle& bundle, const std::string& name) {
bundle->putString(RESULT_BUNDLE_NAME_INDEX, name); bundle.putString(RESULT_BUNDLE_NAME_INDEX, name);
} }
void setResultCode(std::shared_ptr<Bundle>& bundle, const std::string& code) { void setResultCode(Bundle& bundle, const std::string& code) {
bundle->putString(RESULT_BUNDLE_CODE_INDEX, code); bundle.putString(RESULT_BUNDLE_CODE_INDEX, code);
} }
// endregion // endregion
static void onUpdateTimer(std::shared_ptr<void> context) {
auto data = std::static_pointer_cast<Data>(context);
updateList(data);
}
static void onTextareaValueChanged(TT_UNUSED lv_event_t* e) { class TimeZoneApp : public App {
auto* app = service::loader::getCurrentApp();
auto app_data = app->getData();
auto data = std::static_pointer_cast<Data>(app_data);
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) { private:
if (data->updateTimer->isRunning()) {
data->updateTimer->stop(); Mutex mutex;
std::vector<TimeZoneEntry> entries;
std::unique_ptr<Timer> updateTimer;
lv_obj_t* listWidget = nullptr;
lv_obj_t* filterTextareaWidget = nullptr;
static void onTextareaValueChangedCallback(TT_UNUSED lv_event_t* e) {
auto* app = (TimeZoneApp*)lv_event_get_user_data(e);
app->onTextareaValueChanged(e);
} }
data->updateTimer->start(500 / portTICK_PERIOD_MS); void onTextareaValueChanged(TT_UNUSED lv_event_t* e) {
if (mutex.lock(100 / portTICK_PERIOD_MS)) {
data->mutex.unlock(); if (updateTimer->isRunning()) {
updateTimer->stop();
} }
}
static void onListItemSelected(lv_event_t* e) { updateTimer->start(500 / portTICK_PERIOD_MS);
mutex.unlock();
}
}
static void onListItemSelectedCallback(lv_event_t* e) {
auto index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e)); auto index = reinterpret_cast<std::size_t>(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<TimeZoneApp>(appContext->getApp());
app->onListItemSelected(index);
}
}
void onListItemSelected(std::size_t index) {
TT_LOG_I(TAG, "Selected item at index %zu", index); TT_LOG_I(TAG, "Selected item at index %zu", index);
auto* app = service::loader::getCurrentApp();
auto data = std::static_pointer_cast<Data>(app->getData());
auto& entry = data->entries[index]; auto& entry = entries[index];
auto bundle = std::make_shared<Bundle>(); auto bundle = std::make_unique<Bundle>();
setResultName(bundle, entry.name); setResultName(*bundle, entry.name);
setResultCode(bundle, entry.code); setResultCode(*bundle, entry.code);
app->setResult(app::ResultOk, bundle);
setResult(app::Result::Ok, std::move(bundle));
service::loader::stopApp(); service::loader::stopApp();
} }
static void createListItem(lv_obj_t* list, const std::string& title, size_t index) { 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_t* btn = lv_list_add_button(list, nullptr, title.c_str());
lv_obj_add_event_cb(btn, &onListItemSelected, LV_EVENT_SHORT_CLICKED, (void*)index); lv_obj_add_event_cb(btn, &onListItemSelectedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
} }
static void readTimeZones(const std::shared_ptr<Data>& data, std::string filter) { static void updateTimerCallback(std::shared_ptr<void> context) {
auto appContext = service::loader::getCurrentAppContext();
if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
auto app = std::static_pointer_cast<TimeZoneApp>(appContext->getApp());
app->updateList();
}
}
void readTimeZones(std::string filter) {
auto path = std::string(MOUNT_POINT_SYSTEM) + "/timezones.csv"; auto path = std::string(MOUNT_POINT_SYSTEM) + "/timezones.csv";
auto* file = fopen(path.c_str(), "rb"); auto* file = fopen(path.c_str(), "rb");
if (file == nullptr) { if (file == nullptr) {
@ -125,15 +137,12 @@ static void readTimeZones(const std::shared_ptr<Data>& data, std::string filter)
std::string name; std::string name;
std::string code; std::string code;
uint32_t count = 0; uint32_t count = 0;
std::vector<TimeZoneEntry> entries; std::vector<TimeZoneEntry> new_entries;
while (fgets(line, 96, file)) { while (fgets(line, 96, file)) {
if (parseEntry(line, name, code)) { if (parseEntry(line, name, code)) {
if (tt::string::lowercase(name).find(filter) != std::string::npos) { if (tt::string::lowercase(name).find(filter) != std::string::npos) {
count++; count++;
entries.push_back({ new_entries.push_back({.name = name, .code = code});
.name = name,
.code = code
});
// Safety guard // Safety guard
if (count > 50) { if (count > 50) {
@ -148,20 +157,20 @@ static void readTimeZones(const std::shared_ptr<Data>& data, std::string filter)
fclose(file); fclose(file);
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) { if (mutex.lock(100 / portTICK_PERIOD_MS)) {
data->entries = std::move(entries); entries = std::move(new_entries);
data->mutex.unlock(); mutex.unlock();
} else { } else {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED); TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
} }
TT_LOG_I(TAG, "Processed %lu entries", count); TT_LOG_I(TAG, "Processed %lu entries", count);
} }
static void updateList(std::shared_ptr<Data>& data) { void updateList() {
if (lvgl::lock(100 / portTICK_PERIOD_MS)) { if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
std::string filter = tt::string::lowercase(std::string(lv_textarea_get_text(data->filterTextareaWidget))); std::string filter = tt::string::lowercase(std::string(lv_textarea_get_text(filterTextareaWidget)));
readTimeZones(data, filter); readTimeZones(filter);
lvgl::unlock(); lvgl::unlock();
} else { } else {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL"); TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
@ -169,25 +178,25 @@ static void updateList(std::shared_ptr<Data>& data) {
} }
if (lvgl::lock(100 / portTICK_PERIOD_MS)) { if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) { if (mutex.lock(100 / portTICK_PERIOD_MS)) {
lv_obj_clean(data->listWidget); lv_obj_clean(listWidget);
uint32_t index = 0; uint32_t index = 0;
for (auto& entry : data->entries) { for (auto& entry : entries) {
createListItem(data->listWidget, entry.name, index); createListItem(listWidget, entry.name, index);
index++; index++;
} }
data->mutex.unlock(); mutex.unlock();
} }
lvgl::unlock(); lvgl::unlock();
} }
} }
static void onShow(AppContext& app, lv_obj_t* parent) { public:
auto data = std::static_pointer_cast<Data>(app.getData());
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); lvgl::toolbar_create(parent, app);
@ -210,8 +219,8 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
auto* textarea = lv_textarea_create(search_wrapper); auto* textarea = lv_textarea_create(search_wrapper);
lv_textarea_set_placeholder_text(textarea, "e.g. Europe/Amsterdam"); lv_textarea_set_placeholder_text(textarea, "e.g. Europe/Amsterdam");
lv_textarea_set_one_line(textarea, true); lv_textarea_set_one_line(textarea, true);
lv_obj_add_event_cb(textarea, onTextareaValueChanged, LV_EVENT_VALUE_CHANGED, nullptr); lv_obj_add_event_cb(textarea, onTextareaValueChangedCallback, LV_EVENT_VALUE_CHANGED, this);
data->filterTextareaWidget = textarea; filterTextareaWidget = textarea;
lv_obj_set_flex_grow(textarea, 1); lv_obj_set_flex_grow(textarea, 1);
service::gui::keyboardAddTextArea(textarea); service::gui::keyboardAddTextArea(textarea);
@ -219,21 +228,19 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lv_obj_set_width(list, LV_PCT(100)); lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1); lv_obj_set_flex_grow(list, 1);
lv_obj_set_style_border_width(list, 0, 0); lv_obj_set_style_border_width(list, 0, 0);
data->listWidget = list; listWidget = list;
} }
static void onStart(AppContext& app) { void onStart(AppContext& app) override {
auto data = std::make_shared<Data>(); updateTimer = std::make_unique<Timer>(Timer::Type::Once, updateTimerCallback, nullptr);
data->updateTimer = std::make_unique<Timer>(Timer::Type::Once, onUpdateTimer, data); }
app.setData(data); };
}
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "TimeZone", .id = "TimeZone",
.name = "Select timezone", .name = "Select timezone",
.type = TypeHidden, .type = Type::Hidden,
.onStart = onStart, .createApp = create<TimeZoneApp>
.onShow = onShow,
}; };
void start() { void start() {

View File

@ -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.h"
#include "lvgl/Toolbar.h" #include "lvgl/Toolbar.h"
#include "hal/usb/Usb.h"
#define TAG "usb_settings" #define TAG "usb_settings"
@ -10,7 +13,9 @@ static void onRebootMassStorage(TT_UNUSED lv_event_t* event) {
hal::usb::rebootIntoMassStorageSdmmc(); hal::usb::rebootIntoMassStorageSdmmc();
} }
static void onShow(AppContext& app, lv_obj_t* parent) { class UsbSettingsApp : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
auto* toolbar = lvgl::toolbar_create(parent, app); auto* toolbar = lvgl::toolbar_create(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
@ -31,15 +36,15 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lv_label_set_text(label_b, second); lv_label_set_text(label_b, second);
lv_obj_align_to(label_b, label_a, LV_ALIGN_OUT_BOTTOM_MID, 0, 4); lv_obj_align_to(label_b, label_a, LV_ALIGN_OUT_BOTTOM_MID, 0, 4);
} }
}
} };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "UsbSettings", .id = "UsbSettings",
.name = "USB", .name = "USB",
.icon = LV_SYMBOL_USB, .icon = LV_SYMBOL_USB,
.type = TypeSettings, .type = Type::Settings,
.onShow = onShow .createApp = create<UsbSettingsApp>
}; };
} // namespace } // namespace

View File

@ -15,9 +15,9 @@ namespace tt::app::wifiapsettings {
extern const AppManifest manifest; 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. */ /** 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() { const std::shared_ptr<AppContext> _Nullable optWifiApSettingsApp() {
app::AppContext* app = service::loader::getCurrentApp(); auto app = service::loader::getCurrentAppContext();
if (app->getManifest().id == manifest.id) { if (app != nullptr && app->getManifest().id == manifest.id) {
return app; return app;
} else { } else {
return nullptr; return nullptr;
@ -41,7 +41,7 @@ static void onPressForget(TT_UNUSED lv_event_t* event) {
static void onToggleAutoConnect(lv_event_t* event) { static void onToggleAutoConnect(lv_event_t* event) {
lv_event_code_t code = lv_event_get_code(event); lv_event_code_t code = lv_event_get_code(event);
auto* app = optWifiApSettingsApp(); auto app = optWifiApSettingsApp();
if (app == nullptr) { if (app == nullptr) {
return; return;
} }
@ -66,7 +66,9 @@ static void onToggleAutoConnect(lv_event_t* event) {
} }
} }
static void onShow(AppContext& app, lv_obj_t* parent) { class WifiApSettings : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
auto paremeters = app.getParameters(); auto paremeters = app.getParameters();
tt_check(paremeters != nullptr, "Parameters missing"); tt_check(paremeters != nullptr, "Parameters missing");
std::string ssid = paremeters->getString("ssid"); std::string ssid = paremeters->getString("ssid");
@ -117,12 +119,12 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lv_obj_add_flag(forget_button, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(forget_button, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(auto_connect_wrapper, 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) { void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED Result result, std::unique_ptr<Bundle> bundle) override {
auto index = alertdialog::getResultIndex(bundle); auto index = alertdialog::getResultIndex(*bundle);
if (index == 0) {// Yes if (index == 0) { // Yes
auto* app = optWifiApSettingsApp(); auto app = optWifiApSettingsApp();
if (app == nullptr) { if (app == nullptr) {
return; return;
} }
@ -147,15 +149,15 @@ void onResult(TT_UNUSED AppContext& app, TT_UNUSED Result result, const Bundle&
TT_LOG_E(TAG, "Failed to remove SSID"); TT_LOG_E(TAG, "Failed to remove SSID");
} }
} }
} }
};
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "WifiApSettings", .id = "WifiApSettings",
.name = "Wi-Fi AP Settings", .name = "Wi-Fi AP Settings",
.icon = LV_SYMBOL_WIFI, .icon = LV_SYMBOL_WIFI,
.type = TypeHidden, .type = Type::Hidden,
.onShow = onShow, .createApp = create<WifiApSettings>
.onResult = onResult
}; };
} // namespace } // namespace

View File

@ -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. */ /** 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<WifiConnect> _Nullable optWifiConnect() { std::shared_ptr<WifiConnect> _Nullable optWifiConnect() {
app::AppContext* app = service::loader::getCurrentApp(); auto appContext = service::loader::getCurrentAppContext();
if (app->getManifest().id == manifest.id) { if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
return std::static_pointer_cast<WifiConnect>(app->getData()); return std::static_pointer_cast<WifiConnect>(appContext->getApp());
} else { } else {
return nullptr; return nullptr;
} }
@ -105,30 +105,12 @@ void WifiConnect::onHide(TT_UNUSED AppContext& app) {
unlock(); unlock();
} }
static void onShow(AppContext& app, lv_obj_t* parent) {
auto wifi = std::static_pointer_cast<WifiConnect>(app.getData());
wifi->onShow(app, parent);
}
static void onHide(AppContext& app) {
auto wifi = std::static_pointer_cast<WifiConnect>(app.getData());
wifi->onHide(app);
}
static void onStart(AppContext& app) {
auto wifi = std::make_shared<WifiConnect>();
app.setData(wifi);
}
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "WifiConnect", .id = "WifiConnect",
.name = "Wi-Fi Connect", .name = "Wi-Fi Connect",
.icon = LV_SYMBOL_WIFI, .icon = LV_SYMBOL_WIFI,
.type = TypeHidden, .type = Type::Hidden,
.onStart = &onStart, .createApp = create<WifiConnect>
.onShow = &onShow,
.onHide = &onHide
}; };
void start(const std::string& ssid, const std::string& password) { void start(const std::string& ssid, const std::string& password) {

View File

@ -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. */ /** 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<WifiManage> _Nullable optWifiManage() { std::shared_ptr<WifiManage> _Nullable optWifiManage() {
app::AppContext* app = service::loader::getCurrentApp(); auto appContext = service::loader::getCurrentAppContext();
if (app->getManifest().id == manifest.id) { if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
return std::static_pointer_cast<WifiManage>(app->getData()); return std::static_pointer_cast<WifiManage>(appContext->getApp());
} else { } else {
return nullptr; return nullptr;
} }
@ -146,33 +146,12 @@ void WifiManage::onHide(TT_UNUSED AppContext& app) {
unlock(); unlock();
} }
// region Manifest methods
static void onStart(AppContext& app) {
auto wifi = std::make_shared<WifiManage>();
app.setData(wifi);
}
static void onShow(AppContext& app, lv_obj_t* parent) {
auto wifi = std::static_pointer_cast<WifiManage>(app.getData());
wifi->onShow(app, parent);
}
static void onHide(AppContext& app) {
auto wifi = std::static_pointer_cast<WifiManage>(app.getData());
wifi->onHide(app);
}
// endregion
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "WifiManage", .id = "WifiManage",
.name = "Wi-Fi", .name = "Wi-Fi",
.icon = LV_SYMBOL_WIFI, .icon = LV_SYMBOL_WIFI,
.type = TypeSettings, .type = Type::Settings,
.onStart = onStart, .createApp = create<WifiManage>
.onShow = onShow,
.onHide = onHide
}; };
void start() { void start() {

View File

@ -19,9 +19,8 @@ Gui* gui = nullptr;
void onLoaderMessage(const void* message, TT_UNUSED void* context) { void onLoaderMessage(const void* message, TT_UNUSED void* context) {
auto* event = static_cast<const loader::LoaderEvent*>(message); auto* event = static_cast<const loader::LoaderEvent*>(message);
if (event->type == loader::LoaderEventTypeApplicationShowing) { if (event->type == loader::LoaderEventTypeApplicationShowing) {
app::AppContext& app = event->app_showing.app; auto app_instance = service::loader::getCurrentAppContext();
const app::AppManifest& app_manifest = app.getManifest(); showApp(app_instance);
showApp(app, app_manifest.onShow, app_manifest.onHide);
} else if (event->type == loader::LoaderEventTypeApplicationHiding) { } else if (event->type == loader::LoaderEventTypeApplicationHiding) {
hideApp(); hideApp();
} }
@ -94,27 +93,25 @@ void requestDraw() {
thread_flags_set(thread_id, GUI_THREAD_FLAG_DRAW); 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::AppContext> app) {
lock(); lock();
tt_check(gui->appViewPort == nullptr); tt_check(gui->appToRender == nullptr);
gui->appViewPort = view_port_alloc(app, on_show, on_hide); gui->appToRender = std::move(app);
unlock(); unlock();
requestDraw(); requestDraw();
} }
void hideApp() { void hideApp() {
lock(); lock();
ViewPort* view_port = gui->appViewPort; tt_check(gui->appToRender != nullptr);
tt_check(view_port != nullptr);
// We must lock the LVGL port, because the viewport hide callbacks // 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) // might call LVGL APIs (e.g. to remove the keyboard from the screen root)
tt_check(lvgl::lock(configTICK_RATE_HZ)); tt_check(lvgl::lock(configTICK_RATE_HZ));
view_port_hide(view_port); gui->appToRender->getApp()->onHide(*gui->appToRender);
lvgl::unlock(); lvgl::unlock();
view_port_free(view_port); gui->appToRender = nullptr;
gui->appViewPort = nullptr;
unlock(); unlock();
} }

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
#include "app/AppInstance.h"
#include "app/AppContext.h" #include "app/AppContext.h"
#include "ViewPort.h"
namespace tt::service::gui { 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. * Set the app viewport in the gui state and request the gui to draw it.
* @param[in] app * @param[in] app
* @param[in] onShow
* @param[in] onHide
*/ */
void showApp(app::AppContext& app, ViewPortShowCallback onShow, ViewPortHideCallback onHide); void showApp(std::shared_ptr<app::AppContext> app);
/** /**
* Hide the current app's viewport. * Hide the current app's viewport.

View File

@ -9,9 +9,10 @@ namespace tt::service::gui {
#define TAG "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_send_event(gui->statusbarWidget, LV_EVENT_DRAW_MAIN, nullptr);
lv_obj_t* child_container = lv_obj_create(parent); 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_width(child_container, LV_PCT(100));
lv_obj_set_flex_grow(child_container, 1); lv_obj_set_flex_grow(child_container, 1);
@ -34,19 +35,17 @@ void redraw(Gui* gui) {
if (lvgl::lock(1000)) { if (lvgl::lock(1000)) {
lv_obj_clean(gui->appRootWidget); lv_obj_clean(gui->appRootWidget);
ViewPort* view_port = gui->appViewPort; if (gui->appToRender != nullptr) {
if (view_port != nullptr) {
app::AppContext& app = view_port->app;
app::Flags flags = app.getFlags(); app::Flags flags = std::static_pointer_cast<app::AppInstance>(gui->appToRender)->getFlags();
if (flags.showStatusbar) { if (flags.showStatusbar) {
lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN);
} else { } else {
lv_obj_add_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN);
} }
lv_obj_t* container = createAppViews(gui, gui->appRootWidget, app); lv_obj_t* container = createAppViews(gui, gui->appRootWidget);
view_port_show(view_port, container); gui->appToRender->getApp()->onShow(*gui->appToRender, container);
} else { } else {
TT_LOG_W(TAG, "nothing to draw"); TT_LOG_W(TAG, "nothing to draw");
} }

View File

@ -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

View File

@ -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

View File

@ -6,8 +6,9 @@
#include "RtosCompat.h" #include "RtosCompat.h"
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include "esp_heap_caps.h"
#include "TactilityHeadless.h" #include "TactilityHeadless.h"
#include "app/ElfApp.h"
#include "esp_heap_caps.h"
#else #else
#include "lvgl/LvglSync.h" #include "lvgl/LvglSync.h"
@ -61,17 +62,22 @@ void stopApp() {
loader_singleton->dispatcherThread->dispatch(onStopAppMessage, nullptr); loader_singleton->dispatcherThread->dispatch(onStopAppMessage, nullptr);
} }
app::AppContext* _Nullable getCurrentApp() { std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext() {
tt_assert(loader_singleton); tt_assert(loader_singleton);
if (loader_singleton->mutex.lock(10 / portTICK_PERIOD_MS)) { 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(); loader_singleton->mutex.unlock();
return dynamic_cast<app::AppContext*>(app); return std::move(app);
} else { } else {
return nullptr; return nullptr;
} }
} }
std::shared_ptr<app::App> _Nullable getCurrentApp() {
auto app_context = getCurrentAppContext();
return app_context != nullptr ? app_context->getApp() : nullptr;
}
std::shared_ptr<PubSub> getPubsub() { std::shared_ptr<PubSub> getPubsub() {
tt_assert(loader_singleton); tt_assert(loader_singleton);
// it's safe to return pubsub without locking // 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) { static void transitionAppToState(std::shared_ptr<app::AppInstance> app, app::State state) {
const app::AppManifest& manifest = app.getManifest(); const app::AppManifest& manifest = app->getManifest();
const app::State old_state = app.getState(); const app::State old_state = app->getState();
TT_LOG_I( TT_LOG_I(
TAG, TAG,
@ -111,49 +117,35 @@ static void transitionAppToState(app::AppInstance& app, app::State state) {
switch (state) { switch (state) {
case app::StateInitial: case app::StateInitial:
app.setState(app::StateInitial); app->setState(app::StateInitial);
break; break;
case app::StateStarted: case app::StateStarted:
if (manifest.onStart != nullptr) { app->getApp()->onStart(*app);
manifest.onStart(app); app->setState(app::StateStarted);
}
app.setState(app::StateStarted);
break; break;
case app::StateShowing: { case app::StateShowing: {
LoaderEvent event_showing = { LoaderEvent event_showing = { .type = LoaderEventTypeApplicationShowing };
.type = LoaderEventTypeApplicationShowing,
.app_showing = {
.app = app
}
};
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_showing); tt_pubsub_publish(loader_singleton->pubsubExternal, &event_showing);
app.setState(app::StateShowing); app->setState(app::StateShowing);
break; break;
} }
case app::StateHiding: { case app::StateHiding: {
LoaderEvent event_hiding = { LoaderEvent event_hiding = { .type = LoaderEventTypeApplicationHiding };
.type = LoaderEventTypeApplicationHiding,
.app_hiding = {
.app = app
}
};
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_hiding); tt_pubsub_publish(loader_singleton->pubsubExternal, &event_hiding);
app.setState(app::StateHiding); app->setState(app::StateHiding);
break; break;
} }
case app::StateStopped: case app::StateStopped:
if (manifest.onStop) { // TODO: Verify manifest
manifest.onStop(app); app->getApp()->onStop(*app);
} app->setState(app::StateStopped);
app.setData(nullptr);
app.setState(app::StateStopped);
break; break;
} }
} }
static LoaderStatus startAppWithManifestInternal( static LoaderStatus startAppWithManifestInternal(
const app::AppManifest* manifest, const std::shared_ptr<app::AppManifest>& manifest,
std::shared_ptr<const Bundle> _Nullable parameters const std::shared_ptr<const Bundle> _Nullable& parameters
) { ) {
tt_check(loader_singleton != nullptr); 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 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<app::AppInstance>(manifest, parameters);
new_app->mutableFlags().showStatusbar = (manifest->type != app::Type::Boot);
loader_singleton->appStack.push(new_app); loader_singleton->appStack.push(new_app);
transitionAppToState(*new_app, app::StateInitial); transitionAppToState(new_app, app::StateInitial);
transitionAppToState(*new_app, app::StateStarted); transitionAppToState(new_app, app::StateStarted);
// We might have to hide the previous app first // We might have to hide the previous app first
if (previous_app != nullptr) { 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}; LoaderEvent event_external = { .type = LoaderEventTypeApplicationStarted };
tt_pubsub_publish(loader_singleton->pubsubInternal, &event_internal);
LoaderEvent event_external = {
.type = LoaderEventTypeApplicationStarted,
.app_started = {
.app = *new_app
}
};
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_external); tt_pubsub_publish(loader_singleton->pubsubExternal, &event_external);
return LoaderStatus::Ok; return LoaderStatus::Ok;
@ -208,7 +194,7 @@ static LoaderStatus startAppInternal(
) { ) {
TT_LOG_I(TAG, "Start by id %s", id.c_str()); 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) { if (manifest == nullptr) {
TT_LOG_E(TAG, "App not found: %s", id.c_str()); TT_LOG_E(TAG, "App not found: %s", id.c_str());
return LoaderStatus::ErrorUnknownApp; return LoaderStatus::ErrorUnknownApp;
@ -233,75 +219,75 @@ static void stopAppInternal() {
} }
// Stop current app // 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"); TT_LOG_E(TAG, "Stop app: can't stop root app");
return; return;
} }
auto result_holder = std::move(app_to_stop->getResult()); bool result_set = false;
app::Result result;
std::unique_ptr<Bundle> 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::StateHiding); transitionAppToState(app_to_stop, app::StateStopped);
transitionAppToState(*app_to_stop, app::StateStopped);
loader_singleton->appStack.pop(); 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 #ifdef ESP_PLATFORM
TT_LOG_I(TAG, "Free heap: %zu", heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); TT_LOG_I(TAG, "Free heap: %zu", heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
#endif #endif
app::AppOnResult on_result = nullptr; std::shared_ptr<app::AppInstance> instance_to_resume;
app::AppInstance* app_to_resume = nullptr;
// If there's a previous app, resume it // If there's a previous app, resume it
if (!loader_singleton->appStack.empty()) { if (!loader_singleton->appStack.empty()) {
app_to_resume = loader_singleton->appStack.top(); instance_to_resume = loader_singleton->appStack.top();
tt_assert(app_to_resume); tt_assert(instance_to_resume);
transitionAppToState(*app_to_resume, app::StateShowing); transitionAppToState(instance_to_resume, app::StateShowing);
on_result = app_to_resume->getManifest().onResult;
} }
// Unlock so that we can send results to app and they can also start/stop new apps while processing these results // Unlock so that we can send results to app and they can also start/stop new apps while processing these results
scoped_lock->unlock(); 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! // 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}; LoaderEvent event_external = { .type = LoaderEventTypeApplicationStopped };
tt_pubsub_publish(loader_singleton->pubsubInternal, &event_internal);
LoaderEvent event_external = {
.type = LoaderEventTypeApplicationStopped,
.app_stopped = {
.manifest = manifest
}
};
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_external); tt_pubsub_publish(loader_singleton->pubsubExternal, &event_external);
if (on_result != nullptr && app_to_resume != nullptr) { if (instance_to_resume != nullptr) {
if (result_holder != nullptr) { if (result_set) {
auto result_bundle = result_holder->resultData.get();
if (result_bundle != nullptr) { if (result_bundle != nullptr) {
on_result( instance_to_resume->getApp()->onResult(
*app_to_resume, *instance_to_resume,
result_holder->result, result,
*result_bundle std::move(result_bundle)
); );
} else { } else {
const Bundle empty_bundle; instance_to_resume->getApp()->onResult(
on_result( *instance_to_resume,
*app_to_resume, result,
result_holder->result, nullptr
empty_bundle
); );
} }
} else { } else {
const Bundle empty_bundle; const Bundle empty_bundle;
on_result( instance_to_resume->getApp()->onResult(
*app_to_resume, *instance_to_resume,
app::ResultCancelled, app::Result::Cancelled,
empty_bundle nullptr
); );
} }
} }

View File

@ -21,8 +21,11 @@ void startApp(const std::string& id, const std::shared_ptr<const Bundle>& _Nulla
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */ /** @brief Stop the currently showing app. Show the previous app if any app was still running. */
void stopApp(); void stopApp();
/** @return the currently running app context (it is only ever null before the splash screen is shown) */
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
/** @return the currently running app (it is only ever null before the splash screen is shown) */ /** @return the currently running app (it is only ever null before the splash screen is shown) */
app::AppContext* _Nullable getCurrentApp(); std::shared_ptr<app::App> _Nullable getCurrentApp();
/** /**
* @brief PubSub for LoaderEvent * @brief PubSub for LoaderEvent

View File

@ -87,9 +87,9 @@ void ScreenshotTask::taskMain() {
} }
} }
} else if (work.type == TASK_WORK_TYPE_APPS) { } else if (work.type == TASK_WORK_TYPE_APPS) {
app::AppContext* _Nullable app = loader::getCurrentApp(); auto appContext = loader::getCurrentAppContext();
if (app) { if (appContext != nullptr) {
const app::AppManifest& manifest = app->getManifest(); const app::AppManifest& manifest = appContext->getManifest();
if (manifest.id != last_app_id) { if (manifest.id != last_app_id) {
kernel::delayMillis(100); kernel::delayMillis(100);
last_app_id = manifest.id; last_app_id = manifest.id;

View File

@ -1,4 +1,5 @@
#include "tt_app_context.h" #include "tt_app_context.h"
#include <app/App.h>
#include <app/AppContext.h> #include <app/AppContext.h>
struct AppContextDataWrapper { struct AppContextDataWrapper {
@ -9,28 +10,17 @@ extern "C" {
#define HANDLE_AS_APP_CONTEXT(handle) ((tt::app::AppContext*)(handle)) #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<AppContextDataWrapper>(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<AppContextDataWrapper>();
wrapper->data = data;
HANDLE_AS_APP_CONTEXT(handle)->setData(std::move(wrapper));
}
BundleHandle _Nullable tt_app_context_get_parameters(AppContextHandle handle) { BundleHandle _Nullable tt_app_context_get_parameters(AppContextHandle handle) {
return (BundleHandle)HANDLE_AS_APP_CONTEXT(handle)->getParameters().get(); return (BundleHandle)HANDLE_AS_APP_CONTEXT(handle)->getParameters().get();
} }
void tt_app_context_set_result(AppContextHandle handle, Result result, BundleHandle _Nullable bundle) { void tt_app_context_set_result(AppContextHandle handle, Result result, BundleHandle _Nullable bundle) {
auto shared_bundle = std::shared_ptr<tt::Bundle>((tt::Bundle*)bundle); auto shared_bundle = std::unique_ptr<tt::Bundle>((tt::Bundle*)bundle);
HANDLE_AS_APP_CONTEXT(handle)->setResult((tt::app::Result)result, std::move(shared_bundle)); HANDLE_AS_APP_CONTEXT(handle)->getApp()->setResult((tt::app::Result)result, std::move(shared_bundle));
} }
bool tt_app_context_has_result(AppContextHandle handle) { bool tt_app_context_has_result(AppContextHandle handle) {
return HANDLE_AS_APP_CONTEXT(handle)->hasResult(); return HANDLE_AS_APP_CONTEXT(handle)->getApp()->hasResult();
} }
} }

View File

@ -11,14 +11,6 @@ typedef void* AppContextHandle;
/** @return the data that was attached to this app context */ /** @return the data that was attached to this app context */
void* _Nullable tt_app_context_get_data(AppContextHandle handle); 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 */ /** @return the bundle that belongs to this application, or null */
BundleHandle _Nullable tt_app_context_get_parameters(AppContextHandle handle); BundleHandle _Nullable tt_app_context_get_parameters(AppContextHandle handle);

View File

@ -1,105 +1,29 @@
#include "tt_app_manifest.h" #include "tt_app_manifest.h"
#include <Check.h> #include <Check.h>
#include <Log.h>
#include <app/ElfApp.h> #include <app/ElfApp.h>
#include <app/AppCompatC.h>
#define TAG "tt_app" #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" { extern "C" {
void tt_set_app_manifest( void tt_app_register(
const char* name, const ExternalAppManifest* manifest
const char* _Nullable icon,
AppOnStart onStart,
AppOnStop _Nullable onStop,
AppOnShow _Nullable onShow,
AppOnHide _Nullable onHide,
AppOnResult _Nullable onResult
) { ) {
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
manifest.name = name; tt_assert((manifest->createData == nullptr) == (manifest->destroyData == nullptr));
manifest.icon = icon ? icon : ""; tt::app::setElfAppManifest(
elfOnStart = onStart; manifest->name,
elfOnStop = onStop; manifest->icon,
elfOnShow = onShow; (tt::app::CreateData)manifest->createData,
elfOnHide = onHide; (tt::app::DestroyData)manifest->destroyData,
elfOnResult = onResult; (tt::app::OnStart)manifest->onStart,
tt::app::setElfAppManifest(manifest); (tt::app::OnStop)manifest->onStop,
(tt::app::OnShow)manifest->onShow,
(tt::app::OnHide)manifest->onHide,
(tt::app::OnResult)manifest->onResult
);
#else #else
tt_crash("TactilityC is intended for PC/Simulator"); tt_crash("TactilityC is intended for PC/Simulator");
#endif #endif

View File

@ -7,39 +7,47 @@
extern "C" { extern "C" {
#endif #endif
/** Important: These values must map to tt::app::Result values exactly */
typedef enum { typedef enum {
AppResultOk, AppResultOk = 0,
AppResultCancelled, AppResultCancelled = 1,
AppResultError AppResultError = 2
} Result; } Result;
typedef void* AppContextHandle; typedef void* AppContextHandle;
typedef void (*AppOnStart)(AppContextHandle app); /** Important: These function types must map to t::app types exactly */
typedef void (*AppOnStop)(AppContextHandle app); typedef void* (*AppCreateData)();
typedef void (*AppOnShow)(AppContextHandle app, lv_obj_t* parent); typedef void (*AppDestroyData)(void* data);
typedef void (*AppOnHide)(AppContextHandle app); typedef void (*AppOnStart)(AppContextHandle app, void* _Nullable data);
typedef void (*AppOnResult)(AppContextHandle app, Result result, BundleHandle resultData); 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);
/** typedef struct {
* This is used to register the manifest of an external app. /** The application's human-readable name */
* @param[in] name the application's human-readable name const char* name;
* @param[in] icon the optional application icon (you can use LV_SYMBOL_* too) /** The application icon (you can use LV_SYMBOL_* too) */
* @param[in] onStart called when the app is launched (started) const char* _Nullable icon;
* @param[in] onStop called when the app is exited (stopped) /** The application can allocate data to re-use later (e.g. struct with state) */
* @param[in] onShow called when the app is about to be shown to the user (app becomes visible) AppCreateData _Nullable createData;
* @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) /** If createData is specified, this one must be specified too */
* @param[in] onResult called when the app receives a result after launching another app AppDestroyData _Nullable destroyData;
*/ /** Called when the app is launched (started) */
void tt_set_app_manifest( AppOnStart _Nullable onStart;
const char* name, /** Called when the app is exited (stopped) */
const char* _Nullable icon, AppOnStop _Nullable onStop;
AppOnStart onStart, /** Called when the app is about to be shown to the user (app becomes visible) */
AppOnStop _Nullable onStop, AppOnShow _Nullable onShow;
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, AppOnHide _Nullable onHide;
AppOnResult _Nullable onResult /** 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 #ifdef __cplusplus
} }

View File

@ -22,8 +22,7 @@ extern "C" {
const struct esp_elfsym elf_symbols[] { const struct esp_elfsym elf_symbols[] {
// Tactility // Tactility
ESP_ELFSYM_EXPORT(tt_app_context_get_data), ESP_ELFSYM_EXPORT(tt_app_register),
ESP_ELFSYM_EXPORT(tt_app_context_set_data),
ESP_ELFSYM_EXPORT(tt_app_context_get_parameters), ESP_ELFSYM_EXPORT(tt_app_context_get_parameters),
ESP_ELFSYM_EXPORT(tt_app_context_set_result), ESP_ELFSYM_EXPORT(tt_app_context_set_result),
ESP_ELFSYM_EXPORT(tt_app_context_has_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_bool),
ESP_ELFSYM_EXPORT(tt_bundle_put_int32), ESP_ELFSYM_EXPORT(tt_bundle_put_int32),
ESP_ELFSYM_EXPORT(tt_bundle_put_string), 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_start),
ESP_ELFSYM_EXPORT(tt_hal_i2c_stop), ESP_ELFSYM_EXPORT(tt_hal_i2c_stop),
ESP_ELFSYM_EXPORT(tt_hal_i2c_is_started), ESP_ELFSYM_EXPORT(tt_hal_i2c_is_started),

View File

@ -14,7 +14,7 @@ void tt_service_loader_stop_app() {
} }
AppContextHandle tt_service_loader_get_current_app() { AppContextHandle tt_service_loader_get_current_app() {
return tt::service::loader::getCurrentApp(); return tt::service::loader::getCurrentAppContext().get();
} }
} }

View File

@ -2,7 +2,14 @@
#include "LogMessages.h" #include "LogMessages.h"
#if CONFIG_SPIRAM_USE_MALLOC == 1 or not defined(ESP_PLATFORM) #ifdef ESP_TARGET
#include <esp_log.h>
#else
#include <cstdarg>
#include <cstdio>
#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_ENTRY_COUNT 200
#define TT_LOG_MESSAGE_SIZE 128 #define TT_LOG_MESSAGE_SIZE 128
#else #else
@ -35,12 +42,6 @@ LogEntry* copyLogEntries(unsigned int& outIndex);
} // namespace tt } // namespace tt
#ifdef ESP_TARGET
#include "esp_log.h"
#else
#include <cstdarg>
#include <cstdio>
#endif
#ifdef ESP_TARGET #ifdef ESP_TARGET

View File

@ -78,4 +78,13 @@ std::string join(const std::vector<std::string>& input, const std::string& delim
return stream.str(); 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 } // namespace

View File

@ -66,5 +66,9 @@ std::basic_string<T> lowercase(const std::basic_string<T>& input) {
return std::move(output); 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 } // namespace

View File

@ -34,7 +34,7 @@ public:
* @param[in] callback The callback function * @param[in] callback The callback function
* @param callbackContext The callback context * @param callbackContext The callback context
*/ */
Timer(Type type, Callback callback, std::shared_ptr<void> callbackContext); Timer(Type type, Callback callback, std::shared_ptr<void> callbackContext = nullptr);
~Timer(); ~Timer();

View File

@ -2,12 +2,9 @@
#include "Timer.h" #include "Timer.h"
#include "service/ServiceContext.h" #include "service/ServiceContext.h"
#include "TactilityCore.h"
#include "TactilityHeadless.h" #include "TactilityHeadless.h"
#include "service/ServiceRegistry.h" #include "service/ServiceRegistry.h"
#include <cstdlib>
#define TAG "sdcard_service" #define TAG "sdcard_service"
namespace tt::service::sdcard { namespace tt::service::sdcard {
@ -28,7 +25,6 @@ struct ServiceData {
} }
}; };
static void onUpdate(std::shared_ptr<void> context) { static void onUpdate(std::shared_ptr<void> context) {
auto sdcard = tt::hal::getConfiguration()->sdcard; auto sdcard = tt::hal::getConfiguration()->sdcard;
if (sdcard == nullptr) { if (sdcard == nullptr) {