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;
lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* label = lv_label_create(parent); class HelloWorldApp : public App {
lv_label_set_text(label, "Hello, world!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}
extern const tt::app::AppManifest hello_world_app = { void onShow(AppContext& context, lv_obj_t* parent) override {
lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* label = lv_label_create(parent);
lv_label_set_text(label, "Hello, world!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}
};
extern const AppManifest hello_world_app = {
.id = "HelloWorld", .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,126 +1,198 @@
#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 {
TT_LOG_I(TAG, "Starting ELF %s", filePath.c_str());
assert(elfFileData == nullptr); private:
size_t size = 0; const std::string filePath;
elfFileData = file::readBinary(filePath, size); std::unique_ptr<uint8_t[]> elfFileData;
if (elfFileData == nullptr) { esp_elf_t elf;
return false; 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());
assert(elfFileData == nullptr);
size_t size = 0;
elfFileData = file::readBinary(filePath, size);
if (elfFileData == nullptr) {
return false;
}
if (esp_elf_init(&elf) < 0) {
TT_LOG_E(TAG, "Failed to initialize");
shouldCleanupElf = true;
return false;
}
if (esp_elf_relocate(&elf, elfFileData.get()) < 0) {
TT_LOG_E(TAG, "Failed to load executable");
return false;
}
int argc = 0;
char* argv[] = {};
if (esp_elf_request(&elf, 0, argc, argv) < 0) {
TT_LOG_W(TAG, "Executable returned error code");
return false;
}
return true;
} }
if (esp_elf_init(&elf) < 0) { void stopElf() {
TT_LOG_E(TAG, "Failed to initialize"); TT_LOG_I(TAG, "Cleaning up ELF");
return false;
if (shouldCleanupElf) {
esp_elf_deinit(&elf);
}
if (elfFileData != nullptr) {
elfFileData = nullptr;
}
} }
if (esp_elf_relocate(&elf, elfFileData.get()) < 0) { public:
TT_LOG_E(TAG, "Failed to load executable");
return false; 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();
}
} }
int argc = 0; void onStop(AppContext& appContext) override {
char* argv[] = {}; TT_LOG_I(TAG, "Cleaning up app");
if (manifest != nullptr) {
if (manifest->onStop != nullptr) {
manifest->onStop(appContext, data);
}
size_t manifest_set_count = elfManifestSetCount; if (manifest->destroyData != nullptr && data != nullptr) {
if (esp_elf_request(&elf, 0, argc, argv) < 0) { manifest->destroyData(data);
TT_LOG_W(TAG, "Executable returned error code"); }
return false;
this->manifest = nullptr;
}
stopElf();
} }
if (elfManifestSetCount > manifest_set_count) { void onShow(AppContext& appContext, lv_obj_t* parent) override {
service::loader::startApp(ELF_WRAPPER_APP_ID); if (manifest != nullptr && manifest->onShow != nullptr) {
} else { manifest->onShow(appContext, data, parent);
TT_LOG_W(TAG, "App did not set manifest to run - cleaning up ELF"); }
esp_elf_deinit(&elf);
elfFileData = nullptr;
} }
return true; void onHide(AppContext& appContext) override {
} if (manifest != nullptr && manifest->onHide != nullptr) {
manifest->onHide(appContext, data);
}
}
static void onStart(AppContext& app) {} void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> resultBundle) override {
static void onStop(AppContext& app) {} if (manifest != nullptr && manifest->onResult != nullptr) {
static void onShow(AppContext& app, lv_obj_t* parent) {} manifest->onResult(appContext, data, result, std::move(resultBundle));
static void onHide(AppContext& app) {} }
static void onResult(AppContext& app, Result result, const Bundle& resultBundle) {} }
AppManifest elfManifest = {
.id = "",
.name = "",
.type = TypeHidden,
.onStart = onStart,
.onStop = onStop,
.onShow = onShow,
.onHide = onHide,
.onResult = onResult
}; };
static void onStartWrapper(AppContext& app) { void setElfAppManifest(
elfManifest.onStart(app); const char* name,
} const char* _Nullable icon,
CreateData _Nullable createData,
static void onStopWrapper(AppContext& app) { DestroyData _Nullable destroyData,
elfManifest.onStop(app); OnStart _Nullable onStart,
TT_LOG_I(TAG, "Cleaning up ELF"); OnStop _Nullable onStop,
esp_elf_deinit(&elf); OnShow _Nullable onShow,
elfFileData = nullptr; OnHide _Nullable onHide,
} OnResult _Nullable onResult
) {
static void onShowWrapper(AppContext& app, lv_obj_t* parent) { elfManifest = ElfManifest {
elfManifest.onShow(app, parent); .name = name ? name : "",
} .icon = icon ? icon : "",
.createData = createData,
static void onHideWrapper(AppContext& app) { .destroyData = destroyData,
elfManifest.onHide(app); .onStart = onStart,
} .onStop = onStop,
.onShow = onShow,
static void onResultWrapper(AppContext& app, Result result, const Bundle& bundle) { .onHide = onHide,
elfManifest.onResult(app, result, bundle); .onResult = onResult
} };
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,78 +43,93 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
} }
} }
static void onButtonClicked(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(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::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) { class AlertDialogApp : public App {
lv_obj_t* button = lv_button_create(parent);
lv_obj_t* button_label = lv_label_create(button);
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(button_label, text.c_str());
lv_obj_add_event_cb(button, &onButtonClicked, LV_EVENT_SHORT_CLICKED, (void*)index);
}
static void onShow(AppContext& app, lv_obj_t* parent) { private:
auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing");
std::string title = getTitleParameter(app.getParameters()); static void onButtonClickedCallback(lv_event_t* e) {
lv_obj_t* toolbar = lvgl::toolbar_create(parent, title); auto appContext = service::loader::getCurrentAppContext();
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); tt_assert(appContext != nullptr);
auto app = std::static_pointer_cast<AlertDialogApp>(appContext->getApp());
lv_obj_t* message_label = lv_label_create(parent); app->onButtonClicked(e);
lv_obj_align(message_label, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_width(message_label, LV_PCT(80));
std::string message;
if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) {
lv_label_set_text(message_label, message.c_str());
lv_label_set_long_mode(message_label, LV_LABEL_LONG_WRAP);
} }
lv_obj_t* button_wrapper = lv_obj_create(parent); void onButtonClicked(lv_event_t* e) {
lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW); lv_event_code_t code = lv_event_get_code(e);
lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT); auto index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e));
lv_obj_set_style_pad_all(button_wrapper, 0, 0); TT_LOG_I(TAG, "Selected item at index %d", index);
lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_border_width(button_wrapper, 0, 0);
lv_obj_align(button_wrapper, LV_ALIGN_BOTTOM_MID, 0, -4);
std::string items_concatenated; auto bundle = std::make_unique<Bundle>();
if (parameters->optString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_concatenated)) { bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index);
std::vector<std::string> labels = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN); setResult(app::Result::Ok, std::move(bundle));
if (labels.empty() || labels.front().empty()) {
TT_LOG_E(TAG, "No items provided"); service::loader::stopApp();
app.setResult(ResultError); }
service::loader::stopApp();
} else if (labels.size() == 1) { static void createButton(lv_obj_t* parent, const std::string& text, size_t index) {
auto result_bundle = std::make_shared<Bundle>(); lv_obj_t* button = lv_button_create(parent);
setResultIndex(result_bundle, 0); lv_obj_t* button_label = lv_label_create(button);
app.setResult(ResultOk, result_bundle); lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
service::loader::stopApp(); lv_label_set_text(button_label, text.c_str());
TT_LOG_W(TAG, "Auto-selecting single item"); lv_obj_add_event_cb(button, onButtonClickedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
} else { }
size_t index = 0; public:
for (const auto& label: labels) {
createButton(button_wrapper, label, index++); void onShow(AppContext& app, lv_obj_t* parent) override {
auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing");
std::string title = getTitleParameter(app.getParameters());
lv_obj_t* toolbar = lvgl::toolbar_create(parent, title);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* message_label = lv_label_create(parent);
lv_obj_align(message_label, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_width(message_label, LV_PCT(80));
std::string message;
if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) {
lv_label_set_text(message_label, message.c_str());
lv_label_set_long_mode(message_label, LV_LABEL_LONG_WRAP);
}
lv_obj_t* button_wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(button_wrapper, 0, 0);
lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_border_width(button_wrapper, 0, 0);
lv_obj_align(button_wrapper, LV_ALIGN_BOTTOM_MID, 0, -4);
std::string items_concatenated;
if (parameters->optString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_concatenated)) {
std::vector<std::string> labels = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN);
if (labels.empty() || labels.front().empty()) {
TT_LOG_E(TAG, "No items provided");
setResult(Result::Error);
service::loader::stopApp();
} else if (labels.size() == 1) {
auto result_bundle = std::make_unique<Bundle>();
result_bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, 0);
setResult(Result::Ok, std::move(result_bundle));
service::loader::stopApp();
TT_LOG_W(TAG, "Auto-selecting single item");
} else {
size_t index = 0;
for (const auto& label: labels) {
createButton(button_wrapper, label, index++);
}
} }
} }
} }
} };
extern const AppManifest manifest = { 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,54 +8,61 @@
namespace tt::app::applist { namespace tt::app::applist {
static void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
service::loader::startApp(manifest->id);
}
static void createAppWidget(const AppManifest* manifest, void* parent) { class AppListApp : public App {
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;
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);
}
static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { private:
auto* toolbar = lvgl::toolbar_create(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* list = lv_list_create(parent); static void onAppPressed(lv_event_t* e) {
lv_obj_set_width(list, LV_PCT(100)); const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
lv_obj_align_to(list, toolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0); service::loader::startApp(manifest->id);
auto toolbar_height = lv_obj_get_height(toolbar);
auto parent_content_height = lv_obj_get_content_height(parent);
lv_obj_set_height(list, parent_content_height - toolbar_height);
auto manifests = getApps();
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
lv_list_add_text(list, "User");
for (const auto& manifest: manifests) {
if (manifest->type == TypeUser) {
createAppWidget(manifest, list);
}
} }
lv_list_add_text(list, "System"); static void createAppWidget(const std::shared_ptr<AppManifest>& manifest, lv_obj_t* list) {
for (const auto& manifest: manifests) { const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
if (manifest->type == TypeSystem) { lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str());
createAppWidget(manifest, list); lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest.get());
}
public:
void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
auto* toolbar = lvgl::toolbar_create(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* list = lv_list_create(parent);
lv_obj_set_width(list, LV_PCT(100));
lv_obj_align_to(list, toolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0);
auto toolbar_height = lv_obj_get_height(toolbar);
auto parent_content_height = lv_obj_get_content_height(parent);
lv_obj_set_height(list, parent_content_height - toolbar_height);
auto manifests = getApps();
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
lv_list_add_text(list, "User");
for (const auto& manifest: manifests) {
if (manifest->type == Type::User) {
createAppWidget(manifest, list);
}
}
lv_list_add_text(list, "System");
for (const auto& manifest: manifests) {
if (manifest->type == Type::System) {
createAppWidget(manifest, list);
}
} }
} }
} };
extern const AppManifest manifest = { 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,101 +24,93 @@
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);
auto* lvgl_display = lv_display_get_default(); auto* lvgl_display = lv_display_get_default();
tt_assert(lvgl_display != nullptr); tt_assert(lvgl_display != nullptr);
auto* hal_display = (hal::Display*)lv_display_get_user_data(lvgl_display); auto* hal_display = (hal::Display*)lv_display_get_user_data(lvgl_display);
tt_assert(hal_display != nullptr); tt_assert(hal_display != nullptr);
if (hal_display->supportsBacklightDuty()) { if (hal_display->supportsBacklightDuty()) {
int32_t backlight_duty = app::display::getBacklightDuty(); int32_t backlight_duty = app::display::getBacklightDuty();
hal_display->setBacklightDuty(backlight_duty); hal_display->setBacklightDuty(backlight_duty);
}
if (hal::usb::isUsbBootMode()) {
TT_LOG_I(TAG, "Rebooting into mass storage device mode");
hal::usb::resetUsbBootMode();
hal::usb::startMassStorageWithSdmmc();
} else {
TickType_t end_time = tt::kernel::getTicks();
TickType_t ticks_passed = end_time - start_time;
TickType_t minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS);
if (minimum_ticks > ticks_passed) {
kernel::delayTicks(minimum_ticks - ticks_passed);
} }
tt::service::loader::stopApp(); if (hal::usb::isUsbBootMode()) {
startNextApp(); TT_LOG_I(TAG, "Rebooting into mass storage device mode");
hal::usb::resetUsbBootMode();
hal::usb::startMassStorageWithSdmmc();
} else {
TickType_t end_time = tt::kernel::getTicks();
TickType_t ticks_passed = end_time - start_time;
TickType_t minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS);
if (minimum_ticks > ticks_passed) {
kernel::delayTicks(minimum_ticks - ticks_passed);
}
tt::service::loader::stopApp();
startNextApp();
}
return 0;
} }
return 0; static void startNextApp() {
}
static void startNextApp() {
#ifdef ESP_PLATFORM #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) {
app::crashdiagnostics::start(); app::crashdiagnostics::start();
return; return;
} }
#endif #endif
auto* config = tt::getConfiguration(); auto* config = tt::getConfiguration();
if (config->autoStartAppId) { if (config->autoStartAppId) {
TT_LOG_I(TAG, "init auto-starting %s", config->autoStartAppId); TT_LOG_I(TAG, "init auto-starting %s", config->autoStartAppId);
tt::service::loader::startApp(config->autoStartAppId); tt::service::loader::startApp(config->autoStartAppId);
} 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());
auto* image = lv_image_create(parent); void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
lv_obj_set_size(image, LV_PCT(100), LV_PCT(100)); auto* image = lv_image_create(parent);
lv_obj_set_size(image, LV_PCT(100), LV_PCT(100));
auto paths = app.getPaths(); auto paths = app.getPaths();
const char* logo = hal::usb::isUsbBootMode() ? "logo_usb.png" : "logo.png"; const char* logo = hal::usb::isUsbBootMode() ? "logo_usb.png" : "logo.png";
auto logo_path = paths->getSystemPathLvgl(logo); auto logo_path = paths->getSystemPathLvgl(logo);
TT_LOG_I(TAG, "%s", logo_path.c_str()); TT_LOG_I(TAG, "%s", logo_path.c_str());
lv_image_set_src(image, logo_path.c_str()); lv_image_set_src(image, logo_path.c_str());
lvgl::obj_set_style_bg_blacken(parent); lvgl::obj_set_style_bg_blacken(parent);
data->thread.start(); // Just in case this app is somehow resumed
} if (thread.getState() == Thread::State::Stopped) {
thread.start();
}
}
static void onStart(AppContext& app) { 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,103 +17,108 @@ 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 {
auto* display = lv_obj_get_display(parent);
int32_t parent_height = lv_display_get_vertical_resolution(display) - STATUSBAR_HEIGHT;
lv_obj_add_event_cb(parent, onContinuePressed, LV_EVENT_SHORT_CLICKED, nullptr); public:
auto* top_label = lv_label_create(parent);
lv_label_set_text(top_label, "Oops! We've crashed ..."); // TODO: Funny messages
lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2);
auto* bottom_label = lv_label_create(parent); void onShow(AppContext& app, lv_obj_t* parent) override {
lv_label_set_text(bottom_label, "Tap screen to continue"); auto* display = lv_obj_get_display(parent);
lv_obj_align(bottom_label, LV_ALIGN_BOTTOM_MID, 0, -2); int32_t parent_height = lv_display_get_vertical_resolution(display) - STATUSBAR_HEIGHT;
std::string url = getUrlFromCrashData(); lv_obj_add_event_cb(parent, onContinuePressed, LV_EVENT_SHORT_CLICKED, nullptr);
TT_LOG_I(TAG, "%s", url.c_str()); auto* top_label = lv_label_create(parent);
size_t url_length = url.length(); lv_label_set_text(top_label, "Oops! We've crashed ..."); // TODO: Funny messages
lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2);
int qr_version; auto* bottom_label = lv_label_create(parent);
if (!getQrVersionForBinaryDataLength(url_length, qr_version)) { lv_label_set_text(bottom_label, "Tap screen to continue");
TT_LOG_E(TAG, "QR is too large"); lv_obj_align(bottom_label, LV_ALIGN_BOTTOM_MID, 0, -2);
service::loader::stopApp();
return;
}
TT_LOG_I(TAG, "QR version %d (length: %d)", qr_version, url_length); std::string url = getUrlFromCrashData();
auto qrcodeData = std::make_shared<uint8_t[]>(qrcode_getBufferSize(qr_version)); TT_LOG_I(TAG, "%s", url.c_str());
if (qrcodeData == nullptr) { size_t url_length = url.length();
TT_LOG_E(TAG, "Failed to allocate QR buffer");
service::loader::stopApp();
return;
}
QRCode qrcode; int qr_version;
TT_LOG_I(TAG, "QR init text"); if (!getQrVersionForBinaryDataLength(url_length, qr_version)) {
if (qrcode_initText(&qrcode, qrcodeData.get(), qr_version, ECC_LOW, url.c_str()) != 0) { TT_LOG_E(TAG, "QR is too large");
TT_LOG_E(TAG, "QR init text failed"); service::loader::stopApp();
service::loader::stopApp(); return;
return; }
}
TT_LOG_I(TAG, "QR size: %d", qrcode.size); TT_LOG_I(TAG, "QR version %d (length: %d)", qr_version, url_length);
auto qrcodeData = std::make_shared<uint8_t[]>(qrcode_getBufferSize(qr_version));
if (qrcodeData == nullptr) {
TT_LOG_E(TAG, "Failed to allocate QR buffer");
service::loader::stopApp();
return;
}
// Calculate QR dot size QRCode qrcode;
int32_t top_label_height = lv_obj_get_height(top_label) + 2; TT_LOG_I(TAG, "QR init text");
int32_t bottom_label_height = lv_obj_get_height(bottom_label) + 2; if (qrcode_initText(&qrcode, qrcodeData.get(), qr_version, ECC_LOW, url.c_str()) != 0) {
TT_LOG_I(TAG, "Create canvas"); TT_LOG_E(TAG, "QR init text failed");
int32_t available_height = parent_height - top_label_height - bottom_label_height; service::loader::stopApp();
int32_t available_width = lv_display_get_horizontal_resolution(display); return;
int32_t smallest_size = TT_MIN(available_height, available_width); }
int32_t pixel_size;
if (qrcode.size * 2 <= smallest_size) {
pixel_size = 2;
} else if (qrcode.size <= smallest_size) {
pixel_size = 1;
} else {
TT_LOG_E(TAG, "QR code won't fit screen");
service::loader::stopApp();
return;
}
auto* canvas = lv_canvas_create(parent); TT_LOG_I(TAG, "QR size: %d", qrcode.size);
lv_obj_set_size(canvas, pixel_size * qrcode.size, pixel_size * qrcode.size);
lv_obj_align(canvas, LV_ALIGN_CENTER, 0, 0);
lv_canvas_fill_bg(canvas, lv_color_black(), LV_OPA_COVER);
lv_obj_set_content_height(canvas, qrcode.size * pixel_size);
lv_obj_set_content_width(canvas, qrcode.size * pixel_size);
TT_LOG_I(TAG, "Create draw buffer"); // Calculate QR dot size
auto* draw_buf = lv_draw_buf_create(pixel_size * qrcode.size, pixel_size * qrcode.size, LV_COLOR_FORMAT_RGB565, LV_STRIDE_AUTO); int32_t top_label_height = lv_obj_get_height(top_label) + 2;
if (draw_buf == nullptr) { int32_t bottom_label_height = lv_obj_get_height(bottom_label) + 2;
TT_LOG_E(TAG, "Draw buffer alloc"); TT_LOG_I(TAG, "Create canvas");
service::loader::stopApp(); int32_t available_height = parent_height - top_label_height - bottom_label_height;
return; int32_t available_width = lv_display_get_horizontal_resolution(display);
} int32_t smallest_size = TT_MIN(available_height, available_width);
int32_t pixel_size;
if (qrcode.size * 2 <= smallest_size) {
pixel_size = 2;
} else if (qrcode.size <= smallest_size) {
pixel_size = 1;
} else {
TT_LOG_E(TAG, "QR code won't fit screen");
service::loader::stopApp();
return;
}
lv_canvas_set_draw_buf(canvas, draw_buf); auto* canvas = lv_canvas_create(parent);
lv_obj_set_size(canvas, pixel_size * qrcode.size, pixel_size * qrcode.size);
lv_obj_align(canvas, LV_ALIGN_CENTER, 0, 0);
lv_canvas_fill_bg(canvas, lv_color_black(), LV_OPA_COVER);
lv_obj_set_content_height(canvas, qrcode.size * pixel_size);
lv_obj_set_content_width(canvas, qrcode.size * pixel_size);
for (uint8_t y = 0; y < qrcode.size; y++) { TT_LOG_I(TAG, "Create draw buffer");
for (uint8_t x = 0; x < qrcode.size; x++) { auto* draw_buf = lv_draw_buf_create(pixel_size * qrcode.size, pixel_size * qrcode.size, LV_COLOR_FORMAT_RGB565, LV_STRIDE_AUTO);
bool colored = qrcode_getModule(&qrcode, x, y); if (draw_buf == nullptr) {
auto color = colored ? lv_color_white() : lv_color_black(); TT_LOG_E(TAG, "Draw buffer alloc");
int32_t pos_x = x * pixel_size; service::loader::stopApp();
int32_t pos_y = y * pixel_size; return;
for (int px = 0; px < pixel_size; px++) { }
for (int py = 0; py < pixel_size; py++) {
lv_canvas_set_px(canvas, pos_x + px, pos_y + py, color, LV_OPA_COVER); lv_canvas_set_draw_buf(canvas, draw_buf);
for (uint8_t y = 0; y < qrcode.size; y++) {
for (uint8_t x = 0; x < qrcode.size; x++) {
bool colored = qrcode_getModule(&qrcode, x, y);
auto color = colored ? lv_color_white() : lv_color_black();
int32_t pos_x = x * pixel_size;
int32_t pos_y = y * pixel_size;
for (int px = 0; px < pixel_size; px++) {
for (int py = 0; py < pixel_size; py++) {
lv_canvas_set_px(canvas, pos_x + px, pos_y + py, color, LV_OPA_COVER);
}
} }
} }
} }
} }
} };
extern const AppManifest manifest = { 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,92 +98,92 @@ static void onOrientationSet(lv_event_t* event) {
} }
} }
static void onShow(AppContext& app, lv_obj_t* parent) { class DisplayApp : public App {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* main_wrapper = lv_obj_create(parent); lvgl::toolbar_create(parent, app);
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(main_wrapper, LV_PCT(100));
lv_obj_set_flex_grow(main_wrapper, 1);
lv_obj_t* wrapper = lv_obj_create(main_wrapper); lv_obj_t* main_wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(wrapper, 8, 0); lv_obj_set_width(main_wrapper, LV_PCT(100));
lv_obj_set_style_border_width(wrapper, 0, 0); lv_obj_set_flex_grow(main_wrapper, 1);
lv_obj_t* brightness_label = lv_label_create(wrapper); lv_obj_t* wrapper = lv_obj_create(main_wrapper);
lv_label_set_text(brightness_label, "Brightness"); lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_style_pad_all(wrapper, 8, 0);
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_t* brightness_slider = lv_slider_create(wrapper); lv_obj_t* brightness_label = lv_label_create(wrapper);
lv_obj_set_width(brightness_slider, LV_PCT(50)); lv_label_set_text(brightness_label, "Brightness");
lv_obj_align(brightness_slider, LV_ALIGN_TOP_RIGHT, -8, 0);
lv_slider_set_range(brightness_slider, 0, 255);
lv_obj_add_event_cb(brightness_slider, onBacklightSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_t* gamma_label = lv_label_create(wrapper); lv_obj_t* brightness_slider = lv_slider_create(wrapper);
lv_label_set_text(gamma_label, "Gamma"); lv_obj_set_width(brightness_slider, LV_PCT(50));
lv_obj_set_y(gamma_label, 40); lv_obj_align(brightness_slider, LV_ALIGN_TOP_RIGHT, -8, 0);
lv_slider_set_range(brightness_slider, 0, 255);
lv_obj_add_event_cb(brightness_slider, onBacklightSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_t* gamma_slider = lv_slider_create(wrapper); lv_obj_t* gamma_label = lv_label_create(wrapper);
lv_obj_set_width(gamma_slider, LV_PCT(50)); lv_label_set_text(gamma_label, "Gamma");
lv_obj_align(gamma_slider, LV_ALIGN_TOP_RIGHT, -8, 40); lv_obj_set_y(gamma_label, 40);
lv_slider_set_range(gamma_slider, 0, getHalDisplay(parent)->getGammaCurveCount());
lv_obj_add_event_cb(gamma_slider, onGammaSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr);
auto* hal_display = getHalDisplay(parent); lv_obj_t* gamma_slider = lv_slider_create(wrapper);
tt_assert(hal_display != nullptr); lv_obj_set_width(gamma_slider, LV_PCT(50));
lv_obj_align(gamma_slider, LV_ALIGN_TOP_RIGHT, -8, 40);
lv_slider_set_range(gamma_slider, 0, getHalDisplay(parent)->getGammaCurveCount());
lv_obj_add_event_cb(gamma_slider, onGammaSliderEvent, LV_EVENT_VALUE_CHANGED, nullptr);
if (!hal_display->supportsBacklightDuty()) { auto* hal_display = getHalDisplay(parent);
lv_slider_set_value(brightness_slider, 255, LV_ANIM_OFF); tt_assert(hal_display != nullptr);
lv_obj_add_state(brightness_slider, LV_STATE_DISABLED);
} else { if (!hal_display->supportsBacklightDuty()) {
uint8_t value = getBacklightDuty(); lv_slider_set_value(brightness_slider, 255, LV_ANIM_OFF);
lv_slider_set_value(brightness_slider, value, LV_ANIM_OFF); lv_obj_add_state(brightness_slider, LV_STATE_DISABLED);
} else {
uint8_t value = getBacklightDuty();
lv_slider_set_value(brightness_slider, value, LV_ANIM_OFF);
}
lv_slider_set_value(gamma_slider, 128, LV_ANIM_OFF);
lv_obj_t* orientation_label = lv_label_create(wrapper);
lv_label_set_text(orientation_label, "Orientation");
lv_obj_align(orientation_label, LV_ALIGN_TOP_LEFT, 0, 80);
auto lvgl_display = lv_obj_get_display(parent);
auto horizontal_px = lv_display_get_horizontal_resolution(lvgl_display);
auto vertical_px = lv_display_get_vertical_resolution(lvgl_display);
bool is_landscape_display = horizontal_px > vertical_px;
lv_obj_t* orientation_dropdown = lv_dropdown_create(wrapper);
if (is_landscape_display) {
lv_dropdown_set_options(orientation_dropdown, "Landscape\nLandscape (flipped)\nPortrait Left\nPortrait Right");
} else {
lv_dropdown_set_options(orientation_dropdown, "Portrait\nPortrait (flipped)\nLandscape Left\nLandscape Right");
}
lv_obj_align(orientation_dropdown, LV_ALIGN_TOP_RIGHT, 0, 72);
lv_obj_add_event_cb(orientation_dropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, nullptr);
uint32_t orientation_selected = dipslayOrientationToOrientationSetting(
lv_display_get_rotation(lv_display_get_default())
);
lv_dropdown_set_selected(orientation_dropdown, orientation_selected);
} }
lv_slider_set_value(gamma_slider, 128, LV_ANIM_OFF); void onHide(TT_UNUSED AppContext& app) override {
if (backlight_duty_set) {
lv_obj_t* orientation_label = lv_label_create(wrapper); setBacklightDuty(backlight_duty);
lv_label_set_text(orientation_label, "Orientation"); }
lv_obj_align(orientation_label, LV_ALIGN_TOP_LEFT, 0, 80);
auto lvgl_display = lv_obj_get_display(parent);
auto horizontal_px = lv_display_get_horizontal_resolution(lvgl_display);
auto vertical_px = lv_display_get_vertical_resolution(lvgl_display);
bool is_landscape_display = horizontal_px > vertical_px;
lv_obj_t* orientation_dropdown = lv_dropdown_create(wrapper);
if (is_landscape_display) {
lv_dropdown_set_options(orientation_dropdown, "Landscape\nLandscape (flipped)\nPortrait Left\nPortrait Right");
} else {
lv_dropdown_set_options(orientation_dropdown, "Portrait\nPortrait (flipped)\nLandscape Left\nLandscape Right");
} }
};
lv_obj_align(orientation_dropdown, LV_ALIGN_TOP_RIGHT, 0, 72);
lv_obj_add_event_cb(orientation_dropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, nullptr);
uint32_t orientation_selected = dipslayOrientationToOrientationSetting(
lv_display_get_rotation(lv_display_get_default())
);
lv_dropdown_set_selected(orientation_dropdown, orientation_selected);
}
static void onHide(TT_UNUSED AppContext& app) {
if (backlight_duty_set) {
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,28 +69,31 @@ 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 {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent); void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_grow(wrapper, 1); lvgl::toolbar_create(parent, app);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(wrapper);
lvgl::obj_set_style_bg_invisible(wrapper);
for (const auto& configuration: getConfiguration()->hardware->i2c) { lv_obj_t* wrapper = lv_obj_create(parent);
show(wrapper, configuration); lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(wrapper);
lvgl::obj_set_style_bg_invisible(wrapper);
for (const auto& configuration: getConfiguration()->hardware->i2c) {
show(wrapper, configuration);
}
} }
} };
extern const AppManifest manifest = { 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,50 +12,53 @@ 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 {
auto wrapper = lv_obj_create(parent);
lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_border_width(wrapper, 0, 0);
lvgl::obj_set_style_no_padding(wrapper);
auto toolbar = lvgl::toolbar_create(wrapper, app); void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); auto wrapper = lv_obj_create(parent);
lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_border_width(wrapper, 0, 0);
lvgl::obj_set_style_no_padding(wrapper);
auto* image_wrapper = lv_obj_create(wrapper); auto toolbar = lvgl::toolbar_create(wrapper, app);
lv_obj_align_to(image_wrapper, toolbar, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_set_width(image_wrapper, LV_PCT(100));
auto parent_height = lv_obj_get_height(wrapper);
lv_obj_set_height(image_wrapper, parent_height - TOOLBAR_HEIGHT);
lv_obj_set_flex_flow(image_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(image_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lvgl::obj_set_style_no_padding(image_wrapper);
lvgl::obj_set_style_bg_invisible(image_wrapper);
auto* image = lv_image_create(image_wrapper); auto* image_wrapper = lv_obj_create(wrapper);
lv_obj_align(image, LV_ALIGN_CENTER, 0, 0); lv_obj_align_to(image_wrapper, toolbar, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 0);
lv_obj_set_width(image_wrapper, LV_PCT(100));
auto parent_height = lv_obj_get_height(wrapper);
lv_obj_set_height(image_wrapper, parent_height - TOOLBAR_HEIGHT);
lv_obj_set_flex_flow(image_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(image_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lvgl::obj_set_style_no_padding(image_wrapper);
lvgl::obj_set_style_bg_invisible(image_wrapper);
auto* file_label = lv_label_create(wrapper); auto* image = lv_image_create(image_wrapper);
lv_obj_align_to(file_label, wrapper, LV_ALIGN_BOTTOM_LEFT, 0, 0); lv_obj_align(image, LV_ALIGN_CENTER, 0, 0);
std::shared_ptr<const Bundle> bundle = app.getParameters(); auto* file_label = lv_label_create(wrapper);
tt_check(bundle != nullptr, "Parameters not set"); lv_obj_align_to(file_label, wrapper, LV_ALIGN_BOTTOM_LEFT, 0, 0);
std::string file_argument;
if (bundle->optString(IMAGE_VIEWER_FILE_ARGUMENT, file_argument)) { std::shared_ptr<const Bundle> bundle = app.getParameters();
std::string prefixed_path = "A:" + file_argument; tt_check(bundle != nullptr, "Parameters not set");
TT_LOG_I(TAG, "Opening %s", prefixed_path.c_str()); std::string file_argument;
lv_img_set_src(image, prefixed_path.c_str()); if (bundle->optString(IMAGE_VIEWER_FILE_ARGUMENT, file_argument)) {
auto path = string::getLastPathSegment(file_argument); std::string prefixed_path = "A:" + file_argument;
lv_label_set_text(file_label, path.c_str()); TT_LOG_I(TAG, "Opening %s", prefixed_path.c_str());
} else { lv_img_set_src(image, prefixed_path.c_str());
lv_label_set_text(file_label, "File not found"); auto path = string::getLastPathSegment(file_argument);
lv_label_set_text(file_label, path.c_str());
} else {
lv_label_set_text(file_label, "File not found");
}
} }
} };
extern const AppManifest manifest = { 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,75 +43,87 @@ 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) {
auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing");
std::string title = getTitleParameter(app.getParameters());
auto* toolbar = lvgl::toolbar_create(parent, title);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
auto* message_label = lv_label_create(parent);
lv_obj_align(message_label, LV_ALIGN_CENTER, 0, -20);
lv_obj_set_width(message_label, LV_PCT(80));
std::string message;
if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) {
lv_label_set_text(message_label, message.c_str());
lv_label_set_long_mode(message_label, LV_LABEL_LONG_WRAP);
} }
auto* textarea = lv_textarea_create(parent); static void onButtonClickedCallback(lv_event_t* e) {
lv_obj_align_to(textarea, message_label, LV_ALIGN_OUT_BOTTOM_MID, 0, 4); auto appContext = service::loader::getCurrentAppContext();
lv_textarea_set_one_line(textarea, true); tt_assert(appContext != nullptr);
std::string prefilled; auto app = std::static_pointer_cast<InputDialogApp>(appContext->getApp());
if (parameters->optString(PARAMETER_BUNDLE_KEY_PREFILLED, prefilled)) { app->onButtonClicked(e);
lv_textarea_set_text(textarea, prefilled.c_str());
} }
service::gui::keyboardAddTextArea(textarea);
auto* button_wrapper = lv_obj_create(parent); void onButtonClicked(lv_event_t* e) {
lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW); auto user_data = lv_event_get_user_data(e);
lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT); int index = (user_data != 0) ? 0 : 1;
lv_obj_set_style_pad_all(button_wrapper, 0, 0); TT_LOG_I(TAG, "Selected item at index %d", index);
lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); if (index == 0) {
lv_obj_set_style_border_width(button_wrapper, 0, 0); auto bundle = std::make_unique<Bundle>();
lv_obj_align(button_wrapper, LV_ALIGN_BOTTOM_MID, 0, -4); const char* text = lv_textarea_get_text((lv_obj_t*)user_data);
bundle->putString(RESULT_BUNDLE_KEY_RESULT, text);
setResult(app::Result::Ok, std::move(bundle));
} else {
setResult(app::Result::Cancelled);
createButton(button_wrapper, "OK", textarea); }
createButton(button_wrapper, "Cancel", nullptr); service::loader::stopApp();
} }
public:
void onShow(AppContext& app, lv_obj_t* parent) override {
auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing");
std::string title = getTitleParameter(app.getParameters());
auto* toolbar = lvgl::toolbar_create(parent, title);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
auto* message_label = lv_label_create(parent);
lv_obj_align(message_label, LV_ALIGN_CENTER, 0, -20);
lv_obj_set_width(message_label, LV_PCT(80));
std::string message;
if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) {
lv_label_set_text(message_label, message.c_str());
lv_label_set_long_mode(message_label, LV_LABEL_LONG_WRAP);
}
auto* textarea = lv_textarea_create(parent);
lv_obj_align_to(textarea, message_label, LV_ALIGN_OUT_BOTTOM_MID, 0, 4);
lv_textarea_set_one_line(textarea, true);
std::string prefilled;
if (parameters->optString(PARAMETER_BUNDLE_KEY_PREFILLED, prefilled)) {
lv_textarea_set_text(textarea, prefilled.c_str());
}
service::gui::keyboardAddTextArea(textarea);
auto* button_wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(button_wrapper, 0, 0);
lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_border_width(button_wrapper, 0, 0);
lv_obj_align(button_wrapper, LV_ALIGN_BOTTOM_MID, 0, -4);
createButton(button_wrapper, "OK", textarea);
createButton(button_wrapper, "Cancel", nullptr);
}
};
extern const AppManifest manifest = { 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,42 +42,45 @@ 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 {
auto* wrapper = lv_obj_create(parent);
lv_obj_align(wrapper, LV_ALIGN_CENTER, 0, 0); void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
lv_obj_set_style_pad_all(wrapper, 0, 0); auto* wrapper = lv_obj_create(parent);
lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_flex_grow(wrapper, 1);
auto* display = lv_obj_get_display(parent); lv_obj_align(wrapper, LV_ALIGN_CENTER, 0, 0);
auto horizontal_px = lv_display_get_horizontal_resolution(display); lv_obj_set_style_pad_all(wrapper, 0, 0);
auto vertical_px = lv_display_get_vertical_resolution(display); lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
bool is_landscape_display = horizontal_px > vertical_px; lv_obj_set_style_border_width(wrapper, 0, 0);
if (is_landscape_display) { lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
} else { auto* display = lv_obj_get_display(parent);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); auto horizontal_px = lv_display_get_horizontal_resolution(display);
auto vertical_px = lv_display_get_vertical_resolution(display);
bool is_landscape_display = horizontal_px > vertical_px;
if (is_landscape_display) {
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
} else {
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
}
int32_t available_width = lv_display_get_horizontal_resolution(display) - (3 * 80);
int32_t padding = is_landscape_display ? TT_MIN(available_width / 4, 64) : 0;
auto paths = app.getPaths();
auto apps_icon_path = paths->getSystemPathLvgl("icon_apps.png");
auto files_icon_path = paths->getSystemPathLvgl("icon_files.png");
auto settings_icon_path = paths->getSystemPathLvgl("icon_settings.png");
createAppButton(wrapper, "Apps", apps_icon_path.c_str(), "AppList", 0);
createAppButton(wrapper, "Files", files_icon_path.c_str(), "Files", padding);
createAppButton(wrapper, "Settings", settings_icon_path.c_str(), "Settings", padding);
} }
};
int32_t available_width = lv_display_get_horizontal_resolution(display) - (3 * 80);
int32_t padding = is_landscape_display ? TT_MIN(available_width / 4, 64) : 0;
auto paths = app.getPaths();
auto apps_icon_path = paths->getSystemPathLvgl("icon_apps.png");
auto files_icon_path = paths->getSystemPathLvgl("icon_files.png");
auto settings_icon_path = paths->getSystemPathLvgl("icon_settings.png");
createAppButton(wrapper, "Apps", apps_icon_path.c_str(), "AppList", 0);
createAppButton(wrapper, "Files", files_icon_path.c_str(), "Files", padding);
createAppButton(wrapper, "Settings", settings_icon_path.c_str(), "Settings", padding);
}
extern const AppManifest manifest = { 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,139 +11,121 @@
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;
unsigned int index;
auto* entries = copyLogEntries(index);
std::stringstream buffer;
if (entries != nullptr) {
for (unsigned int i = index; i < TT_LOG_ENTRY_COUNT; ++i) {
if (shouldShowLog(filterLevel, entries[i].level)) {
buffer << entries[i].message;
}
} }
if (index != 0) { }
for (unsigned int i = 0; i < index; ++i) {
void updateLogEntries() {
unsigned int index;
auto* entries = copyLogEntries(index);
std::stringstream buffer;
if (entries != nullptr) {
for (unsigned int i = index; i < TT_LOG_ENTRY_COUNT; ++i) {
if (shouldShowLog(filterLevel, entries[i].level)) { if (shouldShowLog(filterLevel, entries[i].level)) {
buffer << entries[i].message; buffer << entries[i].message;
} }
} }
} if (index != 0) {
delete entries; for (unsigned int i = 0; i < index; ++i) {
if (!buffer.str().empty()) { if (shouldShowLog(filterLevel, entries[i].level)) {
lv_label_set_text(label, buffer.str().c_str()); buffer << entries[i].message;
}
}
}
delete entries;
if (!buffer.str().empty()) {
lv_label_set_text(labelWidget, buffer.str().c_str());
} else {
lv_label_set_text(labelWidget, "No logs for the selected log level");
}
} else { } else {
lv_label_set_text(label, "No logs for the selected log level"); lv_label_set_text(labelWidget, "Failed to load log");
}
} else {
lv_label_set_text(label, "Failed to load log");
}
}
static void onLevelFilterPressed(TT_UNUSED lv_event_t* event) {
std::vector<std::string> items = {
"Verbose",
"Debug",
"Info",
"Warning",
"Error",
};
app::selectiondialog::start("Log Level", items);
}
static void updateViews() {
auto app = service::loader::getCurrentApp();
if (app == nullptr) {
return;
}
auto data = std::static_pointer_cast<LogAppData>(app->getData());
assert(data != nullptr);
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
setLogEntries(data->labelWidget);
lvgl::unlock();
}
}
static void onShow(AppContext& app, lv_obj_t* parent) {
auto data = std::static_pointer_cast<LogAppData>(app.getData());
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
auto* toolbar = lvgl::toolbar_create(parent, app);
lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_EDIT, onLevelFilterPressed, nullptr);
auto* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(wrapper);
lvgl::obj_set_style_bg_invisible(wrapper);
data->labelWidget = lv_label_create(wrapper);
lv_obj_align(data->labelWidget, LV_ALIGN_CENTER, 0, 0);
setLogEntries(data->labelWidget);
}
static void onStart(AppContext& app) {
auto data = std::make_shared<LogAppData>();
app.setData(data);
}
static void onResult(AppContext& app, Result result, const Bundle& bundle) {
auto resultIndex = selectiondialog::getResultIndex(bundle);
auto data = std::static_pointer_cast<LogAppData>(app.getData());
if (result == ResultOk) {
switch (resultIndex) {
case 0:
data->filterLevel = LogLevel::Verbose;
break;
case 1:
data->filterLevel = LogLevel::Debug;
break;
case 2:
data->filterLevel = LogLevel::Info;
break;
case 3:
data->filterLevel = LogLevel::Warning;
break;
case 4:
data->filterLevel = LogLevel::Error;
break;
default:
break;
} }
} }
updateViews(); 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 = {
"Verbose",
"Debug",
"Info",
"Warning",
"Error",
};
app::selectiondialog::start("Log Level", items);
}
public:
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
auto* toolbar = lvgl::toolbar_create(parent, app);
lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_EDIT, onLevelFilterPressedCallback, this);
auto* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(wrapper);
lvgl::obj_set_style_bg_invisible(wrapper);
labelWidget = lv_label_create(wrapper);
lv_obj_align(labelWidget, LV_ALIGN_CENTER, 0, 0);
updateLogEntries();
}
void onResult(AppContext& app, Result result, std::unique_ptr<Bundle> bundle) override {
auto resultIndex = selectiondialog::getResultIndex(*bundle);
if (result == Result::Ok) {
switch (resultIndex) {
case 0:
filterLevel = LogLevel::Verbose;
break;
case 1:
filterLevel = LogLevel::Debug;
break;
case 2:
filterLevel = LogLevel::Info;
break;
case 3:
filterLevel = LogLevel::Warning;
break;
case 4:
filterLevel = LogLevel::Error;
break;
default:
break;
}
}
updateViews();
}
};
extern const AppManifest manifest = { 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,174 +15,173 @@ 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 {
const char* charge_state;
hal::Power::MetricData metric_data; private:
if (data->power->getMetric(hal::Power::MetricType::IsCharging, metric_data)) {
charge_state = metric_data.valueAsBool ? "yes" : "no"; Timer update_timer = Timer(Timer::Type::Periodic, &onTimer, nullptr);
} else { std::shared_ptr<tt::hal::Power> power = getConfiguration()->hardware->power();
charge_state = "N/A"; 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();
}
} }
uint8_t charge_level; void onPowerEnabledChanged(lv_event_t* event) {
bool charge_level_scaled_set = false; lv_event_code_t code = lv_event_get_code(event);
if (data->power->getMetric(hal::Power::MetricType::ChargeLevel, metric_data)) { auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
charge_level = metric_data.valueAsUint8; if (code == LV_EVENT_VALUE_CHANGED) {
charge_level_scaled_set = true; bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
}
bool charging_enabled_set = data->power->supportsChargeControl(); if (power->isAllowedToCharge() != is_on) {
bool charging_enabled_and_allowed = data->power->supportsChargeControl() && data->power->isAllowedToCharge(); power->setAllowedToCharge(is_on);
updateUi();
int32_t current;
bool current_set = false;
if (data->power->getMetric(hal::Power::MetricType::Current, metric_data)) {
current = metric_data.valueAsInt32;
current_set = true;
}
uint32_t battery_voltage;
bool battery_voltage_set = false;
if (data->power->getMetric(hal::Power::MetricType::BatteryVoltage, metric_data)) {
battery_voltage = metric_data.valueAsUint32;
battery_voltage_set = true;
}
lvgl::lock(kernel::millisToTicks(1000));
if (charging_enabled_set) {
lv_obj_set_state(data->enable_switch, LV_STATE_CHECKED, charging_enabled_and_allowed);
lv_obj_remove_flag(data->enable_switch, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(data->enable_label, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(data->enable_switch, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(data->enable_label, LV_OBJ_FLAG_HIDDEN);
}
lv_label_set_text_fmt(data->charge_state, "Charging: %s", charge_state);
if (battery_voltage_set) {
lv_label_set_text_fmt(data->battery_voltage, "Battery voltage: %lu mV", battery_voltage);
} else {
lv_label_set_text_fmt(data->battery_voltage, "Battery voltage: N/A");
}
if (charge_level_scaled_set) {
lv_label_set_text_fmt(data->charge_level, "Charge level: %d%%", charge_level);
} else {
lv_label_set_text_fmt(data->charge_level, "Charge level: N/A");
}
if (current_set) {
lv_label_set_text_fmt(data->current, "Current: %ld mAh", current);
} else {
lv_label_set_text_fmt(data->current, "Current: N/A");
}
lvgl::unlock();
}
static void onTimer(TT_UNUSED std::shared_ptr<void> context) {
auto data = optData();
if (data != nullptr) {
updateUi(data);
}
}
static void onPowerEnabledChanged(lv_event_t* event) {
lv_event_code_t code = lv_event_get_code(event);
auto* enable_switch = static_cast<lv_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();
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) { static void onPowerEnabledChangedCallback(lv_event_t* event) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); auto* app = (PowerApp*)lv_event_get_user_data(event);
app->onPowerEnabledChanged(event);
}
lvgl::toolbar_create(parent, app); void updateUi() {
const char* charge_state;
hal::Power::MetricData metric_data;
if (power->getMetric(hal::Power::MetricType::IsCharging, metric_data)) {
charge_state = metric_data.valueAsBool ? "yes" : "no";
} else {
charge_state = "N/A";
}
lv_obj_t* wrapper = lv_obj_create(parent); uint8_t charge_level;
lv_obj_set_width(wrapper, LV_PCT(100)); bool charge_level_scaled_set = false;
lv_obj_set_style_border_width(wrapper, 0, 0); if (power->getMetric(hal::Power::MetricType::ChargeLevel, metric_data)) {
lv_obj_set_flex_grow(wrapper, 1); charge_level = metric_data.valueAsUint8;
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); charge_level_scaled_set = true;
}
auto data = std::static_pointer_cast<Data>(app.getData()); bool charging_enabled_set = power->supportsChargeControl();
bool charging_enabled_and_allowed = power->supportsChargeControl() && power->isAllowedToCharge();
// Top row: enable/disable int32_t current;
lv_obj_t* switch_container = lv_obj_create(wrapper); bool current_set = false;
lv_obj_set_width(switch_container, LV_PCT(100)); if (power->getMetric(hal::Power::MetricType::Current, metric_data)) {
lv_obj_set_height(switch_container, LV_SIZE_CONTENT); current = metric_data.valueAsInt32;
lvgl::obj_set_style_no_padding(switch_container); current_set = true;
lvgl::obj_set_style_bg_invisible(switch_container); }
data->enable_label = lv_label_create(switch_container); uint32_t battery_voltage;
lv_label_set_text(data->enable_label, "Charging enabled"); bool battery_voltage_set = false;
lv_obj_set_align(data->enable_label, LV_ALIGN_LEFT_MID); if (power->getMetric(hal::Power::MetricType::BatteryVoltage, metric_data)) {
battery_voltage = metric_data.valueAsUint32;
battery_voltage_set = true;
}
lv_obj_t* enable_switch = lv_switch_create(switch_container); lvgl::lock(kernel::millisToTicks(1000));
lv_obj_add_event_cb(enable_switch, onPowerEnabledChanged, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID);
data->enable_switch = enable_switch; if (charging_enabled_set) {
data->charge_state = lv_label_create(wrapper); lv_obj_set_state(enableSwitch, LV_STATE_CHECKED, charging_enabled_and_allowed);
data->charge_level = lv_label_create(wrapper); lv_obj_remove_flag(enableSwitch, LV_OBJ_FLAG_HIDDEN);
data->battery_voltage = lv_label_create(wrapper); lv_obj_remove_flag(enableLabel, LV_OBJ_FLAG_HIDDEN);
data->current = lv_label_create(wrapper); } else {
lv_obj_add_flag(enableSwitch, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(enableLabel, LV_OBJ_FLAG_HIDDEN);
}
updateUi(data); lv_label_set_text_fmt(chargeStateLabel, "Charging: %s", charge_state);
data->update_timer.start(kernel::millisToTicks(1000));
}
static void onHide(TT_UNUSED AppContext& app) { if (battery_voltage_set) {
auto data = std::static_pointer_cast<Data>(app.getData()); lv_label_set_text_fmt(batteryVoltageLabel, "Battery voltage: %lu mV", battery_voltage);
data->update_timer.stop(); } else {
} lv_label_set_text_fmt(batteryVoltageLabel, "Battery voltage: N/A");
}
static void onStart(AppContext& app) { if (charge_level_scaled_set) {
auto data = std::make_shared<Data>(); lv_label_set_text_fmt(chargeLevelLabel, "Charge level: %d%%", charge_level);
app.setData(data); } else {
assert(data->power != nullptr); // The Power app only shows up on supported devices lv_label_set_text_fmt(chargeLevelLabel, "Charge level: N/A");
} }
if (current_set) {
lv_label_set_text_fmt(currentLabel, "Current: %ld mAh", current);
} else {
lv_label_set_text_fmt(currentLabel, "Current: N/A");
}
lvgl::unlock();
}
public:
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
// Top row: enable/disable
lv_obj_t* switch_container = lv_obj_create(wrapper);
lv_obj_set_width(switch_container, LV_PCT(100));
lv_obj_set_height(switch_container, LV_SIZE_CONTENT);
lvgl::obj_set_style_no_padding(switch_container);
lvgl::obj_set_style_bg_invisible(switch_container);
enableLabel = lv_label_create(switch_container);
lv_label_set_text(enableLabel, "Charging enabled");
lv_obj_set_align(enableLabel, LV_ALIGN_LEFT_MID);
lv_obj_t* enable_switch = lv_switch_create(switch_container);
lv_obj_add_event_cb(enable_switch, onPowerEnabledChangedCallback, LV_EVENT_VALUE_CHANGED, this);
lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID);
enableSwitch = enable_switch;
chargeStateLabel = lv_label_create(wrapper);
chargeLevelLabel = lv_label_create(wrapper);
batteryVoltageLabel = lv_label_create(wrapper);
currentLabel = lv_label_create(wrapper);
updateUi();
update_timer.start(kernel::millisToTicks(1000));
}
void onHide(TT_UNUSED AppContext& app) override {
update_timer.stop();
}
};
extern const AppManifest manifest = { 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,64 +41,77 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
} }
} }
static void onListItemSelected(lv_event_t* e) { class SelectionDialogApp : public App {
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::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 createChoiceItem(void* parent, const std::string& title, size_t index) { private:
auto* list = static_cast<lv_obj_t*>(parent);
lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str());
lv_obj_add_event_cb(btn, &onListItemSelected, LV_EVENT_SHORT_CLICKED, (void*)index);
}
static void onShow(AppContext& app, lv_obj_t* parent) { static void onListItemSelectedCallback(lv_event_t* e) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); auto appContext = service::loader::getCurrentAppContext();
std::string title = getTitleParameter(app.getParameters()); tt_assert(appContext != nullptr);
lvgl::toolbar_create(parent, title); auto app = std::static_pointer_cast<SelectionDialogApp>(appContext->getApp());
app->onListItemSelected(e);
}
lv_obj_t* list = lv_list_create(parent); void onListItemSelected(lv_event_t* e) {
lv_obj_set_width(list, LV_PCT(100)); size_t index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e));
lv_obj_set_flex_grow(list, 1); TT_LOG_I(TAG, "Selected item at index %d", index);
auto bundle = std::make_unique<Bundle>();
auto parameters = app.getParameters(); bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index);
tt_check(parameters != nullptr, "Parameters missing"); setResult(app::Result::Ok, std::move(bundle));
std::string items_concatenated;
if (parameters->optString(PARAMETER_BUNDLE_KEY_ITEMS, items_concatenated)) {
std::vector<std::string> items = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN);
if (items.empty() || items.front().empty()) {
TT_LOG_E(TAG, "No items provided");
app.setResult(ResultError);
service::loader::stopApp();
} else if (items.size() == 1) {
auto result_bundle = std::make_shared<Bundle>();
setResultIndex(result_bundle, 0);
app.setResult(ResultOk, result_bundle);
service::loader::stopApp();
TT_LOG_W(TAG, "Auto-selecting single item");
} else {
size_t index = 0;
for (const auto& item: items) {
createChoiceItem(list, item, index++);
}
}
} else {
TT_LOG_E(TAG, "No items provided");
app.setResult(ResultError);
service::loader::stopApp(); service::loader::stopApp();
} }
}
static void createChoiceItem(void* parent, const std::string& title, size_t index) {
auto* list = static_cast<lv_obj_t*>(parent);
lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str());
lv_obj_add_event_cb(btn, onListItemSelectedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
}
public:
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
std::string title = getTitleParameter(app.getParameters());
lvgl::toolbar_create(parent, title);
lv_obj_t* list = lv_list_create(parent);
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing");
std::string items_concatenated;
if (parameters->optString(PARAMETER_BUNDLE_KEY_ITEMS, items_concatenated)) {
std::vector<std::string> items = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN);
if (items.empty() || items.front().empty()) {
TT_LOG_E(TAG, "No items provided");
setResult(Result::Error);
service::loader::stopApp();
} else if (items.size() == 1) {
auto result_bundle = std::make_unique<Bundle>();
result_bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, 0);
setResult(Result::Ok, std::move(result_bundle));
service::loader::stopApp();
TT_LOG_W(TAG, "Auto-selecting single item");
} else {
size_t index = 0;
for (const auto& item: items) {
createChoiceItem(list, item, index++);
}
}
} else {
TT_LOG_E(TAG, "No items provided");
setResult(Result::Error);
service::loader::stopApp();
}
}
};
extern const AppManifest manifest = { 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,38 +13,41 @@ 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 {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app); void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* list = lv_list_create(parent); lvgl::toolbar_create(parent, app);
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
auto manifests = getApps(); lv_obj_t* list = lv_list_create(parent);
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName); lv_obj_set_width(list, LV_PCT(100));
for (const auto& manifest: manifests) { lv_obj_set_flex_grow(list, 1);
if (manifest->type == TypeSettings) {
createWidget(manifest, list); auto manifests = getApps();
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
for (const auto& manifest: manifests) {
if (manifest->type == Type::Settings) {
createWidget(manifest, list);
}
} }
} }
} };
extern const AppManifest manifest = { 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,57 +108,58 @@ static void addRtosTasks(lv_obj_t* parent) {
#endif #endif
static void onShow(AppContext& app, lv_obj_t* parent) { class SystemInfoApp : public App {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app);
// This wrapper automatically has its children added vertically underneath eachother void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_t* wrapper = lv_obj_create(parent); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_border_width(wrapper, 0, 0); lvgl::toolbar_create(parent, app);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
// Wrapper for the memory usage bars // This wrapper automatically has its children added vertically underneath eachother
lv_obj_t* memory_label = lv_label_create(wrapper); lv_obj_t* wrapper = lv_obj_create(parent);
lv_label_set_text(memory_label, "Memory usage"); lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_t* memory_wrapper = lv_obj_create(wrapper); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_flow(memory_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_size(memory_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_flex_grow(wrapper, 1);
addMemoryBar(memory_wrapper, "Heap", getHeapTotal() - getHeapFree(), getHeapTotal()); // Wrapper for the memory usage bars
addMemoryBar(memory_wrapper, "SPI", getSpiTotal() - getSpiFree(), getSpiTotal()); lv_obj_t* memory_label = lv_label_create(wrapper);
lv_label_set_text(memory_label, "Memory usage");
lv_obj_t* memory_wrapper = lv_obj_create(wrapper);
lv_obj_set_flex_flow(memory_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_size(memory_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
addMemoryBar(memory_wrapper, "Heap", getHeapTotal() - getHeapFree(), getHeapTotal());
addMemoryBar(memory_wrapper, "SPI", getSpiTotal() - getSpiFree(), getSpiTotal());
#if configUSE_TRACE_FACILITY #if configUSE_TRACE_FACILITY
lv_obj_t* tasks_label = lv_label_create(wrapper); lv_obj_t* tasks_label = lv_label_create(wrapper);
lv_label_set_text(tasks_label, "Tasks"); lv_label_set_text(tasks_label, "Tasks");
lv_obj_t* tasks_wrapper = lv_obj_create(wrapper); lv_obj_t* tasks_wrapper = lv_obj_create(wrapper);
lv_obj_set_flex_flow(tasks_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(tasks_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_size(tasks_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(tasks_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
addRtosTasks(tasks_wrapper); addRtosTasks(tasks_wrapper);
#endif #endif
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
// Build info // Build info
lv_obj_t* build_info_label = lv_label_create(wrapper); lv_obj_t* build_info_label = lv_label_create(wrapper);
lv_label_set_text(build_info_label, "Build info"); lv_label_set_text(build_info_label, "Build info");
lv_obj_t* build_info_wrapper = lv_obj_create(wrapper); lv_obj_t* build_info_wrapper = lv_obj_create(wrapper);
lv_obj_set_flex_flow(build_info_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(build_info_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_size(build_info_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(build_info_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_t* esp_idf_version = lv_label_create(build_info_wrapper); lv_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,40 +11,43 @@
namespace tt::app::textviewer { namespace tt::app::textviewer {
static void onShow(AppContext& app, lv_obj_t* parent) { class TextViewerApp : public App {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent); void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_grow(wrapper, 1); lvgl::toolbar_create(parent, app);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_no_padding(wrapper);
lvgl::obj_set_style_bg_invisible(wrapper);
lv_obj_t* label = lv_label_create(wrapper); lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); lv_obj_set_width(wrapper, LV_PCT(100));
auto parameters = app.getParameters(); lv_obj_set_flex_grow(wrapper, 1);
tt_check(parameters != nullptr, "Parameters missing"); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
bool success = false; lvgl::obj_set_style_no_padding(wrapper);
std::string file_argument; lvgl::obj_set_style_bg_invisible(wrapper);
if (parameters->optString(TEXT_VIEWER_FILE_ARGUMENT, file_argument)) {
TT_LOG_I(TAG, "Opening %s", file_argument.c_str()); lv_obj_t* label = lv_label_create(wrapper);
if (lvgl::label_set_text_file(label, file_argument.c_str())) { lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
success = true; auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing");
bool success = false;
std::string file_argument;
if (parameters->optString(TEXT_VIEWER_FILE_ARGUMENT, file_argument)) {
TT_LOG_I(TAG, "Opening %s", file_argument.c_str());
if (lvgl::label_set_text_file(label, file_argument.c_str())) {
success = true;
}
}
if (!success) {
lv_label_set_text_fmt(label, "Failed to load %s", file_argument.c_str());
} }
} }
};
if (!success) {
lv_label_set_text_fmt(label, "Failed to load %s", file_argument.c_str());
}
}
extern const AppManifest manifest = { 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,119 +13,104 @@ 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() { timezone::start();
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) { static void onTimeFormatChanged(lv_event_t* event) {
timezone::start(); auto* widget = lv_event_get_target_obj(event);
} bool show_24 = lv_obj_has_state(widget, LV_STATE_CHECKED);
time::setTimeFormat24Hour(show_24);
static void onTimeFormatChanged(lv_event_t* event) {
auto* widget = lv_event_get_target_obj(event);
bool show_24 = lv_obj_has_state(widget, LV_STATE_CHECKED);
time::setTimeFormat24Hour(show_24);
}
static void onShow(AppContext& app, lv_obj_t* parent) {
auto data = std::static_pointer_cast<Data>(app.getData());
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app);
auto* main_wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(main_wrapper, LV_PCT(100));
lv_obj_set_flex_grow(main_wrapper, 1);
auto* region_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_width(region_wrapper, LV_PCT(100));
lv_obj_set_height(region_wrapper, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(region_wrapper, 0, 0);
lv_obj_set_style_border_width(region_wrapper, 0, 0);
auto* region_prefix_label = lv_label_create(region_wrapper);
lv_label_set_text(region_prefix_label, "Region: ");
lv_obj_align(region_prefix_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* region_label = lv_label_create(region_wrapper);
std::string timeZoneName = time::getTimeZoneName();
if (timeZoneName.empty()) {
timeZoneName = "not set";
} }
data->regionLabelWidget = region_label;
lv_label_set_text(region_label, timeZoneName.c_str());
// TODO: Find out why Y offset is needed
lv_obj_align_to(region_label, region_prefix_label, LV_ALIGN_OUT_RIGHT_MID, 0, 8);
auto* region_button = lv_button_create(region_wrapper); public:
lv_obj_align(region_button, LV_ALIGN_TOP_RIGHT, 0, 0);
auto* region_button_image = lv_image_create(region_button);
lv_obj_add_event_cb(region_button, onConfigureTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr);
lv_image_set_src(region_button_image, LV_SYMBOL_SETTINGS);
auto* time_format_wrapper= lv_obj_create(main_wrapper); void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_width(time_format_wrapper, LV_PCT(100)); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(time_format_wrapper, 0, 0);
lv_obj_set_style_border_width(time_format_wrapper, 0, 0);
auto* time_24h_label = lv_label_create(time_format_wrapper); lvgl::toolbar_create(parent, app);
lv_label_set_text(time_24h_label, "24-hour clock");
lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* time_24h_switch = lv_switch_create(time_format_wrapper); auto* main_wrapper = lv_obj_create(parent);
lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_add_event_cb(time_24h_switch, onTimeFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr); lv_obj_set_width(main_wrapper, LV_PCT(100));
if (time::isTimeFormat24Hour()) { lv_obj_set_flex_grow(main_wrapper, 1);
lv_obj_add_state(time_24h_switch, LV_STATE_CHECKED);
} else { auto* region_wrapper = lv_obj_create(main_wrapper);
lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED); lv_obj_set_width(region_wrapper, LV_PCT(100));
lv_obj_set_height(region_wrapper, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(region_wrapper, 0, 0);
lv_obj_set_style_border_width(region_wrapper, 0, 0);
auto* region_prefix_label = lv_label_create(region_wrapper);
lv_label_set_text(region_prefix_label, "Region: ");
lv_obj_align(region_prefix_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* region_label = lv_label_create(region_wrapper);
std::string timeZoneName = time::getTimeZoneName();
if (timeZoneName.empty()) {
timeZoneName = "not set";
}
regionLabelWidget = region_label;
lv_label_set_text(region_label, timeZoneName.c_str());
// TODO: Find out why Y offset is needed
lv_obj_align_to(region_label, region_prefix_label, LV_ALIGN_OUT_RIGHT_MID, 0, 8);
auto* region_button = lv_button_create(region_wrapper);
lv_obj_align(region_button, LV_ALIGN_TOP_RIGHT, 0, 0);
auto* region_button_image = lv_image_create(region_button);
lv_obj_add_event_cb(region_button, onConfigureTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr);
lv_image_set_src(region_button_image, LV_SYMBOL_SETTINGS);
auto* time_format_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_width(time_format_wrapper, LV_PCT(100));
lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(time_format_wrapper, 0, 0);
lv_obj_set_style_border_width(time_format_wrapper, 0, 0);
auto* time_24h_label = lv_label_create(time_format_wrapper);
lv_label_set_text(time_24h_label, "24-hour clock");
lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* time_24h_switch = lv_switch_create(time_format_wrapper);
lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(time_24h_switch, onTimeFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr);
if (time::isTimeFormat24Hour()) {
lv_obj_add_state(time_24h_switch, LV_STATE_CHECKED);
} else {
lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED);
}
} }
}
static void onStart(AppContext& app) { 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);
TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str());
time::setTimeZone(name, code);
static void onResult(AppContext& app, Result result, const Bundle& bundle) { if (!name.empty()) {
if (result == ResultOk) { if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
auto data = std::static_pointer_cast<Data>(app.getData()); lv_label_set_text(regionLabelWidget, name.c_str());
auto name = timezone::getResultName(bundle); lvgl::unlock();
auto code = timezone::getResultCode(bundle); }
TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str());
time::setTimeZone(name, code);
if (!name.empty()) {
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
lv_label_set_text(data->regionLabelWidget, name.c_str());
lvgl::unlock();
} }
} }
} }
} };
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,178 +52,195 @@ 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();
}
data->updateTimer->start(500 / portTICK_PERIOD_MS); Mutex mutex;
data->mutex.unlock();
}
}
static void onListItemSelected(lv_event_t* e) {
auto index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e));
TT_LOG_I(TAG, "Selected item at index %zu", index);
auto* app = service::loader::getCurrentApp();
auto data = std::static_pointer_cast<Data>(app->getData());
auto& entry = data->entries[index];
auto bundle = std::make_shared<Bundle>();
setResultName(bundle, entry.name);
setResultCode(bundle, entry.code);
app->setResult(app::ResultOk, bundle);
service::loader::stopApp();
}
static void createListItem(lv_obj_t* list, const std::string& title, size_t index) {
lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str());
lv_obj_add_event_cb(btn, &onListItemSelected, LV_EVENT_SHORT_CLICKED, (void*)index);
}
static void readTimeZones(const std::shared_ptr<Data>& data, std::string filter) {
auto path = std::string(MOUNT_POINT_SYSTEM) + "/timezones.csv";
auto* file = fopen(path.c_str(), "rb");
if (file == nullptr) {
TT_LOG_E(TAG, "Failed to open %s", path.c_str());
return;
}
char line[96];
std::string name;
std::string code;
uint32_t count = 0;
std::vector<TimeZoneEntry> entries; std::vector<TimeZoneEntry> entries;
while (fgets(line, 96, file)) { std::unique_ptr<Timer> updateTimer;
if (parseEntry(line, name, code)) { lv_obj_t* listWidget = nullptr;
if (tt::string::lowercase(name).find(filter) != std::string::npos) { lv_obj_t* filterTextareaWidget = nullptr;
count++;
entries.push_back({
.name = name,
.code = code
});
// Safety guard static void onTextareaValueChangedCallback(TT_UNUSED lv_event_t* e) {
if (count > 50) { auto* app = (TimeZoneApp*)lv_event_get_user_data(e);
// TODO: Show warning that we're not displaying a complete list app->onTextareaValueChanged(e);
break; }
void onTextareaValueChanged(TT_UNUSED lv_event_t* e) {
if (mutex.lock(100 / portTICK_PERIOD_MS)) {
if (updateTimer->isRunning()) {
updateTimer->stop();
}
updateTimer->start(500 / portTICK_PERIOD_MS);
mutex.unlock();
}
}
static void onListItemSelectedCallback(lv_event_t* e) {
auto index = reinterpret_cast<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);
auto& entry = entries[index];
auto bundle = std::make_unique<Bundle>();
setResultName(*bundle, entry.name);
setResultCode(*bundle, entry.code);
setResult(app::Result::Ok, std::move(bundle));
service::loader::stopApp();
}
static void createListItem(lv_obj_t* list, const std::string& title, size_t index) {
lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str());
lv_obj_add_event_cb(btn, &onListItemSelectedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
}
static void updateTimerCallback(std::shared_ptr<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* file = fopen(path.c_str(), "rb");
if (file == nullptr) {
TT_LOG_E(TAG, "Failed to open %s", path.c_str());
return;
}
char line[96];
std::string name;
std::string code;
uint32_t count = 0;
std::vector<TimeZoneEntry> new_entries;
while (fgets(line, 96, file)) {
if (parseEntry(line, name, code)) {
if (tt::string::lowercase(name).find(filter) != std::string::npos) {
count++;
new_entries.push_back({.name = name, .code = code});
// Safety guard
if (count > 50) {
// TODO: Show warning that we're not displaying a complete list
break;
}
} }
} else {
TT_LOG_E(TAG, "Parse error at line %lu", count);
} }
}
fclose(file);
if (mutex.lock(100 / portTICK_PERIOD_MS)) {
entries = std::move(new_entries);
mutex.unlock();
} else { } else {
TT_LOG_E(TAG, "Parse error at line %lu", count); TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
} }
TT_LOG_I(TAG, "Processed %lu entries", count);
} }
fclose(file); void updateList() {
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
std::string filter = tt::string::lowercase(std::string(lv_textarea_get_text(filterTextareaWidget)));
readTimeZones(filter);
lvgl::unlock();
} else {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
return;
}
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) { if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
data->entries = std::move(entries); if (mutex.lock(100 / portTICK_PERIOD_MS)) {
data->mutex.unlock(); lv_obj_clean(listWidget);
} else {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
}
TT_LOG_I(TAG, "Processed %lu entries", count); uint32_t index = 0;
} for (auto& entry : entries) {
createListItem(listWidget, entry.name, index);
index++;
}
static void updateList(std::shared_ptr<Data>& data) { mutex.unlock();
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
std::string filter = tt::string::lowercase(std::string(lv_textarea_get_text(data->filterTextareaWidget)));
readTimeZones(data, filter);
lvgl::unlock();
} else {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
return;
}
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) {
lv_obj_clean(data->listWidget);
uint32_t index = 0;
for (auto& entry : data->entries) {
createListItem(data->listWidget, entry.name, index);
index++;
} }
data->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());
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); void onShow(AppContext& app, lv_obj_t* parent) override {
lvgl::toolbar_create(parent, app); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app);
auto* search_wrapper = lv_obj_create(parent); auto* search_wrapper = lv_obj_create(parent);
lv_obj_set_size(search_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(search_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(search_wrapper, LV_FLEX_FLOW_ROW); lv_obj_set_flex_flow(search_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(search_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); lv_obj_set_flex_align(search_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START);
lv_obj_set_style_pad_all(search_wrapper, 0, 0); lv_obj_set_style_pad_all(search_wrapper, 0, 0);
lv_obj_set_style_border_width(search_wrapper, 0, 0); lv_obj_set_style_border_width(search_wrapper, 0, 0);
auto* icon = lv_image_create(search_wrapper); auto* icon = lv_image_create(search_wrapper);
lv_obj_set_style_margin_left(icon, 8, 0); lv_obj_set_style_margin_left(icon, 8, 0);
lv_obj_set_style_image_recolor_opa(icon, 255, 0); lv_obj_set_style_image_recolor_opa(icon, 255, 0);
lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0); lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0);
std::string icon_path = app.getPaths()->getSystemPathLvgl("search.png"); std::string icon_path = app.getPaths()->getSystemPathLvgl("search.png");
lv_image_set_src(icon, icon_path.c_str()); lv_image_set_src(icon, icon_path.c_str());
lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0); lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0);
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);
auto* list = lv_list_create(parent); auto* list = lv_list_create(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,36 +13,38 @@ 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 {
auto* toolbar = lvgl::toolbar_create(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
if (hal::usb::canRebootIntoMassStorageSdmmc()) { void onShow(AppContext& app, lv_obj_t* parent) override {
auto* button = lv_button_create(parent); auto* toolbar = lvgl::toolbar_create(parent, app);
auto* label = lv_label_create(button); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_label_set_text(label, "Reboot as USB storage");
lv_obj_align(button, LV_ALIGN_CENTER, 0, 0); if (hal::usb::canRebootIntoMassStorageSdmmc()) {
lv_obj_add_event_cb(button, onRebootMassStorage, LV_EVENT_SHORT_CLICKED, nullptr); auto* button = lv_button_create(parent);
} else { auto* label = lv_label_create(button);
bool supported = hal::usb::isSupported(); lv_label_set_text(label, "Reboot as USB storage");
const char* first = supported ? "USB storage not available:" : "USB driver not supported"; lv_obj_align(button, LV_ALIGN_CENTER, 0, 0);
const char* second = supported ? "SD card not mounted" : "on this hardware"; lv_obj_add_event_cb(button, onRebootMassStorage, LV_EVENT_SHORT_CLICKED, nullptr);
auto* label_a = lv_label_create(parent); } else {
lv_label_set_text(label_a, first); bool supported = hal::usb::isSupported();
lv_obj_align(label_a, LV_ALIGN_CENTER, 0, 0); const char* first = supported ? "USB storage not available:" : "USB driver not supported";
auto* label_b = lv_label_create(parent); const char* second = supported ? "SD card not mounted" : "on this hardware";
lv_label_set_text(label_b, second); auto* label_a = lv_label_create(parent);
lv_obj_align_to(label_b, label_a, LV_ALIGN_OUT_BOTTOM_MID, 0, 4); lv_label_set_text(label_a, first);
lv_obj_align(label_a, LV_ALIGN_CENTER, 0, 0);
auto* label_b = lv_label_create(parent);
lv_label_set_text(label_b, second);
lv_obj_align_to(label_b, label_a, LV_ALIGN_OUT_BOTTOM_MID, 0, 4);
}
} }
};
}
extern const AppManifest manifest = { 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,96 +66,98 @@ static void onToggleAutoConnect(lv_event_t* event) {
} }
} }
static void onShow(AppContext& app, lv_obj_t* parent) { class WifiApSettings : public App {
auto paremeters = app.getParameters();
tt_check(paremeters != nullptr, "Parameters missing");
std::string ssid = paremeters->getString("ssid");
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); void onShow(AppContext& app, lv_obj_t* parent) override {
lvgl::toolbar_create(parent, ssid); auto paremeters = app.getParameters();
tt_check(paremeters != nullptr, "Parameters missing");
std::string ssid = paremeters->getString("ssid");
// Wrappers lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, ssid);
lv_obj_t* wrapper = lv_obj_create(parent); // Wrappers
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_bg_invisible(wrapper);
// Auto-connect toggle lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lvgl::obj_set_style_bg_invisible(wrapper);
lv_obj_t* auto_connect_wrapper = lv_obj_create(wrapper); // Auto-connect toggle
lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lvgl::obj_set_style_no_padding(auto_connect_wrapper);
lv_obj_set_style_border_width(auto_connect_wrapper, 0, 0);
lv_obj_t* auto_connect_label = lv_label_create(auto_connect_wrapper); lv_obj_t* auto_connect_wrapper = lv_obj_create(wrapper);
lv_label_set_text(auto_connect_label, "Auto-connect"); lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_align(auto_connect_label, LV_ALIGN_TOP_LEFT, 0, 6); lvgl::obj_set_style_no_padding(auto_connect_wrapper);
lv_obj_set_style_border_width(auto_connect_wrapper, 0, 0);
lv_obj_t* auto_connect_switch = lv_switch_create(auto_connect_wrapper); lv_obj_t* auto_connect_label = lv_label_create(auto_connect_wrapper);
lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, (void*)&paremeters); lv_label_set_text(auto_connect_label, "Auto-connect");
lv_obj_align(auto_connect_switch, LV_ALIGN_TOP_RIGHT, 0, 0); lv_obj_align(auto_connect_label, LV_ALIGN_TOP_LEFT, 0, 6);
lv_obj_t* forget_button = lv_button_create(wrapper); lv_obj_t* auto_connect_switch = lv_switch_create(auto_connect_wrapper);
lv_obj_set_width(forget_button, LV_PCT(100)); lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, (void*)&paremeters);
lv_obj_align_to(forget_button, auto_connect_wrapper, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); lv_obj_align(auto_connect_switch, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, nullptr);
lv_obj_t* forget_button_label = lv_label_create(forget_button);
lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(forget_button_label, "Forget");
service::wifi::settings::WifiApSettings settings {}; lv_obj_t* forget_button = lv_button_create(wrapper);
if (service::wifi::settings::load(ssid.c_str(), &settings)) { lv_obj_set_width(forget_button, LV_PCT(100));
if (settings.auto_connect) { lv_obj_align_to(forget_button, auto_connect_wrapper, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED); lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, nullptr);
lv_obj_t* forget_button_label = lv_label_create(forget_button);
lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(forget_button_label, "Forget");
service::wifi::settings::WifiApSettings settings {};
if (service::wifi::settings::load(ssid.c_str(), &settings)) {
if (settings.auto_connect) {
lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED);
} else {
lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED);
}
} else { } else {
lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED); TT_LOG_W(TAG, "No settings found");
lv_obj_add_flag(forget_button, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(auto_connect_wrapper, LV_OBJ_FLAG_HIDDEN);
} }
} else {
TT_LOG_W(TAG, "No settings found");
lv_obj_add_flag(forget_button, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(auto_connect_wrapper, LV_OBJ_FLAG_HIDDEN);
} }
}
void onResult(TT_UNUSED AppContext& app, TT_UNUSED Result result, const Bundle& bundle) { 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;
}
auto parameters = app->getParameters();
tt_check(parameters != nullptr, "Parameters missing");
std::string ssid = parameters->getString("ssid");
if (service::wifi::settings::remove(ssid.c_str())) {
TT_LOG_I(TAG, "Removed SSID");
if (
service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive &&
service::wifi::getConnectionTarget() == ssid
) {
service::wifi::disconnect();
} }
// Stop self auto parameters = app->getParameters();
service::loader::stopApp(); tt_check(parameters != nullptr, "Parameters missing");
} else {
TT_LOG_E(TAG, "Failed to remove SSID"); std::string ssid = parameters->getString("ssid");
if (service::wifi::settings::remove(ssid.c_str())) {
TT_LOG_I(TAG, "Removed SSID");
if (
service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive &&
service::wifi::getConnectionTarget() == ssid
) {
service::wifi::disconnect();
}
// Stop self
service::loader::stopApp();
} else {
TT_LOG_E(TAG, "Failed to remove SSID");
}
} }
} }
} };
extern const AppManifest manifest = { 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) {