mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-04-23 03:45:05 +00:00
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:
parent
2bbd44a8b5
commit
c3bcf93698
@ -1,7 +1,12 @@
|
||||
#include "app/AppManifest.h"
|
||||
#include "lvgl.h"
|
||||
#include "lvgl/Toolbar.h"
|
||||
|
||||
static void onShow(tt::app::AppContext& context, lv_obj_t* parent) {
|
||||
using namespace tt::app;
|
||||
|
||||
class HelloWorldApp : public App {
|
||||
|
||||
void onShow(AppContext& context, lv_obj_t* parent) override {
|
||||
lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
@ -9,9 +14,10 @@ static void onShow(tt::app::AppContext& context, lv_obj_t* parent) {
|
||||
lv_label_set_text(label, "Hello, world!");
|
||||
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
extern const tt::app::AppManifest hello_world_app = {
|
||||
extern const AppManifest hello_world_app = {
|
||||
.id = "HelloWorld",
|
||||
.name = "Hello World",
|
||||
.onShow = onShow,
|
||||
.createApp = create<HelloWorldApp>
|
||||
};
|
||||
|
||||
@ -3,12 +3,15 @@
|
||||
|
||||
#define TAG "bq24295"
|
||||
|
||||
/** Reference: https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.h?ref_type=heads */
|
||||
/** Reference:
|
||||
* https://www.ti.com/lit/ds/symlink/bq24295.pdf
|
||||
* https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.h?ref_type=heads
|
||||
*/
|
||||
namespace registers {
|
||||
static const uint8_t WATCHDOG = 0x05U; // Charge end/timer cntrl
|
||||
static const uint8_t OPERATION_CONTROL = 0x07U; // Misc operation control
|
||||
static const uint8_t STATUS = 0x08U; // System status
|
||||
static const uint8_t VERSION = 0x0AU; // Vendor/part/revision status
|
||||
static const uint8_t WATCHDOG = 0x05U; // Datasheet page 35: Charge end/timer cntrl
|
||||
static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control
|
||||
static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status
|
||||
static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status
|
||||
} // namespace registers
|
||||
|
||||
// region Watchdog
|
||||
|
||||
@ -8,4 +8,4 @@ Please open an [Issue](https://github.com/ByteWelder/Tactility/issues/new) on Gi
|
||||
|
||||
# Code Style
|
||||
|
||||
See [this document](CODING_STYLE.md).
|
||||
See [this document](CODING_STYLE.md) and [.clang-format](.clang-format).
|
||||
|
||||
@ -9,33 +9,33 @@
|
||||
- Add statusbar icon for memory pressure.
|
||||
- Show error in WiFi screen (e.g. AlertDialog when SPI is not enabled and available memory is below a certain amount)
|
||||
- Clean up static_cast when casting to base class.
|
||||
- M5Stack CoreS3 SD card mounts, but cannot be read. There is currently a notice about it [here](https://github.com/espressif/esp-bsp/blob/master/bsp/m5stack_core_s3/README.md).
|
||||
- EventFlag: Fix return value of set/get/wait (the errors are weirdly mixed in)
|
||||
- Consistently use either ESP_TARGET or ESP_PLATFORM
|
||||
- tt_check() failure during app argument bundle nullptr check seems to trigger SIGSEGV
|
||||
- Fix bug in T-Deck/etc: esp_lvgl_port settings has a large stack size (~9kB) to fix an issue where the T-Deck would get a stackoverflow. This sometimes happens when WiFi is auto-enabled and you open the app while it is still connecting.
|
||||
- M5Stack Core only shows 4MB of SPIRAM in use
|
||||
- Try to improve Core2 and CoreS3 performance by setting swap_bytes of display driver to false (this is a software operation on the display buffer!) and use 24 bit colour mode if needed
|
||||
- Files app: When SD card is not mounted, don't show it
|
||||
- Crash log must mention board type
|
||||
- Oops crashlog site: Add copy-pasteable addr2line command (e.g. xtensa-esp32s3-elf-addr2line -pfiaC -e Tactility.elf 00000000)
|
||||
|
||||
# TODOs
|
||||
- Experiment with what happens when using C++ code in an external app (without using standard library!)
|
||||
- Get rid of "ESP_TARGET" and use official "ESP_PLATFORM"
|
||||
- SpiSdCard should use SDMMC_FREQ_DEFAULT by default
|
||||
- Boards' CMakeLists.txt manually declare each source folder. Update them all to do a recursive search of all folders.
|
||||
- We currently make all boards for a given platform (e.g. ESP32S3), but it's better to filter all irrelevant ones based on the Kconfig board settings:
|
||||
- We currently build all boards for a given platform (e.g. ESP32S3), but it's better to filter all irrelevant ones based on the Kconfig board settings:
|
||||
Projects will load and compile faster as it won't compile all the dependencies of all these other boards
|
||||
- Make a ledger for setting CPU affinity of various services and tasks
|
||||
- Make "blocking" argument the last one, and put it default to false (or remove it entirely?): void startApp(const std::string& id, bool blocking, std::shared_ptr<const Bundle> parameters) {
|
||||
- Boot hooks instead of a single boot method in config. Define different boot phases/levels in enum.
|
||||
- Add toggle to Display app for sysmon overlay: https://docs.lvgl.io/master/API/others/sysmon/index.html
|
||||
- CrashHandler: use "corrupted" flag
|
||||
- CrashHandler: process other types of crashes (WDT?)
|
||||
- Call tt::lvgl::isSyncSet after HAL init and show error (and crash?) when it is not set.
|
||||
- Create different partitions files for different ESP flash size targets (N4, N8, N16, N32)
|
||||
- Attach ELF data to wrapper app (as app data) (check that app state is "running"!) so you can run more than 1 external apps at a time.
|
||||
We'll need to keep track of all manifest instances, so that the wrapper can look up the relevant manifest for the relevant callbacks.
|
||||
- T-Deck: Clear screen before turning on blacklight
|
||||
- T-Deck: Use knob for UI selection
|
||||
- T-Deck: Use knob for UI selection?
|
||||
- Crash monitoring: Keep track of which system phase the app crashed in (e.g. which app in which state)
|
||||
- AppContext's onResult should pass the app id (or launch request id!) that was started, so we can differentiate between multiple types of apps being launched
|
||||
- App::onResult should pass the app id (or launch request id!) that was started, so we can differentiate between multiple types of apps being launched
|
||||
- Create more unit tests for `tactility-core` and `tactility` (PC-only for now)
|
||||
- Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials.
|
||||
- Show a warning screen when a user plugs in the SD card on a device that only supports mounting at boot.
|
||||
@ -47,6 +47,8 @@
|
||||
- Support hot-plugging SD card
|
||||
|
||||
# Nice-to-haves
|
||||
- CoreS3 has a hardware issue that prevents mounting SD cards while using the display too: allow USB Mass Storage to use `/data` instead? Perhaps give the USB settings app a drop down to select the root filesystem to attach.
|
||||
- Give external app a different icon. Allow an external app update their id, icon, type and name once they are running(, and persist that info?). Loader will need to be able to find app by (external) location.
|
||||
- Audio player app
|
||||
- Audio recording app
|
||||
- OTA updates
|
||||
@ -63,6 +65,8 @@
|
||||
- On crash, try to save current log to flash or SD card? (this is risky, though, so ask in Discord first)
|
||||
|
||||
# App Ideas
|
||||
- Weather app: https://lab.flipper.net/apps/flip_weather
|
||||
- wget app: https://lab.flipper.net/apps/web_crawler (add profiles for known public APIs?)
|
||||
- USB implementation to make device act as mass storage device.
|
||||
- System logger
|
||||
- BlueTooth keyboard app
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
* Note: LVGL and Tactility methods need to be exposed manually from TactilityC/Source/tt_init.cpp
|
||||
* Only C is supported for now (C++ symbols fail to link)
|
||||
*/
|
||||
static void onShow(AppContextHandle context, lv_obj_t* parent) {
|
||||
static void onShow(AppContextHandle context, void* data, lv_obj_t* parent) {
|
||||
lv_obj_t* toolbar = tt_lvgl_toolbar_create(parent, context);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
@ -14,15 +14,12 @@ static void onShow(AppContextHandle context, lv_obj_t* parent) {
|
||||
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
|
||||
}
|
||||
|
||||
ExternalAppManifest manifest = {
|
||||
.name = "Hello World",
|
||||
.onShow = onShow
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
tt_set_app_manifest(
|
||||
"Hello World",
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
onShow,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
tt_app_register(&manifest);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/AppContext.h"
|
||||
#include "app/AppManifest.h"
|
||||
#include "Bundle.h"
|
||||
#include "Mutex.h"
|
||||
#include "app/AppContext.h"
|
||||
#include "app/AppManifest.h"
|
||||
#include "app/ElfApp.h"
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
@ -17,19 +18,6 @@ typedef enum {
|
||||
StateStopped // App is not in memory
|
||||
} 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.
|
||||
*/
|
||||
@ -38,7 +26,7 @@ class AppInstance : public AppContext {
|
||||
private:
|
||||
|
||||
Mutex mutex = Mutex(Mutex::Type::Normal);
|
||||
const AppManifest& manifest;
|
||||
const std::shared_ptr<AppManifest> manifest;
|
||||
State state = StateInitial;
|
||||
Flags flags = { .showStatusbar = true };
|
||||
/** @brief Optional parameters to start the app with
|
||||
@ -52,16 +40,40 @@ private:
|
||||
* These manifest methods can optionally allocate/free data that is attached here.
|
||||
*/
|
||||
std::shared_ptr<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:
|
||||
|
||||
explicit AppInstance(const AppManifest& manifest) :
|
||||
manifest(manifest) {}
|
||||
|
||||
AppInstance(const AppManifest& manifest, std::shared_ptr<const Bundle> parameters) :
|
||||
explicit AppInstance(const std::shared_ptr<AppManifest>& 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;
|
||||
|
||||
@ -70,22 +82,15 @@ public:
|
||||
|
||||
const AppManifest& getManifest() const override;
|
||||
|
||||
Flags getFlags() const override;
|
||||
Flags getFlags() const;
|
||||
void setFlags(Flags flags);
|
||||
Flags& mutableFlags() { return flags; } // TODO: locking mechanism
|
||||
|
||||
std::shared_ptr<void> _Nullable getData() const override;
|
||||
void setData(std::shared_ptr<void> data) 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<ResultHolder>& getResult() { return resultHolder; }
|
||||
std::shared_ptr<App> getApp() const override { return app; }
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -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
|
||||
@ -35,7 +35,7 @@ public:
|
||||
void onRenamePressed();
|
||||
void onDeletePressed();
|
||||
void onDirEntryListScrollBegin();
|
||||
void onResult(Result result, const Bundle& bundle);
|
||||
void onResult(Result result, std::unique_ptr<Bundle> bundle);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -19,17 +19,7 @@ enum ScanState {
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@ -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
|
||||
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/App.h"
|
||||
#include "app/wificonnect/Bindings.h"
|
||||
#include "app/wificonnect/State.h"
|
||||
#include "app/wificonnect/View.h"
|
||||
@ -9,7 +10,10 @@
|
||||
|
||||
namespace tt::app::wificonnect {
|
||||
|
||||
class WifiConnect {
|
||||
class WifiConnect : public App {
|
||||
|
||||
private:
|
||||
|
||||
Mutex mutex;
|
||||
State state;
|
||||
Bindings bindings = {
|
||||
@ -28,8 +32,8 @@ public:
|
||||
void lock();
|
||||
void unlock();
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent);
|
||||
void onHide(AppContext& app);
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override;
|
||||
void onHide(AppContext& app) override;
|
||||
|
||||
State& getState() { return state; }
|
||||
Bindings& getBindings() { return bindings; }
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/App.h"
|
||||
#include "Mutex.h"
|
||||
#include "./View.h"
|
||||
#include "./State.h"
|
||||
@ -7,7 +8,9 @@
|
||||
|
||||
namespace tt::app::wifimanage {
|
||||
|
||||
class WifiManage {
|
||||
class WifiManage : public App {
|
||||
|
||||
private:
|
||||
|
||||
PubSubSubscription* wifiSubscription = nullptr;
|
||||
Mutex mutex;
|
||||
@ -23,8 +26,8 @@ public:
|
||||
void lock();
|
||||
void unlock();
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent);
|
||||
void onHide(AppContext& app);
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override;
|
||||
void onHide(AppContext& app) override;
|
||||
|
||||
Bindings& getBindings() { return bindings; }
|
||||
State& getState() { return state; }
|
||||
|
||||
@ -4,9 +4,8 @@
|
||||
#include "Mutex.h"
|
||||
#include "Pubsub.h"
|
||||
#include "service/gui/Gui.h"
|
||||
#include "service/gui/ViewPort.h"
|
||||
#include "service/gui/ViewPort_i.h"
|
||||
#include <cstdio>
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::service::gui {
|
||||
|
||||
@ -27,7 +26,7 @@ struct Gui {
|
||||
lv_obj_t* statusbarWidget = nullptr;
|
||||
|
||||
// App-specific
|
||||
ViewPort* appViewPort = nullptr;
|
||||
std::shared_ptr<app::AppContext> appToRender = nullptr;
|
||||
|
||||
lv_obj_t* _Nullable keyboard = nullptr;
|
||||
lv_group_t* keyboardGroup = nullptr;
|
||||
|
||||
@ -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
|
||||
@ -6,7 +6,6 @@
|
||||
#include "MessageQueue.h"
|
||||
#include "Pubsub.h"
|
||||
#include "Thread.h"
|
||||
#include "service/gui/ViewPort.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "RtosCompatSemaphore.h"
|
||||
#include <stack>
|
||||
@ -25,31 +24,9 @@ typedef enum {
|
||||
LoaderEventTypeApplicationStopped
|
||||
} LoaderEventType;
|
||||
|
||||
typedef struct {
|
||||
app::AppInstance& app;
|
||||
} LoaderEventAppStarted;
|
||||
|
||||
typedef struct {
|
||||
app::AppInstance& app;
|
||||
} LoaderEventAppShowing;
|
||||
|
||||
typedef struct {
|
||||
app::AppInstance& app;
|
||||
} LoaderEventAppHiding;
|
||||
|
||||
typedef struct {
|
||||
const app::AppManifest& manifest;
|
||||
} LoaderEventAppStopped;
|
||||
|
||||
typedef struct {
|
||||
struct LoaderEvent {
|
||||
LoaderEventType type;
|
||||
union {
|
||||
LoaderEventAppStarted app_started;
|
||||
LoaderEventAppShowing app_showing;
|
||||
LoaderEventAppHiding app_hiding;
|
||||
LoaderEventAppStopped app_stopped;
|
||||
};
|
||||
} LoaderEvent;
|
||||
|
||||
// endregion LoaderEvent
|
||||
|
||||
@ -77,10 +54,9 @@ public:
|
||||
// endregion LoaderMessage
|
||||
|
||||
struct Loader {
|
||||
std::shared_ptr<PubSub> pubsubInternal = std::make_shared<PubSub>();
|
||||
std::shared_ptr<PubSub> pubsubExternal = std::make_shared<PubSub>();
|
||||
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.
|
||||
* This includes full LVGL redraw via Gui::redraw()
|
||||
*/
|
||||
|
||||
@ -64,7 +64,6 @@ namespace app {
|
||||
namespace screenshot { extern const AppManifest manifest; }
|
||||
#endif
|
||||
#ifdef ESP_PLATFORM
|
||||
extern const AppManifest elfWrapperManifest;
|
||||
namespace crashdiagnostics { extern const AppManifest manifest; }
|
||||
#endif
|
||||
}
|
||||
@ -99,8 +98,7 @@ static const std::vector<const app::AppManifest*> system_apps = {
|
||||
&app::screenshot::manifest,
|
||||
#endif
|
||||
#ifdef ESP_PLATFORM
|
||||
&app::crashdiagnostics::manifest,
|
||||
&app::elfWrapperManifest, // For hot-loading ELF apps
|
||||
&app::crashdiagnostics::manifest
|
||||
#endif
|
||||
};
|
||||
|
||||
@ -109,11 +107,11 @@ static const std::vector<const app::AppManifest*> system_apps = {
|
||||
static void register_system_apps() {
|
||||
TT_LOG_I(TAG, "Registering default apps");
|
||||
for (const auto* app_manifest: system_apps) {
|
||||
addApp(app_manifest);
|
||||
addApp(*app_manifest);
|
||||
}
|
||||
|
||||
if (getConfiguration()->hardware->power != nullptr) {
|
||||
addApp(&app::power::manifest);
|
||||
addApp(app::power::manifest);
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +119,7 @@ static void register_user_apps(const std::vector<const app::AppManifest*>& apps)
|
||||
TT_LOG_I(TAG, "Registering user apps");
|
||||
for (auto* manifest : apps) {
|
||||
assert(manifest != nullptr);
|
||||
addApp(manifest);
|
||||
addApp(*manifest);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
78
Tactility/Source/app/App.h
Normal file
78
Tactility/Source/app/App.h
Normal 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); }
|
||||
|
||||
}
|
||||
109
Tactility/Source/app/AppCompatC.h
Normal file
109
Tactility/Source/app/AppCompatC.h
Normal 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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,12 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppManifest.h"
|
||||
#include "Bundle.h"
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
// Forward declarations
|
||||
class App;
|
||||
class Paths;
|
||||
struct AppManifest;
|
||||
enum class Result;
|
||||
|
||||
typedef union {
|
||||
struct {
|
||||
@ -28,14 +31,10 @@ protected:
|
||||
public:
|
||||
|
||||
virtual const AppManifest& getManifest() const = 0;
|
||||
virtual std::shared_ptr<void> _Nullable getData() const = 0;
|
||||
virtual void setData(std::shared_ptr<void> data) = 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::shared_ptr<App> getApp() const = 0;
|
||||
};
|
||||
|
||||
class Paths {
|
||||
|
||||
@ -25,7 +25,8 @@ State AppInstance::getState() const {
|
||||
* Consider not exposing bundle, but expose `app_get_bundle_int(key)` methods with locking in it.
|
||||
*/
|
||||
const AppManifest& AppInstance::getManifest() const {
|
||||
return manifest;
|
||||
tt_assert(manifest != nullptr);
|
||||
return *manifest;
|
||||
}
|
||||
|
||||
Flags AppInstance::getFlags() const {
|
||||
@ -41,19 +42,6 @@ void AppInstance::setFlags(Flags newFlags) {
|
||||
mutex.release();
|
||||
}
|
||||
|
||||
std::shared_ptr<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 {
|
||||
mutex.acquire(TtWaitForever);
|
||||
std::shared_ptr<const Bundle> result = parameters;
|
||||
@ -61,29 +49,9 @@ std::shared_ptr<const Bundle> AppInstance::getParameters() const {
|
||||
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 {
|
||||
return std::make_unique<AppInstancePaths>(manifest);
|
||||
tt_assert(manifest != nullptr);
|
||||
return std::make_unique<AppInstancePaths>(*manifest);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -1,44 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <Bundle.h>
|
||||
#include "CoreDefines.h"
|
||||
#include "ManifestRegistry.h"
|
||||
#include <Bundle.h>
|
||||
#include <string>
|
||||
|
||||
// Forward declarations
|
||||
typedef struct _lv_obj_t lv_obj_t;
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
class App;
|
||||
class AppContext;
|
||||
|
||||
/** Application types */
|
||||
enum Type {
|
||||
enum class Type {
|
||||
/** Boot screen, shown before desktop is launched. */
|
||||
TypeBoot,
|
||||
Boot,
|
||||
/** A launcher app sits at the root of the app stack after the boot splash is finished */
|
||||
TypeLauncher,
|
||||
Launcher,
|
||||
/** Apps that generally aren't started from the desktop (e.g. image viewer) */
|
||||
TypeHidden,
|
||||
Hidden,
|
||||
/** Standard apps, provided by the system. */
|
||||
TypeSystem,
|
||||
System,
|
||||
/** The apps that are launched/shown by the Settings app. The Settings app itself is of type AppTypeSystem. */
|
||||
TypeSettings,
|
||||
Settings,
|
||||
/** User-provided apps. */
|
||||
TypeUser
|
||||
User
|
||||
};
|
||||
|
||||
/** Result status code for application result callback. */
|
||||
typedef enum {
|
||||
ResultOk,
|
||||
ResultCancelled,
|
||||
ResultError
|
||||
} Result;
|
||||
enum class Result {
|
||||
Ok = 0U,
|
||||
Cancelled = 1U,
|
||||
Error = 2U
|
||||
};
|
||||
|
||||
typedef void (*AppOnStart)(AppContext& app);
|
||||
typedef void (*AppOnStop)(AppContext& app);
|
||||
typedef void (*AppOnShow)(AppContext& app, lv_obj_t* parent);
|
||||
typedef void (*AppOnHide)(AppContext& app);
|
||||
typedef void (*AppOnResult)(AppContext& app, Result result, const Bundle& resultData);
|
||||
class Location {
|
||||
|
||||
private:
|
||||
|
||||
std::string path;
|
||||
Location() = default;
|
||||
explicit Location(const std::string& path) : path(path) {}
|
||||
|
||||
public:
|
||||
|
||||
static Location internal() { return {}; }
|
||||
|
||||
static Location external(const std::string& path) {
|
||||
return Location(path);
|
||||
}
|
||||
|
||||
bool isInternal() const { return path.empty(); }
|
||||
bool isExternal() const { return !path.empty(); }
|
||||
const std::string& getPath() const { return path; }
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<App>(*CreateApp)();
|
||||
|
||||
struct AppManifest {
|
||||
/** The identifier by which the app is launched by the system and other apps. */
|
||||
@ -51,26 +70,17 @@ struct AppManifest {
|
||||
std::string icon = {};
|
||||
|
||||
/** App type affects launch behaviour. */
|
||||
Type type = TypeUser;
|
||||
Type type = Type::User;
|
||||
|
||||
/** Non-blocking method to call when app is started. */
|
||||
AppOnStart onStart = nullptr;
|
||||
/** Where the app is located */
|
||||
Location location = Location::internal();
|
||||
|
||||
/** Non-blocking method to call when app is stopped. */
|
||||
AppOnStop _Nullable onStop = nullptr;
|
||||
|
||||
/** Non-blocking method to create the GUI. */
|
||||
AppOnShow _Nullable onShow = nullptr;
|
||||
|
||||
/** Non-blocking method, called before gui is destroyed. */
|
||||
AppOnHide _Nullable onHide = nullptr;
|
||||
|
||||
/** Handle the result for apps that are launched. */
|
||||
AppOnResult _Nullable onResult = nullptr;
|
||||
/** Create the instance of the app */
|
||||
CreateApp createApp = nullptr;
|
||||
};
|
||||
|
||||
struct {
|
||||
bool operator()(const AppManifest* left, const AppManifest* right) const { return left->name < right->name; }
|
||||
bool operator()(const std::shared_ptr<AppManifest>& left, const std::shared_ptr<AppManifest>& right) const { return left->name < right->name; }
|
||||
} SortAppManifestByName;
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -1,24 +1,49 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "file/File.h"
|
||||
#include "ElfApp.h"
|
||||
#include "Log.h"
|
||||
#include "StringUtils.h"
|
||||
#include "TactilityCore.h"
|
||||
#include "esp_elf.h"
|
||||
|
||||
#include "file/File.h"
|
||||
#include "service/loader/Loader.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
#define TAG "elf_app"
|
||||
#define ELF_WRAPPER_APP_ID "ElfWrapper"
|
||||
|
||||
struct ElfManifest {
|
||||
/** The user-readable name of the app. Used in UI. */
|
||||
std::string name;
|
||||
/** Optional icon. */
|
||||
std::string icon;
|
||||
CreateData _Nullable createData;
|
||||
DestroyData _Nullable destroyData;
|
||||
OnStart _Nullable onStart;
|
||||
OnStop _Nullable onStop;
|
||||
OnShow _Nullable onShow;
|
||||
OnHide _Nullable onHide;
|
||||
OnResult _Nullable onResult;
|
||||
};
|
||||
|
||||
static size_t elfManifestSetCount = 0;
|
||||
static ElfManifest elfManifest;
|
||||
|
||||
class ElfApp : public App {
|
||||
|
||||
private:
|
||||
|
||||
const std::string filePath;
|
||||
std::unique_ptr<uint8_t[]> elfFileData;
|
||||
esp_elf_t elf;
|
||||
bool shouldCleanupElf = false; // Whether we have to clean up the above "elf" object
|
||||
std::unique_ptr<ElfManifest> manifest;
|
||||
void* data = nullptr;
|
||||
|
||||
bool startElfApp(const std::string& filePath) {
|
||||
bool startElf() {
|
||||
TT_LOG_I(TAG, "Starting ELF %s", filePath.c_str());
|
||||
|
||||
assert(elfFileData == nullptr);
|
||||
|
||||
size_t size = 0;
|
||||
@ -29,6 +54,7 @@ bool startElfApp(const std::string& filePath) {
|
||||
|
||||
if (esp_elf_init(&elf) < 0) {
|
||||
TT_LOG_E(TAG, "Failed to initialize");
|
||||
shouldCleanupElf = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -40,87 +66,133 @@ bool startElfApp(const std::string& filePath) {
|
||||
int argc = 0;
|
||||
char* argv[] = {};
|
||||
|
||||
size_t manifest_set_count = elfManifestSetCount;
|
||||
if (esp_elf_request(&elf, 0, argc, argv) < 0) {
|
||||
TT_LOG_W(TAG, "Executable returned error code");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (elfManifestSetCount > manifest_set_count) {
|
||||
service::loader::startApp(ELF_WRAPPER_APP_ID);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "App did not set manifest to run - cleaning up ELF");
|
||||
esp_elf_deinit(&elf);
|
||||
elfFileData = nullptr;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void onStart(AppContext& app) {}
|
||||
static void onStop(AppContext& app) {}
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {}
|
||||
static void onHide(AppContext& app) {}
|
||||
static void onResult(AppContext& app, Result result, const Bundle& resultBundle) {}
|
||||
void stopElf() {
|
||||
TT_LOG_I(TAG, "Cleaning up ELF");
|
||||
|
||||
AppManifest elfManifest = {
|
||||
.id = "",
|
||||
.name = "",
|
||||
.type = TypeHidden,
|
||||
if (shouldCleanupElf) {
|
||||
esp_elf_deinit(&elf);
|
||||
}
|
||||
|
||||
if (elfFileData != nullptr) {
|
||||
elfFileData = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
explicit ElfApp(const std::string& filePath) : filePath(filePath) {}
|
||||
|
||||
void onStart(AppContext& appContext) override {
|
||||
auto initial_count = elfManifestSetCount;
|
||||
if (startElf()) {
|
||||
if (elfManifestSetCount > initial_count) {
|
||||
manifest = std::make_unique<ElfManifest>(elfManifest);
|
||||
|
||||
if (manifest->createData != nullptr) {
|
||||
data = manifest->createData();
|
||||
}
|
||||
|
||||
if (manifest->onStart != nullptr) {
|
||||
manifest->onStart(appContext, data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
service::loader::stopApp();
|
||||
}
|
||||
}
|
||||
|
||||
void onStop(AppContext& appContext) override {
|
||||
TT_LOG_I(TAG, "Cleaning up app");
|
||||
if (manifest != nullptr) {
|
||||
if (manifest->onStop != nullptr) {
|
||||
manifest->onStop(appContext, data);
|
||||
}
|
||||
|
||||
if (manifest->destroyData != nullptr && data != nullptr) {
|
||||
manifest->destroyData(data);
|
||||
}
|
||||
|
||||
this->manifest = nullptr;
|
||||
}
|
||||
stopElf();
|
||||
}
|
||||
|
||||
void onShow(AppContext& appContext, lv_obj_t* parent) override {
|
||||
if (manifest != nullptr && manifest->onShow != nullptr) {
|
||||
manifest->onShow(appContext, data, parent);
|
||||
}
|
||||
}
|
||||
|
||||
void onHide(AppContext& appContext) override {
|
||||
if (manifest != nullptr && manifest->onHide != nullptr) {
|
||||
manifest->onHide(appContext, data);
|
||||
}
|
||||
}
|
||||
|
||||
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> resultBundle) override {
|
||||
if (manifest != nullptr && manifest->onResult != nullptr) {
|
||||
manifest->onResult(appContext, data, result, std::move(resultBundle));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void setElfAppManifest(
|
||||
const char* name,
|
||||
const char* _Nullable icon,
|
||||
CreateData _Nullable createData,
|
||||
DestroyData _Nullable destroyData,
|
||||
OnStart _Nullable onStart,
|
||||
OnStop _Nullable onStop,
|
||||
OnShow _Nullable onShow,
|
||||
OnHide _Nullable onHide,
|
||||
OnResult _Nullable onResult
|
||||
) {
|
||||
elfManifest = ElfManifest {
|
||||
.name = name ? name : "",
|
||||
.icon = icon ? icon : "",
|
||||
.createData = createData,
|
||||
.destroyData = destroyData,
|
||||
.onStart = onStart,
|
||||
.onStop = onStop,
|
||||
.onShow = onShow,
|
||||
.onHide = onHide,
|
||||
.onResult = onResult
|
||||
};
|
||||
|
||||
static void onStartWrapper(AppContext& app) {
|
||||
elfManifest.onStart(app);
|
||||
}
|
||||
|
||||
static void onStopWrapper(AppContext& app) {
|
||||
elfManifest.onStop(app);
|
||||
TT_LOG_I(TAG, "Cleaning up ELF");
|
||||
esp_elf_deinit(&elf);
|
||||
elfFileData = nullptr;
|
||||
}
|
||||
|
||||
static void onShowWrapper(AppContext& app, lv_obj_t* parent) {
|
||||
elfManifest.onShow(app, parent);
|
||||
}
|
||||
|
||||
static void onHideWrapper(AppContext& app) {
|
||||
elfManifest.onHide(app);
|
||||
}
|
||||
|
||||
static void onResultWrapper(AppContext& app, Result result, const Bundle& bundle) {
|
||||
elfManifest.onResult(app, result, bundle);
|
||||
}
|
||||
|
||||
AppManifest elfWrapperManifest = {
|
||||
.id = ELF_WRAPPER_APP_ID,
|
||||
.name = "ELF Wrapper",
|
||||
.type = TypeHidden,
|
||||
.onStart = onStartWrapper,
|
||||
.onStop = onStopWrapper,
|
||||
.onShow = onShowWrapper,
|
||||
.onHide = onHideWrapper,
|
||||
.onResult = onResultWrapper
|
||||
};
|
||||
|
||||
void setElfAppManifest(const AppManifest& manifest) {
|
||||
elfManifest.id = manifest.id;
|
||||
elfManifest.name = manifest.name;
|
||||
elfWrapperManifest.name = manifest.name;
|
||||
elfManifest.onStart = manifest.onStart;
|
||||
elfManifest.onStop = manifest.onStop;
|
||||
elfManifest.onShow = manifest.onShow;
|
||||
elfManifest.onHide = manifest.onHide;
|
||||
elfManifest.onResult = manifest.onResult;
|
||||
|
||||
elfManifestSetCount++;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -1,16 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppCompatC.h"
|
||||
#include "AppManifest.h"
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
bool startElfApp(const std::string& filePath);
|
||||
void setElfAppManifest(
|
||||
const char* name,
|
||||
const char* _Nullable icon,
|
||||
CreateData _Nullable createData,
|
||||
DestroyData _Nullable destroyData,
|
||||
OnStart _Nullable onStart,
|
||||
OnStop _Nullable onStop,
|
||||
OnShow _Nullable onShow,
|
||||
OnHide _Nullable onHide,
|
||||
OnResult _Nullable onResult
|
||||
);
|
||||
|
||||
void setElfAppManifest(const AppManifest& manifest);
|
||||
/**
|
||||
* @return the app ID based on the executable's file path.
|
||||
*/
|
||||
std::string getElfAppId(const std::string& filePath);
|
||||
|
||||
/**
|
||||
* @return true when registration was done, false when app was already registered
|
||||
*/
|
||||
bool registerElfApp(const std::string& filePath);
|
||||
|
||||
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest);
|
||||
|
||||
}
|
||||
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
|
||||
@ -1,40 +1,44 @@
|
||||
#include "ManifestRegistry.h"
|
||||
#include "Mutex.h"
|
||||
#include "TactilityCore.h"
|
||||
#include "AppManifest.h"
|
||||
#include <unordered_map>
|
||||
|
||||
#define TAG "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 Mutex hash_mutex(Mutex::Type::Normal);
|
||||
|
||||
void addApp(const AppManifest* manifest) {
|
||||
TT_LOG_I(TAG, "Registering manifest %s", manifest->id.c_str());
|
||||
void addApp(const AppManifest& manifest) {
|
||||
TT_LOG_I(TAG, "Registering manifest %s", manifest.id.c_str());
|
||||
|
||||
hash_mutex.acquire(TtWaitForever);
|
||||
|
||||
if (app_manifest_map[manifest->id] == nullptr) {
|
||||
app_manifest_map[manifest->id] = manifest;
|
||||
if (!app_manifest_map.contains(manifest.id)) {
|
||||
app_manifest_map[manifest.id] = std::make_shared<AppManifest>(manifest);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "App id in use: %s", manifest->id.c_str());
|
||||
TT_LOG_E(TAG, "App id in use: %s", manifest.id.c_str());
|
||||
}
|
||||
|
||||
hash_mutex.release();
|
||||
}
|
||||
|
||||
_Nullable const AppManifest * findAppById(const std::string& id) {
|
||||
_Nullable std::shared_ptr<AppManifest> findAppById(const std::string& id) {
|
||||
hash_mutex.acquire(TtWaitForever);
|
||||
_Nullable const AppManifest* result = app_manifest_map[id.c_str()];
|
||||
auto result = app_manifest_map.find(id);
|
||||
hash_mutex.release();
|
||||
return result;
|
||||
if (result != app_manifest_map.end()) {
|
||||
return result->second;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<const AppManifest*> getApps() {
|
||||
std::vector<const AppManifest*> manifests;
|
||||
std::vector<std::shared_ptr<AppManifest>> getApps() {
|
||||
std::vector<std::shared_ptr<AppManifest>> manifests;
|
||||
hash_mutex.acquire(TtWaitForever);
|
||||
for (const auto& item: app_manifest_map) {
|
||||
manifests.push_back(item.second);
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppManifest.h"
|
||||
#include "App.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
struct AppManifest;
|
||||
|
||||
/** Register an application with its manifest */
|
||||
void addApp(const AppManifest* manifest);
|
||||
void addApp(const AppManifest& manifest);
|
||||
|
||||
/** Find an application manifest by its id
|
||||
* @param[in] id the manifest id
|
||||
* @return the application manifest if it was found
|
||||
*/
|
||||
const AppManifest _Nullable* findAppById(const std::string& id);
|
||||
_Nullable std::shared_ptr<AppManifest> findAppById(const std::string& id);
|
||||
|
||||
/** @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
|
||||
|
||||
@ -34,10 +34,6 @@ int32_t getResultIndex(const Bundle& bundle) {
|
||||
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) {
|
||||
std::string result;
|
||||
if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) {
|
||||
@ -47,14 +43,27 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
|
||||
}
|
||||
}
|
||||
|
||||
static void onButtonClicked(lv_event_t* e) {
|
||||
|
||||
class AlertDialogApp : public App {
|
||||
|
||||
private:
|
||||
|
||||
static void onButtonClickedCallback(lv_event_t* e) {
|
||||
auto appContext = service::loader::getCurrentAppContext();
|
||||
tt_assert(appContext != nullptr);
|
||||
auto app = std::static_pointer_cast<AlertDialogApp>(appContext->getApp());
|
||||
app->onButtonClicked(e);
|
||||
}
|
||||
|
||||
void onButtonClicked(lv_event_t* e) {
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
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);
|
||||
|
||||
auto bundle = std::make_unique<Bundle>();
|
||||
bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index);
|
||||
setResult(app::Result::Ok, std::move(bundle));
|
||||
|
||||
service::loader::stopApp();
|
||||
}
|
||||
|
||||
@ -63,10 +72,11 @@ static void createButton(lv_obj_t* parent, const std::string& text, size_t index
|
||||
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);
|
||||
lv_obj_add_event_cb(button, onButtonClickedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
|
||||
}
|
||||
public:
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
auto parameters = app.getParameters();
|
||||
tt_check(parameters != nullptr, "Parameters missing");
|
||||
|
||||
@ -97,12 +107,12 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
std::vector<std::string> labels = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN);
|
||||
if (labels.empty() || labels.front().empty()) {
|
||||
TT_LOG_E(TAG, "No items provided");
|
||||
app.setResult(ResultError);
|
||||
setResult(Result::Error);
|
||||
service::loader::stopApp();
|
||||
} else if (labels.size() == 1) {
|
||||
auto result_bundle = std::make_shared<Bundle>();
|
||||
setResultIndex(result_bundle, 0);
|
||||
app.setResult(ResultOk, result_bundle);
|
||||
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 {
|
||||
@ -113,12 +123,13 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "AlertDialog",
|
||||
.name = "Alert Dialog",
|
||||
.type = TypeHidden,
|
||||
.onShow = onShow
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<AlertDialogApp>
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -8,20 +8,25 @@
|
||||
|
||||
namespace tt::app::applist {
|
||||
|
||||
|
||||
class AppListApp : public App {
|
||||
|
||||
private:
|
||||
|
||||
static void onAppPressed(lv_event_t* e) {
|
||||
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
|
||||
service::loader::startApp(manifest->id);
|
||||
}
|
||||
|
||||
static void createAppWidget(const AppManifest* manifest, void* parent) {
|
||||
tt_check(parent);
|
||||
auto* list = reinterpret_cast<lv_obj_t*>(parent);
|
||||
static void createAppWidget(const std::shared_ptr<AppManifest>& manifest, lv_obj_t* list) {
|
||||
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
|
||||
lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str());
|
||||
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest);
|
||||
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest.get());
|
||||
}
|
||||
|
||||
static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
|
||||
public:
|
||||
|
||||
void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
|
||||
auto* toolbar = lvgl::toolbar_create(parent, app);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
@ -38,24 +43,26 @@ static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
|
||||
|
||||
lv_list_add_text(list, "User");
|
||||
for (const auto& manifest: manifests) {
|
||||
if (manifest->type == TypeUser) {
|
||||
if (manifest->type == Type::User) {
|
||||
createAppWidget(manifest, list);
|
||||
}
|
||||
}
|
||||
|
||||
lv_list_add_text(list, "System");
|
||||
for (const auto& manifest: manifests) {
|
||||
if (manifest->type == TypeSystem) {
|
||||
if (manifest->type == Type::System) {
|
||||
createAppWidget(manifest, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "AppList",
|
||||
.name = "Apps",
|
||||
.type = TypeHidden,
|
||||
.onShow = onShow,
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<AppListApp>,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -24,14 +24,11 @@
|
||||
|
||||
namespace tt::app::boot {
|
||||
|
||||
static int32_t bootThreadCallback(void* context);
|
||||
static void startNextApp();
|
||||
class BootApp : public App {
|
||||
|
||||
struct Data {
|
||||
Data() : thread("boot", 4096, bootThreadCallback, this) {}
|
||||
private:
|
||||
|
||||
Thread thread;
|
||||
};
|
||||
Thread thread = Thread("boot", 4096, bootThreadCallback, this);
|
||||
|
||||
static int32_t bootThreadCallback(TT_UNUSED void* context) {
|
||||
TickType_t start_time = kernel::getTicks();
|
||||
@ -66,7 +63,6 @@ static int32_t bootThreadCallback(TT_UNUSED void* context) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static void startNextApp() {
|
||||
#ifdef ESP_PLATFORM
|
||||
esp_reset_reason_t reason = esp_reset_reason();
|
||||
@ -85,9 +81,9 @@ static void startNextApp() {
|
||||
}
|
||||
}
|
||||
|
||||
static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
|
||||
auto data = std::static_pointer_cast<Data>(app.getData());
|
||||
public:
|
||||
|
||||
void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
|
||||
auto* image = lv_image_create(parent);
|
||||
lv_obj_set_size(image, LV_PCT(100), LV_PCT(100));
|
||||
|
||||
@ -99,26 +95,22 @@ static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
|
||||
|
||||
lvgl::obj_set_style_bg_blacken(parent);
|
||||
|
||||
data->thread.start();
|
||||
// Just in case this app is somehow resumed
|
||||
if (thread.getState() == Thread::State::Stopped) {
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
static void onStart(AppContext& app) {
|
||||
auto data = std::make_shared<Data>();
|
||||
app.setData(data);
|
||||
}
|
||||
|
||||
static void onStop(AppContext& app) {
|
||||
auto data = std::static_pointer_cast<Data>(app.getData());
|
||||
data->thread.join();
|
||||
void onStop(AppContext& app) override {
|
||||
thread.join();
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Boot",
|
||||
.name = "Boot",
|
||||
.type = TypeBoot,
|
||||
.onStart = onStart,
|
||||
.onStop = onStop,
|
||||
.onShow = onShow,
|
||||
.type = Type::Boot,
|
||||
.createApp = create<BootApp>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -17,7 +17,11 @@ void onContinuePressed(TT_UNUSED lv_event_t* event) {
|
||||
tt::app::launcher::start();
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
class CrashDiagnosticsApp : public App {
|
||||
|
||||
public:
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
auto* display = lv_obj_get_display(parent);
|
||||
int32_t parent_height = lv_display_get_vertical_resolution(display) - STATUSBAR_HEIGHT;
|
||||
|
||||
@ -108,12 +112,13 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "CrashDiagnostics",
|
||||
.name = "Crash Diagnostics",
|
||||
.type = TypeHidden,
|
||||
.onShow = onShow
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<CrashDiagnosticsApp>
|
||||
};
|
||||
|
||||
void start() {
|
||||
|
||||
@ -98,7 +98,9 @@ static void onOrientationSet(lv_event_t* event) {
|
||||
}
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
class DisplayApp : public App {
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
@ -169,21 +171,19 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_dropdown_set_selected(orientation_dropdown, orientation_selected);
|
||||
}
|
||||
|
||||
static void onHide(TT_UNUSED AppContext& app) {
|
||||
void onHide(TT_UNUSED AppContext& app) override {
|
||||
if (backlight_duty_set) {
|
||||
setBacklightDuty(backlight_duty);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Display",
|
||||
.name = "Display",
|
||||
.icon = TT_ASSETS_APP_ICON_DISPLAY_SETTINGS,
|
||||
.type = TypeSettings,
|
||||
.onStart = nullptr,
|
||||
.onStop = nullptr,
|
||||
.onShow = onShow,
|
||||
.onHide = onHide
|
||||
.type = Type::Settings,
|
||||
.createApp = create<DisplayApp>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "app/files/FilesPrivate.h"
|
||||
#include "app/files/View.h"
|
||||
#include "app/files/State.h"
|
||||
|
||||
#include "app/AppContext.h"
|
||||
#include "Assets.h"
|
||||
@ -12,31 +13,31 @@ namespace tt::app::files {
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
|
||||
class FilesApp : public App {
|
||||
std::unique_ptr<View> view;
|
||||
std::shared_ptr<State> state;
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto files = std::static_pointer_cast<Files>(app.getData());
|
||||
files->onShow(parent);
|
||||
public:
|
||||
FilesApp() {
|
||||
state = std::make_shared<State>();
|
||||
view = std::make_unique<View>(state);
|
||||
}
|
||||
|
||||
static void onStart(AppContext& app) {
|
||||
auto files = std::make_shared<Files>();
|
||||
app.setData(files);
|
||||
void onShow(AppContext& appContext, lv_obj_t* parent) override {
|
||||
view->init(parent);
|
||||
}
|
||||
|
||||
static void onResult(AppContext& app, Result result, const Bundle& bundle) {
|
||||
auto files = std::static_pointer_cast<Files>(app.getData());
|
||||
files->onResult(result, bundle);
|
||||
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||
view->onResult(result, std::move(bundle));
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Files",
|
||||
.name = "Files",
|
||||
.icon = TT_ASSETS_APP_ICON_FILES,
|
||||
.type = TypeHidden,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.onResult = onResult
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<FilesApp>
|
||||
};
|
||||
|
||||
void start() {
|
||||
|
||||
@ -8,12 +8,15 @@
|
||||
#include "app/ElfApp.h"
|
||||
#include "lvgl/Toolbar.h"
|
||||
#include "lvgl/LvglSync.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "Tactility.h"
|
||||
#include "StringUtils.h"
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include "service/loader/Loader.h"
|
||||
#endif
|
||||
|
||||
#define TAG "files_app"
|
||||
|
||||
namespace tt::app::files {
|
||||
@ -81,7 +84,9 @@ void View::viewFile(const std::string& path, const std::string& filename) {
|
||||
|
||||
if (isSupportedExecutableFile(filename)) {
|
||||
#ifdef ESP_PLATFORM
|
||||
app::startElfApp(processed_filepath);
|
||||
app::registerElfApp(processed_filepath);
|
||||
auto app_id = app::getElfAppId(processed_filepath);
|
||||
service::loader::startApp(app_id);
|
||||
#endif
|
||||
} else if (isSupportedImageFile(filename)) {
|
||||
app::imageviewer::start(processed_filepath);
|
||||
@ -282,8 +287,8 @@ void View::onNavigate() {
|
||||
}
|
||||
}
|
||||
|
||||
void View::onResult(Result result, const Bundle& bundle) {
|
||||
if (result != ResultOk) {
|
||||
void View::onResult(Result result, std::unique_ptr<Bundle> bundle) {
|
||||
if (result != Result::Ok || bundle == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -292,7 +297,7 @@ void View::onResult(Result result, const Bundle& bundle) {
|
||||
|
||||
switch (state->getPendingAction()) {
|
||||
case State::ActionDelete: {
|
||||
if (alertdialog::getResultIndex(bundle) == 0) {
|
||||
if (alertdialog::getResultIndex(*bundle) == 0) {
|
||||
int delete_count = (int)remove(filepath.c_str());
|
||||
if (delete_count > 0) {
|
||||
TT_LOG_I(TAG, "Deleted %d items", delete_count);
|
||||
@ -305,7 +310,7 @@ void View::onResult(Result result, const Bundle& bundle) {
|
||||
break;
|
||||
}
|
||||
case State::ActionRename: {
|
||||
auto new_name = app::inputdialog::getResult(bundle);
|
||||
auto new_name = app::inputdialog::getResult(*bundle);
|
||||
if (!new_name.empty() && new_name != state->getSelectedChildEntry()) {
|
||||
std::string rename_to = getChildPath(state->getCurrentPath(), new_name);
|
||||
if (rename(filepath.c_str(), rename_to.c_str())) {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#include "Mutex.h"
|
||||
#include "Thread.h"
|
||||
#include "Tactility.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "lvgl/Toolbar.h"
|
||||
|
||||
@ -10,7 +9,9 @@
|
||||
|
||||
namespace tt::app::gpio {
|
||||
|
||||
class Gpio {
|
||||
extern const AppManifest manifest;
|
||||
|
||||
class GpioApp : public App {
|
||||
|
||||
private:
|
||||
|
||||
@ -19,6 +20,9 @@ private:
|
||||
std::unique_ptr<Timer> timer;
|
||||
Mutex mutex;
|
||||
|
||||
static lv_obj_t* createGpioRowWrapper(lv_obj_t* parent);
|
||||
static void onTimer(TT_UNUSED std::shared_ptr<void> context);
|
||||
|
||||
public:
|
||||
|
||||
void lock() const {
|
||||
@ -29,18 +33,17 @@ public:
|
||||
tt_check(mutex.release() == TtStatusOk);
|
||||
}
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent);
|
||||
void onHide(AppContext& app);
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override;
|
||||
void onHide(AppContext& app) override;
|
||||
|
||||
void startTask(std::shared_ptr<Gpio> ptr);
|
||||
void startTask();
|
||||
void stopTask();
|
||||
|
||||
void updatePinStates();
|
||||
void updatePinWidgets();
|
||||
};
|
||||
|
||||
|
||||
void Gpio::updatePinStates() {
|
||||
void GpioApp::updatePinStates() {
|
||||
lock();
|
||||
// Update pin states
|
||||
for (int i = 0; i < GPIO_NUM_MAX; ++i) {
|
||||
@ -53,7 +56,7 @@ void Gpio::updatePinStates() {
|
||||
unlock();
|
||||
}
|
||||
|
||||
void Gpio::updatePinWidgets() {
|
||||
void GpioApp::updatePinWidgets() {
|
||||
if (lvgl::lock(100)) {
|
||||
lock();
|
||||
for (int j = 0; j < GPIO_NUM_MAX; ++j) {
|
||||
@ -75,7 +78,7 @@ void Gpio::updatePinWidgets() {
|
||||
}
|
||||
}
|
||||
|
||||
static lv_obj_t* createGpioRowWrapper(lv_obj_t* parent) {
|
||||
lv_obj_t* GpioApp::createGpioRowWrapper(lv_obj_t* parent) {
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_pad_all(wrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
@ -85,26 +88,29 @@ static lv_obj_t* createGpioRowWrapper(lv_obj_t* parent) {
|
||||
|
||||
// region Task
|
||||
|
||||
static void onTimer(std::shared_ptr<void> context) {
|
||||
auto gpio = std::static_pointer_cast<Gpio>(context);
|
||||
|
||||
gpio->updatePinStates();
|
||||
gpio->updatePinWidgets();
|
||||
void GpioApp::onTimer(TT_UNUSED std::shared_ptr<void> context) {
|
||||
auto appContext = service::loader::getCurrentAppContext();
|
||||
if (appContext->getManifest().id == manifest.id) {
|
||||
auto app = std::static_pointer_cast<GpioApp>(appContext->getApp());
|
||||
if (app != nullptr) {
|
||||
app->updatePinStates();
|
||||
app->updatePinWidgets();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Gpio::startTask(std::shared_ptr<Gpio> ptr) {
|
||||
void GpioApp::startTask() {
|
||||
lock();
|
||||
tt_assert(timer == nullptr);
|
||||
timer = std::make_unique<Timer>(
|
||||
Timer::Type::Periodic,
|
||||
&onTimer,
|
||||
ptr
|
||||
&onTimer
|
||||
);
|
||||
timer->start(100 / portTICK_PERIOD_MS);
|
||||
unlock();
|
||||
}
|
||||
|
||||
void Gpio::stopTask() {
|
||||
void GpioApp::stopTask() {
|
||||
tt_assert(timer);
|
||||
|
||||
timer->stop();
|
||||
@ -114,9 +120,7 @@ void Gpio::stopTask() {
|
||||
// endregion Task
|
||||
|
||||
|
||||
void Gpio::onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto gpio = std::static_pointer_cast<Gpio>(app.getData());
|
||||
|
||||
void GpioApp::onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
@ -139,7 +143,7 @@ void Gpio::onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_t* row_wrapper = createGpioRowWrapper(wrapper);
|
||||
lv_obj_align(row_wrapper, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
gpio->lock();
|
||||
lock();
|
||||
for (int i = GPIO_NUM_MIN; i < GPIO_NUM_MAX; ++i) {
|
||||
|
||||
// Add the GPIO number before the first item on a row
|
||||
@ -152,7 +156,7 @@ void Gpio::onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_t* status_label = lv_label_create(row_wrapper);
|
||||
lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing), 0);
|
||||
lv_label_set_text_fmt(status_label, "%s", LV_SYMBOL_STOP);
|
||||
gpio->lvPins[i] = status_label;
|
||||
lvPins[i] = status_label;
|
||||
|
||||
column++;
|
||||
|
||||
@ -170,42 +174,20 @@ void Gpio::onShow(AppContext& app, lv_obj_t* parent) {
|
||||
column = 0;
|
||||
}
|
||||
}
|
||||
gpio->unlock();
|
||||
unlock();
|
||||
|
||||
gpio->startTask(gpio);
|
||||
startTask();
|
||||
}
|
||||
|
||||
void Gpio::onHide(AppContext& app) {
|
||||
auto gpio = std::static_pointer_cast<Gpio>(app.getData());
|
||||
gpio->stopTask();
|
||||
void GpioApp::onHide(AppContext& app) {
|
||||
stopTask();
|
||||
}
|
||||
|
||||
// region App lifecycle
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto gpio = std::static_pointer_cast<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 = {
|
||||
.id = "Gpio",
|
||||
.name = "GPIO",
|
||||
.type = TypeSystem,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.onHide = onHide
|
||||
.type = Type::System,
|
||||
.createApp = create<GpioApp>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -14,108 +14,64 @@
|
||||
|
||||
namespace tt::app::i2cscanner {
|
||||
|
||||
static void updateViews(std::shared_ptr<Data> data);
|
||||
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. */
|
||||
std::shared_ptr<Data> _Nullable optData() {
|
||||
app::AppContext* app = service::loader::getCurrentApp();
|
||||
if (app->getManifest().id == manifest.id) {
|
||||
return std::static_pointer_cast<Data>(app->getData());
|
||||
std::shared_ptr<I2cScannerApp> _Nullable optApp() {
|
||||
auto appContext = service::loader::getCurrentAppContext();
|
||||
if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
|
||||
return std::static_pointer_cast<I2cScannerApp>(appContext->getApp());
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static void onSelectBus(lv_event_t* event) {
|
||||
auto data = optData();
|
||||
if (data == nullptr) {
|
||||
return;
|
||||
}
|
||||
// region Lifecycle
|
||||
|
||||
auto* dropdown = static_cast<lv_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) {
|
||||
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());
|
||||
void I2cScannerApp::onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
@ -134,61 +90,266 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_t* scan_button = lv_button_create(wrapper);
|
||||
lv_obj_set_width(scan_button, LV_PCT(48));
|
||||
lv_obj_align(scan_button, LV_ALIGN_TOP_LEFT, 0, 1); // Shift 1 pixel to align with selection box
|
||||
lv_obj_add_event_cb(scan_button, &onPressScan, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
lv_obj_add_event_cb(scan_button, onPressScanCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
lv_obj_t* scan_button_label = lv_label_create(scan_button);
|
||||
lv_obj_align(scan_button_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_label_set_text(scan_button_label, START_SCAN_TEXT);
|
||||
data->scanButtonLabelWidget = scan_button_label;
|
||||
scanButtonLabelWidget = scan_button_label;
|
||||
|
||||
lv_obj_t* port_dropdown = lv_dropdown_create(wrapper);
|
||||
std::string dropdown_items = getPortNamesForDropdown();
|
||||
lv_dropdown_set_options(port_dropdown, dropdown_items.c_str());
|
||||
lv_obj_set_width(port_dropdown, LV_PCT(48));
|
||||
lv_obj_align(port_dropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
|
||||
lv_obj_add_event_cb(port_dropdown, onSelectBus, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
lv_obj_add_event_cb(port_dropdown, onSelectBusCallback, LV_EVENT_VALUE_CHANGED, this);
|
||||
lv_dropdown_set_selected(port_dropdown, 0);
|
||||
data->portDropdownWidget = port_dropdown;
|
||||
portDropdownWidget = port_dropdown;
|
||||
|
||||
lv_obj_t* scan_list = lv_list_create(main_wrapper);
|
||||
lv_obj_set_style_margin_top(scan_list, 8, 0);
|
||||
lv_obj_set_width(scan_list, LV_PCT(100));
|
||||
lv_obj_set_height(scan_list, LV_SIZE_CONTENT);
|
||||
lv_obj_add_flag(scan_list, LV_OBJ_FLAG_HIDDEN);
|
||||
data->scanListWidget = scan_list;
|
||||
scanListWidget = scan_list;
|
||||
}
|
||||
|
||||
static void onHide(AppContext& app) {
|
||||
auto data = std::static_pointer_cast<Data>(app.getData());
|
||||
|
||||
void I2cScannerApp::onHide(AppContext& app) {
|
||||
bool isRunning = false;
|
||||
if (data->mutex.acquire(250 / portTICK_PERIOD_MS) == TtStatusOk) {
|
||||
auto* timer = data->scanTimer.get();
|
||||
if (mutex.acquire(250 / portTICK_PERIOD_MS) == TtStatusOk) {
|
||||
auto* timer = scanTimer.get();
|
||||
if (timer != nullptr) {
|
||||
isRunning = timer->isRunning();
|
||||
}
|
||||
data->mutex.release();
|
||||
mutex.release();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
stopScanning(data);
|
||||
stopScanning();
|
||||
}
|
||||
}
|
||||
|
||||
static void onStart(AppContext& app) {
|
||||
auto data = std::make_shared<Data>();
|
||||
app.setData(data);
|
||||
// endregion Lifecycle
|
||||
|
||||
// region Callbacks
|
||||
|
||||
void I2cScannerApp::onSelectBusCallback(lv_event_t* event) {
|
||||
auto* app = (I2cScannerApp*)lv_event_get_user_data(event);
|
||||
if (app != nullptr) {
|
||||
app->onSelectBus(event);
|
||||
}
|
||||
}
|
||||
|
||||
void I2cScannerApp::onPressScanCallback(lv_event_t* event) {
|
||||
auto* app = (I2cScannerApp*)lv_event_get_user_data(event);
|
||||
if (app != nullptr) {
|
||||
app->onPressScan(event);
|
||||
}
|
||||
}
|
||||
|
||||
void I2cScannerApp::onScanTimerCallback(TT_UNUSED std::shared_ptr<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 = {
|
||||
.id = "I2cScanner",
|
||||
.name = "I2C Scanner",
|
||||
.icon = TT_ASSETS_APP_ICON_I2C_SETTINGS,
|
||||
.type = TypeSystem,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.onHide = onHide
|
||||
.type = Type::System,
|
||||
.createApp = create<I2cScannerApp>
|
||||
};
|
||||
|
||||
void start() {
|
||||
|
||||
@ -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
|
||||
@ -69,7 +69,9 @@ static void show(lv_obj_t* parent, const hal::i2c::Configuration& configuration)
|
||||
}
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
class I2cSettingsApp : public App {
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
@ -84,13 +86,14 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
show(wrapper, configuration);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "I2cSettings",
|
||||
.name = "I2C",
|
||||
.icon = TT_ASSETS_APP_ICON_I2C_SETTINGS,
|
||||
.type = TypeSettings,
|
||||
.onShow = onShow
|
||||
.type = Type::Settings,
|
||||
.createApp = create<I2cSettingsApp>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -12,7 +12,9 @@ extern const AppManifest manifest;
|
||||
#define TAG "image_viewer"
|
||||
#define IMAGE_VIEWER_FILE_ARGUMENT "file"
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
class ImageViewerApp : public App {
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
auto wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(wrapper, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
@ -50,12 +52,13 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_label_set_text(file_label, "File not found");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "ImageViewer",
|
||||
.name = "Image Viewer",
|
||||
.type = TypeHidden,
|
||||
.onShow = onShow
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<ImageViewerApp>
|
||||
};
|
||||
|
||||
void start(const std::string& file) {
|
||||
|
||||
@ -18,6 +18,7 @@ namespace tt::app::inputdialog {
|
||||
#define TAG "input_dialog"
|
||||
|
||||
extern const AppManifest manifest;
|
||||
class InputDialogApp;
|
||||
|
||||
void start(const std::string& title, const std::string& message, const std::string& prefilled) {
|
||||
auto bundle = std::make_shared<Bundle>();
|
||||
@ -33,10 +34,6 @@ std::string getResult(const Bundle& bundle) {
|
||||
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) {
|
||||
std::string result;
|
||||
if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) {
|
||||
@ -46,32 +43,43 @@ static std::string getTitleParameter(const std::shared_ptr<const Bundle>& bundle
|
||||
}
|
||||
}
|
||||
|
||||
static void onButtonClicked(lv_event_t* e) {
|
||||
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);
|
||||
class InputDialogApp : public App {
|
||||
|
||||
}
|
||||
service::loader::stopApp();
|
||||
}
|
||||
private:
|
||||
|
||||
static void createButton(lv_obj_t* parent, const std::string& text, void* callbackContext) {
|
||||
lv_obj_t* button = lv_button_create(parent);
|
||||
lv_obj_t* button_label = lv_label_create(button);
|
||||
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_label_set_text(button_label, text.c_str());
|
||||
lv_obj_add_event_cb(button, &onButtonClicked, LV_EVENT_SHORT_CLICKED, callbackContext);
|
||||
lv_obj_add_event_cb(button, onButtonClickedCallback, LV_EVENT_SHORT_CLICKED, callbackContext);
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
static void onButtonClickedCallback(lv_event_t* e) {
|
||||
auto appContext = service::loader::getCurrentAppContext();
|
||||
tt_assert(appContext != nullptr);
|
||||
auto app = std::static_pointer_cast<InputDialogApp>(appContext->getApp());
|
||||
app->onButtonClicked(e);
|
||||
}
|
||||
|
||||
void onButtonClicked(lv_event_t* e) {
|
||||
auto user_data = lv_event_get_user_data(e);
|
||||
int index = (user_data != 0) ? 0 : 1;
|
||||
TT_LOG_I(TAG, "Selected item at index %d", index);
|
||||
if (index == 0) {
|
||||
auto bundle = std::make_unique<Bundle>();
|
||||
const char* text = lv_textarea_get_text((lv_obj_t*)user_data);
|
||||
bundle->putString(RESULT_BUNDLE_KEY_RESULT, text);
|
||||
setResult(app::Result::Ok, std::move(bundle));
|
||||
} else {
|
||||
setResult(app::Result::Cancelled);
|
||||
|
||||
}
|
||||
service::loader::stopApp();
|
||||
}
|
||||
|
||||
public:
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
auto parameters = app.getParameters();
|
||||
tt_check(parameters != nullptr, "Parameters missing");
|
||||
|
||||
@ -109,12 +117,13 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
createButton(button_wrapper, "OK", textarea);
|
||||
createButton(button_wrapper, "Cancel", nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "InputDialog",
|
||||
.name = "Input Dialog",
|
||||
.type = TypeHidden,
|
||||
.onShow = onShow
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<InputDialogApp>
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -42,7 +42,9 @@ static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
|
||||
class LauncherApp : public App {
|
||||
|
||||
void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
|
||||
lv_obj_align(wrapper, LV_ALIGN_CENTER, 0, 0);
|
||||
@ -72,12 +74,13 @@ static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
|
||||
createAppButton(wrapper, "Files", files_icon_path.c_str(), "Files", padding);
|
||||
createAppButton(wrapper, "Settings", settings_icon_path.c_str(), "Settings", padding);
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Launcher",
|
||||
.name = "Launcher",
|
||||
.type = TypeLauncher,
|
||||
.onShow = onShow,
|
||||
.type = Type::Launcher,
|
||||
.createApp = create<LauncherApp>
|
||||
};
|
||||
|
||||
void start() {
|
||||
|
||||
@ -11,10 +11,12 @@
|
||||
|
||||
namespace tt::app::log {
|
||||
|
||||
struct LogAppData {
|
||||
class LogApp : public App {
|
||||
|
||||
private:
|
||||
|
||||
LogLevel filterLevel = LogLevel::Info;
|
||||
lv_obj_t* labelWidget = nullptr;
|
||||
};
|
||||
|
||||
static bool shouldShowLog(LogLevel filterLevel, LogLevel logLevel) {
|
||||
if (filterLevel == LogLevel::None || logLevel == LogLevel::None) {
|
||||
@ -24,14 +26,7 @@ static bool shouldShowLog(LogLevel filterLevel, LogLevel logLevel) {
|
||||
}
|
||||
}
|
||||
|
||||
static void setLogEntries(lv_obj_t* label) {
|
||||
auto app = service::loader::getCurrentApp();
|
||||
if (app == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto data = std::static_pointer_cast<LogAppData>(app->getData());
|
||||
auto filterLevel = data->filterLevel;
|
||||
|
||||
void updateLogEntries() {
|
||||
unsigned int index;
|
||||
auto* entries = copyLogEntries(index);
|
||||
std::stringstream buffer;
|
||||
@ -50,16 +45,23 @@ static void setLogEntries(lv_obj_t* label) {
|
||||
}
|
||||
delete entries;
|
||||
if (!buffer.str().empty()) {
|
||||
lv_label_set_text(label, buffer.str().c_str());
|
||||
lv_label_set_text(labelWidget, buffer.str().c_str());
|
||||
} else {
|
||||
lv_label_set_text(label, "No logs for the selected log level");
|
||||
lv_label_set_text(labelWidget, "No logs for the selected log level");
|
||||
}
|
||||
} else {
|
||||
lv_label_set_text(label, "Failed to load log");
|
||||
lv_label_set_text(labelWidget, "Failed to load log");
|
||||
}
|
||||
}
|
||||
|
||||
static void onLevelFilterPressed(TT_UNUSED lv_event_t* event) {
|
||||
void updateViews() {
|
||||
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
|
||||
updateLogEntries();
|
||||
lvgl::unlock();
|
||||
}
|
||||
}
|
||||
|
||||
static void onLevelFilterPressedCallback(TT_UNUSED lv_event_t* event) {
|
||||
std::vector<std::string> items = {
|
||||
"Verbose",
|
||||
"Debug",
|
||||
@ -70,26 +72,12 @@ static void onLevelFilterPressed(TT_UNUSED lv_event_t* event) {
|
||||
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());
|
||||
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, onLevelFilterPressed, nullptr);
|
||||
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));
|
||||
@ -98,35 +86,30 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
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);
|
||||
labelWidget = lv_label_create(wrapper);
|
||||
lv_obj_align(labelWidget, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
updateLogEntries();
|
||||
}
|
||||
|
||||
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) {
|
||||
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:
|
||||
data->filterLevel = LogLevel::Verbose;
|
||||
filterLevel = LogLevel::Verbose;
|
||||
break;
|
||||
case 1:
|
||||
data->filterLevel = LogLevel::Debug;
|
||||
filterLevel = LogLevel::Debug;
|
||||
break;
|
||||
case 2:
|
||||
data->filterLevel = LogLevel::Info;
|
||||
filterLevel = LogLevel::Info;
|
||||
break;
|
||||
case 3:
|
||||
data->filterLevel = LogLevel::Warning;
|
||||
filterLevel = LogLevel::Warning;
|
||||
break;
|
||||
case 4:
|
||||
data->filterLevel = LogLevel::Error;
|
||||
filterLevel = LogLevel::Error;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -135,15 +118,14 @@ static void onResult(AppContext& app, Result result, const Bundle& bundle) {
|
||||
|
||||
updateViews();
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Log",
|
||||
.name = "Log",
|
||||
.icon = LV_SYMBOL_LIST,
|
||||
.type = TypeSystem,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.onResult = onResult
|
||||
.type = Type::System,
|
||||
.createApp = create<LogApp>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -15,31 +15,60 @@ namespace tt::app::power {
|
||||
extern const AppManifest manifest;
|
||||
static void onTimer(TT_UNUSED std::shared_ptr<void> context);
|
||||
|
||||
struct Data {
|
||||
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;
|
||||
};
|
||||
class PowerApp;
|
||||
|
||||
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
|
||||
std::shared_ptr<Data> _Nullable optData() {
|
||||
app::AppContext* app = service::loader::getCurrentApp();
|
||||
if (app->getManifest().id == manifest.id) {
|
||||
return std::static_pointer_cast<Data>(app->getData());
|
||||
std::shared_ptr<PowerApp> _Nullable optApp() {
|
||||
auto appContext = service::loader::getCurrentAppContext();
|
||||
if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
|
||||
return std::static_pointer_cast<PowerApp>(appContext->getApp());
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static void updateUi(std::shared_ptr<Data> data) {
|
||||
class PowerApp : public App {
|
||||
|
||||
private:
|
||||
|
||||
Timer update_timer = Timer(Timer::Type::Periodic, &onTimer, nullptr);
|
||||
std::shared_ptr<tt::hal::Power> power = getConfiguration()->hardware->power();
|
||||
lv_obj_t* enableLabel = nullptr;
|
||||
lv_obj_t* enableSwitch = nullptr;
|
||||
lv_obj_t* batteryVoltageLabel = nullptr;
|
||||
lv_obj_t* chargeStateLabel = nullptr;
|
||||
lv_obj_t* chargeLevelLabel = nullptr;
|
||||
lv_obj_t* currentLabel = nullptr;
|
||||
|
||||
static void onTimer(TT_UNUSED std::shared_ptr<void> context) {
|
||||
auto app = optApp();
|
||||
if (app != nullptr) {
|
||||
app->updateUi();
|
||||
}
|
||||
}
|
||||
|
||||
void onPowerEnabledChanged(lv_event_t* event) {
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
if (code == LV_EVENT_VALUE_CHANGED) {
|
||||
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
|
||||
|
||||
if (power->isAllowedToCharge() != is_on) {
|
||||
power->setAllowedToCharge(is_on);
|
||||
updateUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void onPowerEnabledChangedCallback(lv_event_t* event) {
|
||||
auto* app = (PowerApp*)lv_event_get_user_data(event);
|
||||
app->onPowerEnabledChanged(event);
|
||||
}
|
||||
|
||||
void updateUi() {
|
||||
const char* charge_state;
|
||||
hal::Power::MetricData metric_data;
|
||||
if (data->power->getMetric(hal::Power::MetricType::IsCharging, metric_data)) {
|
||||
if (power->getMetric(hal::Power::MetricType::IsCharging, metric_data)) {
|
||||
charge_state = metric_data.valueAsBool ? "yes" : "no";
|
||||
} else {
|
||||
charge_state = "N/A";
|
||||
@ -47,24 +76,24 @@ static void updateUi(std::shared_ptr<Data> data) {
|
||||
|
||||
uint8_t charge_level;
|
||||
bool charge_level_scaled_set = false;
|
||||
if (data->power->getMetric(hal::Power::MetricType::ChargeLevel, metric_data)) {
|
||||
if (power->getMetric(hal::Power::MetricType::ChargeLevel, metric_data)) {
|
||||
charge_level = metric_data.valueAsUint8;
|
||||
charge_level_scaled_set = true;
|
||||
}
|
||||
|
||||
bool charging_enabled_set = data->power->supportsChargeControl();
|
||||
bool charging_enabled_and_allowed = data->power->supportsChargeControl() && data->power->isAllowedToCharge();
|
||||
bool charging_enabled_set = power->supportsChargeControl();
|
||||
bool charging_enabled_and_allowed = power->supportsChargeControl() && power->isAllowedToCharge();
|
||||
|
||||
int32_t current;
|
||||
bool current_set = false;
|
||||
if (data->power->getMetric(hal::Power::MetricType::Current, metric_data)) {
|
||||
if (power->getMetric(hal::Power::MetricType::Current, metric_data)) {
|
||||
current = metric_data.valueAsInt32;
|
||||
current_set = true;
|
||||
}
|
||||
|
||||
uint32_t battery_voltage;
|
||||
bool battery_voltage_set = false;
|
||||
if (data->power->getMetric(hal::Power::MetricType::BatteryVoltage, metric_data)) {
|
||||
if (power->getMetric(hal::Power::MetricType::BatteryVoltage, metric_data)) {
|
||||
battery_voltage = metric_data.valueAsUint32;
|
||||
battery_voltage_set = true;
|
||||
}
|
||||
@ -72,61 +101,40 @@ static void updateUi(std::shared_ptr<Data> data) {
|
||||
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);
|
||||
lv_obj_set_state(enableSwitch, LV_STATE_CHECKED, charging_enabled_and_allowed);
|
||||
lv_obj_remove_flag(enableSwitch, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(enableLabel, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(data->enable_switch, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(data->enable_label, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(enableSwitch, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(enableLabel, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
lv_label_set_text_fmt(data->charge_state, "Charging: %s", charge_state);
|
||||
lv_label_set_text_fmt(chargeStateLabel, "Charging: %s", charge_state);
|
||||
|
||||
if (battery_voltage_set) {
|
||||
lv_label_set_text_fmt(data->battery_voltage, "Battery voltage: %lu mV", battery_voltage);
|
||||
lv_label_set_text_fmt(batteryVoltageLabel, "Battery voltage: %lu mV", battery_voltage);
|
||||
} else {
|
||||
lv_label_set_text_fmt(data->battery_voltage, "Battery voltage: N/A");
|
||||
lv_label_set_text_fmt(batteryVoltageLabel, "Battery voltage: N/A");
|
||||
}
|
||||
|
||||
if (charge_level_scaled_set) {
|
||||
lv_label_set_text_fmt(data->charge_level, "Charge level: %d%%", charge_level);
|
||||
lv_label_set_text_fmt(chargeLevelLabel, "Charge level: %d%%", charge_level);
|
||||
} else {
|
||||
lv_label_set_text_fmt(data->charge_level, "Charge level: N/A");
|
||||
lv_label_set_text_fmt(chargeLevelLabel, "Charge level: N/A");
|
||||
}
|
||||
|
||||
if (current_set) {
|
||||
lv_label_set_text_fmt(data->current, "Current: %ld mAh", current);
|
||||
lv_label_set_text_fmt(currentLabel, "Current: %ld mAh", current);
|
||||
} else {
|
||||
lv_label_set_text_fmt(data->current, "Current: N/A");
|
||||
lv_label_set_text_fmt(currentLabel, "Current: N/A");
|
||||
}
|
||||
|
||||
lvgl::unlock();
|
||||
}
|
||||
|
||||
static void onTimer(TT_UNUSED std::shared_ptr<void> context) {
|
||||
auto data = optData();
|
||||
if (data != nullptr) {
|
||||
updateUi(data);
|
||||
}
|
||||
}
|
||||
public:
|
||||
|
||||
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) {
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
@ -137,8 +145,6 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
auto data = std::static_pointer_cast<Data>(app.getData());
|
||||
|
||||
// Top row: enable/disable
|
||||
lv_obj_t* switch_container = lv_obj_create(wrapper);
|
||||
lv_obj_set_width(switch_container, LV_PCT(100));
|
||||
@ -146,43 +152,36 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lvgl::obj_set_style_no_padding(switch_container);
|
||||
lvgl::obj_set_style_bg_invisible(switch_container);
|
||||
|
||||
data->enable_label = lv_label_create(switch_container);
|
||||
lv_label_set_text(data->enable_label, "Charging enabled");
|
||||
lv_obj_set_align(data->enable_label, LV_ALIGN_LEFT_MID);
|
||||
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, onPowerEnabledChanged, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
lv_obj_add_event_cb(enable_switch, onPowerEnabledChangedCallback, LV_EVENT_VALUE_CHANGED, this);
|
||||
lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID);
|
||||
|
||||
data->enable_switch = enable_switch;
|
||||
data->charge_state = lv_label_create(wrapper);
|
||||
data->charge_level = lv_label_create(wrapper);
|
||||
data->battery_voltage = lv_label_create(wrapper);
|
||||
data->current = lv_label_create(wrapper);
|
||||
enableSwitch = enable_switch;
|
||||
chargeStateLabel = lv_label_create(wrapper);
|
||||
chargeLevelLabel = lv_label_create(wrapper);
|
||||
batteryVoltageLabel = lv_label_create(wrapper);
|
||||
currentLabel = lv_label_create(wrapper);
|
||||
|
||||
updateUi(data);
|
||||
data->update_timer.start(kernel::millisToTicks(1000));
|
||||
updateUi();
|
||||
|
||||
update_timer.start(kernel::millisToTicks(1000));
|
||||
}
|
||||
|
||||
static void onHide(TT_UNUSED AppContext& app) {
|
||||
auto data = std::static_pointer_cast<Data>(app.getData());
|
||||
data->update_timer.stop();
|
||||
}
|
||||
|
||||
static void onStart(AppContext& app) {
|
||||
auto data = std::make_shared<Data>();
|
||||
app.setData(data);
|
||||
assert(data->power != nullptr); // The Power app only shows up on supported devices
|
||||
void onHide(TT_UNUSED AppContext& app) override {
|
||||
update_timer.stop();
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Power",
|
||||
.name = "Power",
|
||||
.icon = TT_ASSETS_APP_ICON_POWER_SETTINGS,
|
||||
.type = TypeSettings,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.onHide = onHide
|
||||
.type = Type::Settings,
|
||||
.createApp = create<PowerApp>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -1,29 +1,289 @@
|
||||
#include "TactilityConfig.h"
|
||||
#include <Timer.h>
|
||||
#include <kernel/Kernel.h>
|
||||
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
|
||||
#include "app/screenshot/ScreenshotUi.h"
|
||||
#include <memory>
|
||||
#include "TactilityHeadless.h"
|
||||
#include "app/App.h"
|
||||
#include "app/AppManifest.h"
|
||||
#include "lvgl/LvglSync.h"
|
||||
#include "lvgl/Toolbar.h"
|
||||
#include "service/gui/Gui.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "service/screenshot/Screenshot.h"
|
||||
|
||||
#define TAG "screenshot"
|
||||
|
||||
namespace tt::app::screenshot {
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto ui = std::static_pointer_cast<ScreenshotUi>(app.getData());
|
||||
ui->createWidgets(app, parent);
|
||||
extern const AppManifest manifest;
|
||||
|
||||
class ScreenshotApp : public App {
|
||||
|
||||
lv_obj_t* modeDropdown = nullptr;
|
||||
lv_obj_t* pathTextArea = nullptr;
|
||||
lv_obj_t* startStopButtonLabel = nullptr;
|
||||
lv_obj_t* timerWrapper = nullptr;
|
||||
lv_obj_t* delayTextArea = nullptr;
|
||||
std::unique_ptr<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) {
|
||||
auto ui = std::make_shared<ScreenshotUi>();
|
||||
app.setData(ui); // Ensure data gets deleted when no more in use
|
||||
static void onStartPressedCallback(TT_UNUSED lv_event_t* event) {
|
||||
auto app = optApp();
|
||||
if (app != nullptr) {
|
||||
app->onStartPressed();
|
||||
}
|
||||
}
|
||||
|
||||
static void onModeSetCallback(TT_UNUSED lv_event_t* event) {
|
||||
auto app = optApp();
|
||||
if (app != nullptr) {
|
||||
app->onModeSet();
|
||||
}
|
||||
}
|
||||
|
||||
static void onTimerCallback(TT_UNUSED std::shared_ptr<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 = {
|
||||
.id = "Screenshot",
|
||||
.name = "Screenshot",
|
||||
.icon = LV_SYMBOL_IMAGE,
|
||||
.type = TypeSystem,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.type = Type::System,
|
||||
.createApp = create<ScreenshotApp>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -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
|
||||
@ -32,10 +32,6 @@ int32_t getResultIndex(const Bundle& bundle) {
|
||||
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) {
|
||||
std::string result;
|
||||
if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) {
|
||||
@ -45,23 +41,35 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
|
||||
}
|
||||
}
|
||||
|
||||
static void onListItemSelected(lv_event_t* e) {
|
||||
class SelectionDialogApp : public App {
|
||||
|
||||
private:
|
||||
|
||||
static void onListItemSelectedCallback(lv_event_t* e) {
|
||||
auto appContext = service::loader::getCurrentAppContext();
|
||||
tt_assert(appContext != nullptr);
|
||||
auto app = std::static_pointer_cast<SelectionDialogApp>(appContext->getApp());
|
||||
app->onListItemSelected(e);
|
||||
}
|
||||
|
||||
void onListItemSelected(lv_event_t* e) {
|
||||
size_t index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e));
|
||||
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);
|
||||
auto bundle = std::make_unique<Bundle>();
|
||||
bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index);
|
||||
setResult(app::Result::Ok, std::move(bundle));
|
||||
service::loader::stopApp();
|
||||
}
|
||||
|
||||
static void 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, &onListItemSelected, LV_EVENT_SHORT_CLICKED, (void*)index);
|
||||
lv_obj_add_event_cb(btn, onListItemSelectedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
public:
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
std::string title = getTitleParameter(app.getParameters());
|
||||
lvgl::toolbar_create(parent, title);
|
||||
@ -77,12 +85,12 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
std::vector<std::string> items = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN);
|
||||
if (items.empty() || items.front().empty()) {
|
||||
TT_LOG_E(TAG, "No items provided");
|
||||
app.setResult(ResultError);
|
||||
setResult(Result::Error);
|
||||
service::loader::stopApp();
|
||||
} else if (items.size() == 1) {
|
||||
auto result_bundle = std::make_shared<Bundle>();
|
||||
setResultIndex(result_bundle, 0);
|
||||
app.setResult(ResultOk, result_bundle);
|
||||
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 {
|
||||
@ -93,16 +101,17 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
}
|
||||
} else {
|
||||
TT_LOG_E(TAG, "No items provided");
|
||||
app.setResult(ResultError);
|
||||
setResult(Result::Error);
|
||||
service::loader::stopApp();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "SelectionDialog",
|
||||
.name = "Selection Dialog",
|
||||
.type = TypeHidden,
|
||||
.onShow = onShow
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<SelectionDialogApp>
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -13,15 +13,17 @@ static void onAppPressed(lv_event_t* e) {
|
||||
service::loader::startApp(manifest->id);
|
||||
}
|
||||
|
||||
static void createWidget(const AppManifest* manifest, void* parent) {
|
||||
static void createWidget(const std::shared_ptr<AppManifest>& manifest, void* parent) {
|
||||
tt_check(parent);
|
||||
auto* list = (lv_obj_t*)parent;
|
||||
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
|
||||
lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str());
|
||||
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest);
|
||||
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest.get());
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
class SettingsApp : public App {
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
@ -33,18 +35,19 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto manifests = getApps();
|
||||
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
|
||||
for (const auto& manifest: manifests) {
|
||||
if (manifest->type == TypeSettings) {
|
||||
if (manifest->type == Type::Settings) {
|
||||
createWidget(manifest, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Settings",
|
||||
.name = "Settings",
|
||||
.icon = TT_ASSETS_APP_ICON_SETTINGS,
|
||||
.type = TypeHidden,
|
||||
.onShow = onShow,
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<SettingsApp>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -108,7 +108,9 @@ static void addRtosTasks(lv_obj_t* parent) {
|
||||
|
||||
#endif
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
class SystemInfoApp : public App {
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
@ -150,15 +152,14 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_label_set_text_fmt(esp_idf_version, "IDF version: %d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "SystemInfo",
|
||||
.name = "System Info",
|
||||
.icon = TT_ASSETS_APP_ICON_SYSTEM_INFO,
|
||||
.type = TypeSystem,
|
||||
.onStart = nullptr,
|
||||
.onStop = nullptr,
|
||||
.onShow = onShow
|
||||
.type = Type::System,
|
||||
.createApp = create<SystemInfoApp>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -11,7 +11,9 @@
|
||||
|
||||
namespace tt::app::textviewer {
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
class TextViewerApp : public App {
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
@ -39,12 +41,13 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_label_set_text_fmt(label, "Failed to load %s", file_argument.c_str());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "TextViewer",
|
||||
.name = "Text Viewer",
|
||||
.type = TypeHidden,
|
||||
.onShow = onShow
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<TextViewerApp>
|
||||
};
|
||||
|
||||
void start(const std::string& file) {
|
||||
|
||||
@ -13,20 +13,12 @@ namespace tt::app::timedatesettings {
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
struct Data {
|
||||
class TimeDateSettingsApp : public App {
|
||||
|
||||
private:
|
||||
|
||||
Mutex mutex = Mutex(Mutex::Type::Recursive);
|
||||
lv_obj_t* regionLabelWidget = nullptr;
|
||||
};
|
||||
|
||||
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
|
||||
std::shared_ptr<Data> _Nullable optData() {
|
||||
app::AppContext* app = service::loader::getCurrentApp();
|
||||
if (app->getManifest().id == manifest.id) {
|
||||
return std::static_pointer_cast<Data>(app->getData());
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static void onConfigureTimeZonePressed(TT_UNUSED lv_event_t* event) {
|
||||
timezone::start();
|
||||
@ -38,9 +30,9 @@ static void onTimeFormatChanged(lv_event_t* event) {
|
||||
time::setTimeFormat24Hour(show_24);
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto data = std::static_pointer_cast<Data>(app.getData());
|
||||
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);
|
||||
@ -65,7 +57,7 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
if (timeZoneName.empty()) {
|
||||
timeZoneName = "not set";
|
||||
}
|
||||
data->regionLabelWidget = region_label;
|
||||
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);
|
||||
@ -96,36 +88,29 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
}
|
||||
}
|
||||
|
||||
static void onStart(AppContext& app) {
|
||||
auto data = std::make_shared<Data>();
|
||||
app.setData(data);
|
||||
}
|
||||
|
||||
static void onResult(AppContext& app, Result result, const Bundle& bundle) {
|
||||
if (result == ResultOk) {
|
||||
auto data = std::static_pointer_cast<Data>(app.getData());
|
||||
auto name = timezone::getResultName(bundle);
|
||||
auto code = timezone::getResultCode(bundle);
|
||||
void onResult(AppContext& app, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||
if (result == Result::Ok) {
|
||||
auto name = timezone::getResultName(*bundle);
|
||||
auto code = timezone::getResultCode(*bundle);
|
||||
TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str());
|
||||
time::setTimeZone(name, code);
|
||||
|
||||
if (!name.empty()) {
|
||||
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
|
||||
lv_label_set_text(data->regionLabelWidget, name.c_str());
|
||||
lv_label_set_text(regionLabelWidget, name.c_str());
|
||||
lvgl::unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "TimeDateSettings",
|
||||
.name = "Time & Date",
|
||||
.icon = TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS,
|
||||
.type = TypeSettings,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.onResult = onResult
|
||||
.type = Type::Settings,
|
||||
.createApp = create<TimeDateSettingsApp>
|
||||
};
|
||||
|
||||
void start() {
|
||||
|
||||
@ -26,16 +26,6 @@ struct TimeZoneEntry {
|
||||
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) {
|
||||
std::string partial_strip = input.substr(1, input.size() - 3);
|
||||
auto first_end_quote = partial_strip.find('"');
|
||||
@ -62,59 +52,81 @@ std::string getResultCode(const Bundle& bundle) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void setResultName(std::shared_ptr<Bundle>& bundle, const std::string& name) {
|
||||
bundle->putString(RESULT_BUNDLE_NAME_INDEX, name);
|
||||
void setResultName(Bundle& bundle, const std::string& name) {
|
||||
bundle.putString(RESULT_BUNDLE_NAME_INDEX, name);
|
||||
}
|
||||
|
||||
void setResultCode(std::shared_ptr<Bundle>& bundle, const std::string& code) {
|
||||
bundle->putString(RESULT_BUNDLE_CODE_INDEX, code);
|
||||
void setResultCode(Bundle& bundle, const std::string& code) {
|
||||
bundle.putString(RESULT_BUNDLE_CODE_INDEX, code);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static void onUpdateTimer(std::shared_ptr<void> context) {
|
||||
auto data = std::static_pointer_cast<Data>(context);
|
||||
updateList(data);
|
||||
|
||||
class TimeZoneApp : public App {
|
||||
|
||||
private:
|
||||
|
||||
Mutex mutex;
|
||||
std::vector<TimeZoneEntry> entries;
|
||||
std::unique_ptr<Timer> updateTimer;
|
||||
lv_obj_t* listWidget = nullptr;
|
||||
lv_obj_t* filterTextareaWidget = nullptr;
|
||||
|
||||
static void onTextareaValueChangedCallback(TT_UNUSED lv_event_t* e) {
|
||||
auto* app = (TimeZoneApp*)lv_event_get_user_data(e);
|
||||
app->onTextareaValueChanged(e);
|
||||
}
|
||||
|
||||
static void onTextareaValueChanged(TT_UNUSED lv_event_t* e) {
|
||||
auto* app = service::loader::getCurrentApp();
|
||||
auto app_data = app->getData();
|
||||
auto data = std::static_pointer_cast<Data>(app_data);
|
||||
|
||||
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
if (data->updateTimer->isRunning()) {
|
||||
data->updateTimer->stop();
|
||||
void onTextareaValueChanged(TT_UNUSED lv_event_t* e) {
|
||||
if (mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
if (updateTimer->isRunning()) {
|
||||
updateTimer->stop();
|
||||
}
|
||||
|
||||
data->updateTimer->start(500 / portTICK_PERIOD_MS);
|
||||
updateTimer->start(500 / portTICK_PERIOD_MS);
|
||||
|
||||
data->mutex.unlock();
|
||||
mutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
static void onListItemSelected(lv_event_t* e) {
|
||||
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* app = service::loader::getCurrentApp();
|
||||
auto data = std::static_pointer_cast<Data>(app->getData());
|
||||
|
||||
auto& entry = data->entries[index];
|
||||
auto& entry = entries[index];
|
||||
|
||||
auto bundle = std::make_shared<Bundle>();
|
||||
setResultName(bundle, entry.name);
|
||||
setResultCode(bundle, entry.code);
|
||||
app->setResult(app::ResultOk, bundle);
|
||||
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, &onListItemSelected, LV_EVENT_SHORT_CLICKED, (void*)index);
|
||||
lv_obj_add_event_cb(btn, &onListItemSelectedCallback, LV_EVENT_SHORT_CLICKED, (void*)index);
|
||||
}
|
||||
|
||||
static void readTimeZones(const std::shared_ptr<Data>& data, std::string filter) {
|
||||
static void updateTimerCallback(std::shared_ptr<void> context) {
|
||||
auto appContext = service::loader::getCurrentAppContext();
|
||||
if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
|
||||
auto app = std::static_pointer_cast<TimeZoneApp>(appContext->getApp());
|
||||
app->updateList();
|
||||
}
|
||||
}
|
||||
|
||||
void readTimeZones(std::string filter) {
|
||||
auto path = std::string(MOUNT_POINT_SYSTEM) + "/timezones.csv";
|
||||
auto* file = fopen(path.c_str(), "rb");
|
||||
if (file == nullptr) {
|
||||
@ -125,15 +137,12 @@ static void readTimeZones(const std::shared_ptr<Data>& data, std::string filter)
|
||||
std::string name;
|
||||
std::string code;
|
||||
uint32_t count = 0;
|
||||
std::vector<TimeZoneEntry> entries;
|
||||
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++;
|
||||
entries.push_back({
|
||||
.name = name,
|
||||
.code = code
|
||||
});
|
||||
new_entries.push_back({.name = name, .code = code});
|
||||
|
||||
// Safety guard
|
||||
if (count > 50) {
|
||||
@ -148,9 +157,9 @@ static void readTimeZones(const std::shared_ptr<Data>& data, std::string filter)
|
||||
|
||||
fclose(file);
|
||||
|
||||
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
data->entries = std::move(entries);
|
||||
data->mutex.unlock();
|
||||
if (mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
entries = std::move(new_entries);
|
||||
mutex.unlock();
|
||||
} else {
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
}
|
||||
@ -158,10 +167,10 @@ static void readTimeZones(const std::shared_ptr<Data>& data, std::string filter)
|
||||
TT_LOG_I(TAG, "Processed %lu entries", count);
|
||||
}
|
||||
|
||||
static void updateList(std::shared_ptr<Data>& data) {
|
||||
void updateList() {
|
||||
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
|
||||
std::string filter = tt::string::lowercase(std::string(lv_textarea_get_text(data->filterTextareaWidget)));
|
||||
readTimeZones(data, filter);
|
||||
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");
|
||||
@ -169,25 +178,25 @@ static void updateList(std::shared_ptr<Data>& data) {
|
||||
}
|
||||
|
||||
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
|
||||
if (data->mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
lv_obj_clean(data->listWidget);
|
||||
if (mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
lv_obj_clean(listWidget);
|
||||
|
||||
uint32_t index = 0;
|
||||
for (auto& entry : data->entries) {
|
||||
createListItem(data->listWidget, entry.name, index);
|
||||
for (auto& entry : entries) {
|
||||
createListItem(listWidget, entry.name, index);
|
||||
index++;
|
||||
}
|
||||
|
||||
data->mutex.unlock();
|
||||
mutex.unlock();
|
||||
}
|
||||
|
||||
lvgl::unlock();
|
||||
}
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto data = std::static_pointer_cast<Data>(app.getData());
|
||||
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);
|
||||
|
||||
@ -210,8 +219,8 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto* textarea = lv_textarea_create(search_wrapper);
|
||||
lv_textarea_set_placeholder_text(textarea, "e.g. Europe/Amsterdam");
|
||||
lv_textarea_set_one_line(textarea, true);
|
||||
lv_obj_add_event_cb(textarea, onTextareaValueChanged, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
data->filterTextareaWidget = textarea;
|
||||
lv_obj_add_event_cb(textarea, onTextareaValueChangedCallback, LV_EVENT_VALUE_CHANGED, this);
|
||||
filterTextareaWidget = textarea;
|
||||
lv_obj_set_flex_grow(textarea, 1);
|
||||
service::gui::keyboardAddTextArea(textarea);
|
||||
|
||||
@ -219,21 +228,19 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_set_width(list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(list, 1);
|
||||
lv_obj_set_style_border_width(list, 0, 0);
|
||||
data->listWidget = list;
|
||||
listWidget = list;
|
||||
}
|
||||
|
||||
static void onStart(AppContext& app) {
|
||||
auto data = std::make_shared<Data>();
|
||||
data->updateTimer = std::make_unique<Timer>(Timer::Type::Once, onUpdateTimer, data);
|
||||
app.setData(data);
|
||||
void onStart(AppContext& app) override {
|
||||
updateTimer = std::make_unique<Timer>(Timer::Type::Once, updateTimerCallback, nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "TimeZone",
|
||||
.name = "Select timezone",
|
||||
.type = TypeHidden,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<TimeZoneApp>
|
||||
};
|
||||
|
||||
void start() {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
#include "CoreDefines.h"
|
||||
#include "app/App.h"
|
||||
#include "app/AppManifest.h"
|
||||
#include "hal/usb/Usb.h"
|
||||
#include "lvgl.h"
|
||||
#include "lvgl/Toolbar.h"
|
||||
#include "hal/usb/Usb.h"
|
||||
|
||||
#define TAG "usb_settings"
|
||||
|
||||
@ -10,7 +13,9 @@ static void onRebootMassStorage(TT_UNUSED lv_event_t* event) {
|
||||
hal::usb::rebootIntoMassStorageSdmmc();
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
class UsbSettingsApp : public App {
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
auto* toolbar = lvgl::toolbar_create(parent, app);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
@ -31,15 +36,15 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_label_set_text(label_b, second);
|
||||
lv_obj_align_to(label_b, label_a, LV_ALIGN_OUT_BOTTOM_MID, 0, 4);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "UsbSettings",
|
||||
.name = "USB",
|
||||
.icon = LV_SYMBOL_USB,
|
||||
.type = TypeSettings,
|
||||
.onShow = onShow
|
||||
.type = Type::Settings,
|
||||
.createApp = create<UsbSettingsApp>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -15,9 +15,9 @@ namespace tt::app::wifiapsettings {
|
||||
extern const AppManifest manifest;
|
||||
|
||||
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
|
||||
const AppContext* _Nullable optWifiApSettingsApp() {
|
||||
app::AppContext* app = service::loader::getCurrentApp();
|
||||
if (app->getManifest().id == manifest.id) {
|
||||
const std::shared_ptr<AppContext> _Nullable optWifiApSettingsApp() {
|
||||
auto app = service::loader::getCurrentAppContext();
|
||||
if (app != nullptr && app->getManifest().id == manifest.id) {
|
||||
return app;
|
||||
} else {
|
||||
return nullptr;
|
||||
@ -41,7 +41,7 @@ static void onPressForget(TT_UNUSED lv_event_t* event) {
|
||||
static void onToggleAutoConnect(lv_event_t* event) {
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
|
||||
auto* app = optWifiApSettingsApp();
|
||||
auto app = optWifiApSettingsApp();
|
||||
if (app == nullptr) {
|
||||
return;
|
||||
}
|
||||
@ -66,7 +66,9 @@ static void onToggleAutoConnect(lv_event_t* event) {
|
||||
}
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
class WifiApSettings : public App {
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
auto paremeters = app.getParameters();
|
||||
tt_check(paremeters != nullptr, "Parameters missing");
|
||||
std::string ssid = paremeters->getString("ssid");
|
||||
@ -119,10 +121,10 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
}
|
||||
}
|
||||
|
||||
void onResult(TT_UNUSED AppContext& app, TT_UNUSED Result result, const Bundle& bundle) {
|
||||
auto index = alertdialog::getResultIndex(bundle);
|
||||
void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED Result result, std::unique_ptr<Bundle> bundle) override {
|
||||
auto index = alertdialog::getResultIndex(*bundle);
|
||||
if (index == 0) { // Yes
|
||||
auto* app = optWifiApSettingsApp();
|
||||
auto app = optWifiApSettingsApp();
|
||||
if (app == nullptr) {
|
||||
return;
|
||||
}
|
||||
@ -148,14 +150,14 @@ void onResult(TT_UNUSED AppContext& app, TT_UNUSED Result result, const Bundle&
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "WifiApSettings",
|
||||
.name = "Wi-Fi AP Settings",
|
||||
.icon = LV_SYMBOL_WIFI,
|
||||
.type = TypeHidden,
|
||||
.onShow = onShow,
|
||||
.onResult = onResult
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<WifiApSettings>
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -16,9 +16,9 @@ extern const AppManifest manifest;
|
||||
|
||||
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
|
||||
std::shared_ptr<WifiConnect> _Nullable optWifiConnect() {
|
||||
app::AppContext* app = service::loader::getCurrentApp();
|
||||
if (app->getManifest().id == manifest.id) {
|
||||
return std::static_pointer_cast<WifiConnect>(app->getData());
|
||||
auto appContext = service::loader::getCurrentAppContext();
|
||||
if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
|
||||
return std::static_pointer_cast<WifiConnect>(appContext->getApp());
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
@ -105,30 +105,12 @@ void WifiConnect::onHide(TT_UNUSED AppContext& app) {
|
||||
unlock();
|
||||
}
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto wifi = std::static_pointer_cast<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 = {
|
||||
.id = "WifiConnect",
|
||||
.name = "Wi-Fi Connect",
|
||||
.icon = LV_SYMBOL_WIFI,
|
||||
.type = TypeHidden,
|
||||
.onStart = &onStart,
|
||||
.onShow = &onShow,
|
||||
.onHide = &onHide
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<WifiConnect>
|
||||
};
|
||||
|
||||
void start(const std::string& ssid, const std::string& password) {
|
||||
|
||||
@ -18,9 +18,9 @@ extern const AppManifest manifest;
|
||||
|
||||
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
|
||||
std::shared_ptr<WifiManage> _Nullable optWifiManage() {
|
||||
app::AppContext* app = service::loader::getCurrentApp();
|
||||
if (app->getManifest().id == manifest.id) {
|
||||
return std::static_pointer_cast<WifiManage>(app->getData());
|
||||
auto appContext = service::loader::getCurrentAppContext();
|
||||
if (appContext != nullptr && appContext->getManifest().id == manifest.id) {
|
||||
return std::static_pointer_cast<WifiManage>(appContext->getApp());
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
@ -146,33 +146,12 @@ void WifiManage::onHide(TT_UNUSED AppContext& app) {
|
||||
unlock();
|
||||
}
|
||||
|
||||
// region Manifest methods
|
||||
|
||||
static void onStart(AppContext& app) {
|
||||
auto wifi = std::make_shared<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 = {
|
||||
.id = "WifiManage",
|
||||
.name = "Wi-Fi",
|
||||
.icon = LV_SYMBOL_WIFI,
|
||||
.type = TypeSettings,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.onHide = onHide
|
||||
.type = Type::Settings,
|
||||
.createApp = create<WifiManage>
|
||||
};
|
||||
|
||||
void start() {
|
||||
|
||||
@ -19,9 +19,8 @@ Gui* gui = nullptr;
|
||||
void onLoaderMessage(const void* message, TT_UNUSED void* context) {
|
||||
auto* event = static_cast<const loader::LoaderEvent*>(message);
|
||||
if (event->type == loader::LoaderEventTypeApplicationShowing) {
|
||||
app::AppContext& app = event->app_showing.app;
|
||||
const app::AppManifest& app_manifest = app.getManifest();
|
||||
showApp(app, app_manifest.onShow, app_manifest.onHide);
|
||||
auto app_instance = service::loader::getCurrentAppContext();
|
||||
showApp(app_instance);
|
||||
} else if (event->type == loader::LoaderEventTypeApplicationHiding) {
|
||||
hideApp();
|
||||
}
|
||||
@ -94,27 +93,25 @@ void requestDraw() {
|
||||
thread_flags_set(thread_id, GUI_THREAD_FLAG_DRAW);
|
||||
}
|
||||
|
||||
void showApp(app::AppContext& app, ViewPortShowCallback on_show, ViewPortHideCallback on_hide) {
|
||||
void showApp(std::shared_ptr<app::AppContext> app) {
|
||||
lock();
|
||||
tt_check(gui->appViewPort == nullptr);
|
||||
gui->appViewPort = view_port_alloc(app, on_show, on_hide);
|
||||
tt_check(gui->appToRender == nullptr);
|
||||
gui->appToRender = std::move(app);
|
||||
unlock();
|
||||
requestDraw();
|
||||
}
|
||||
|
||||
void hideApp() {
|
||||
lock();
|
||||
ViewPort* view_port = gui->appViewPort;
|
||||
tt_check(view_port != nullptr);
|
||||
tt_check(gui->appToRender != nullptr);
|
||||
|
||||
// We must lock the LVGL port, because the viewport hide callbacks
|
||||
// might call LVGL APIs (e.g. to remove the keyboard from the screen root)
|
||||
tt_check(lvgl::lock(configTICK_RATE_HZ));
|
||||
view_port_hide(view_port);
|
||||
gui->appToRender->getApp()->onHide(*gui->appToRender);
|
||||
lvgl::unlock();
|
||||
|
||||
view_port_free(view_port);
|
||||
gui->appViewPort = nullptr;
|
||||
gui->appToRender = nullptr;
|
||||
unlock();
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/AppInstance.h"
|
||||
#include "app/AppContext.h"
|
||||
#include "ViewPort.h"
|
||||
|
||||
namespace tt::service::gui {
|
||||
|
||||
@ -10,10 +10,8 @@ typedef struct Gui Gui;
|
||||
/**
|
||||
* Set the app viewport in the gui state and request the gui to draw it.
|
||||
* @param[in] app
|
||||
* @param[in] onShow
|
||||
* @param[in] onHide
|
||||
*/
|
||||
void showApp(app::AppContext& app, ViewPortShowCallback onShow, ViewPortHideCallback onHide);
|
||||
void showApp(std::shared_ptr<app::AppContext> app);
|
||||
|
||||
/**
|
||||
* Hide the current app's viewport.
|
||||
|
||||
@ -9,9 +9,10 @@ namespace tt::service::gui {
|
||||
|
||||
#define TAG "gui"
|
||||
|
||||
static lv_obj_t* createAppViews(Gui* gui, lv_obj_t* parent, app::AppContext& app) {
|
||||
static lv_obj_t* createAppViews(Gui* gui, lv_obj_t* parent) {
|
||||
lv_obj_send_event(gui->statusbarWidget, LV_EVENT_DRAW_MAIN, nullptr);
|
||||
lv_obj_t* child_container = lv_obj_create(parent);
|
||||
lv_obj_set_style_pad_all(child_container, 0, 0);
|
||||
lv_obj_set_width(child_container, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(child_container, 1);
|
||||
|
||||
@ -34,19 +35,17 @@ void redraw(Gui* gui) {
|
||||
if (lvgl::lock(1000)) {
|
||||
lv_obj_clean(gui->appRootWidget);
|
||||
|
||||
ViewPort* view_port = gui->appViewPort;
|
||||
if (view_port != nullptr) {
|
||||
app::AppContext& app = view_port->app;
|
||||
if (gui->appToRender != nullptr) {
|
||||
|
||||
app::Flags flags = app.getFlags();
|
||||
app::Flags flags = std::static_pointer_cast<app::AppInstance>(gui->appToRender)->getFlags();
|
||||
if (flags.showStatusbar) {
|
||||
lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
lv_obj_t* container = createAppViews(gui, gui->appRootWidget, app);
|
||||
view_port_show(view_port, container);
|
||||
lv_obj_t* container = createAppViews(gui, gui->appRootWidget);
|
||||
gui->appToRender->getApp()->onShow(*gui->appToRender, container);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "nothing to draw");
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -6,8 +6,9 @@
|
||||
#include "RtosCompat.h"
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include "esp_heap_caps.h"
|
||||
#include "TactilityHeadless.h"
|
||||
#include "app/ElfApp.h"
|
||||
#include "esp_heap_caps.h"
|
||||
|
||||
#else
|
||||
#include "lvgl/LvglSync.h"
|
||||
@ -61,17 +62,22 @@ void stopApp() {
|
||||
loader_singleton->dispatcherThread->dispatch(onStopAppMessage, nullptr);
|
||||
}
|
||||
|
||||
app::AppContext* _Nullable getCurrentApp() {
|
||||
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext() {
|
||||
tt_assert(loader_singleton);
|
||||
if (loader_singleton->mutex.lock(10 / portTICK_PERIOD_MS)) {
|
||||
app::AppInstance* app = loader_singleton->appStack.top();
|
||||
auto app = loader_singleton->appStack.top();
|
||||
loader_singleton->mutex.unlock();
|
||||
return dynamic_cast<app::AppContext*>(app);
|
||||
return std::move(app);
|
||||
} else {
|
||||
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() {
|
||||
tt_assert(loader_singleton);
|
||||
// it's safe to return pubsub without locking
|
||||
@ -97,9 +103,9 @@ static const char* appStateToString(app::State state) {
|
||||
}
|
||||
}
|
||||
|
||||
static void transitionAppToState(app::AppInstance& app, app::State state) {
|
||||
const app::AppManifest& manifest = app.getManifest();
|
||||
const app::State old_state = app.getState();
|
||||
static void transitionAppToState(std::shared_ptr<app::AppInstance> app, app::State state) {
|
||||
const app::AppManifest& manifest = app->getManifest();
|
||||
const app::State old_state = app->getState();
|
||||
|
||||
TT_LOG_I(
|
||||
TAG,
|
||||
@ -111,49 +117,35 @@ static void transitionAppToState(app::AppInstance& app, app::State state) {
|
||||
|
||||
switch (state) {
|
||||
case app::StateInitial:
|
||||
app.setState(app::StateInitial);
|
||||
app->setState(app::StateInitial);
|
||||
break;
|
||||
case app::StateStarted:
|
||||
if (manifest.onStart != nullptr) {
|
||||
manifest.onStart(app);
|
||||
}
|
||||
app.setState(app::StateStarted);
|
||||
app->getApp()->onStart(*app);
|
||||
app->setState(app::StateStarted);
|
||||
break;
|
||||
case app::StateShowing: {
|
||||
LoaderEvent event_showing = {
|
||||
.type = LoaderEventTypeApplicationShowing,
|
||||
.app_showing = {
|
||||
.app = app
|
||||
}
|
||||
};
|
||||
LoaderEvent event_showing = { .type = LoaderEventTypeApplicationShowing };
|
||||
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_showing);
|
||||
app.setState(app::StateShowing);
|
||||
app->setState(app::StateShowing);
|
||||
break;
|
||||
}
|
||||
case app::StateHiding: {
|
||||
LoaderEvent event_hiding = {
|
||||
.type = LoaderEventTypeApplicationHiding,
|
||||
.app_hiding = {
|
||||
.app = app
|
||||
}
|
||||
};
|
||||
LoaderEvent event_hiding = { .type = LoaderEventTypeApplicationHiding };
|
||||
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_hiding);
|
||||
app.setState(app::StateHiding);
|
||||
app->setState(app::StateHiding);
|
||||
break;
|
||||
}
|
||||
case app::StateStopped:
|
||||
if (manifest.onStop) {
|
||||
manifest.onStop(app);
|
||||
}
|
||||
app.setData(nullptr);
|
||||
app.setState(app::StateStopped);
|
||||
// TODO: Verify manifest
|
||||
app->getApp()->onStop(*app);
|
||||
app->setState(app::StateStopped);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static LoaderStatus startAppWithManifestInternal(
|
||||
const app::AppManifest* manifest,
|
||||
std::shared_ptr<const Bundle> _Nullable parameters
|
||||
const std::shared_ptr<app::AppManifest>& manifest,
|
||||
const std::shared_ptr<const Bundle> _Nullable& parameters
|
||||
) {
|
||||
tt_check(loader_singleton != nullptr);
|
||||
|
||||
@ -165,29 +157,23 @@ static LoaderStatus startAppWithManifestInternal(
|
||||
}
|
||||
|
||||
auto previous_app = !loader_singleton->appStack.empty() ? loader_singleton->appStack.top() : nullptr;
|
||||
auto new_app = new app::AppInstance(*manifest, parameters);
|
||||
new_app->mutableFlags().showStatusbar = (manifest->type != app::TypeBoot);
|
||||
|
||||
auto new_app = std::make_shared<app::AppInstance>(manifest, parameters);
|
||||
|
||||
new_app->mutableFlags().showStatusbar = (manifest->type != app::Type::Boot);
|
||||
|
||||
loader_singleton->appStack.push(new_app);
|
||||
transitionAppToState(*new_app, app::StateInitial);
|
||||
transitionAppToState(*new_app, app::StateStarted);
|
||||
transitionAppToState(new_app, app::StateInitial);
|
||||
transitionAppToState(new_app, app::StateStarted);
|
||||
|
||||
// We might have to hide the previous app first
|
||||
if (previous_app != nullptr) {
|
||||
transitionAppToState(*previous_app, app::StateHiding);
|
||||
transitionAppToState(previous_app, app::StateHiding);
|
||||
}
|
||||
|
||||
transitionAppToState(*new_app, app::StateShowing);
|
||||
transitionAppToState(new_app, app::StateShowing);
|
||||
|
||||
LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStarted};
|
||||
tt_pubsub_publish(loader_singleton->pubsubInternal, &event_internal);
|
||||
|
||||
LoaderEvent event_external = {
|
||||
.type = LoaderEventTypeApplicationStarted,
|
||||
.app_started = {
|
||||
.app = *new_app
|
||||
}
|
||||
};
|
||||
LoaderEvent event_external = { .type = LoaderEventTypeApplicationStarted };
|
||||
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_external);
|
||||
|
||||
return LoaderStatus::Ok;
|
||||
@ -208,7 +194,7 @@ static LoaderStatus startAppInternal(
|
||||
) {
|
||||
TT_LOG_I(TAG, "Start by id %s", id.c_str());
|
||||
|
||||
const app::AppManifest* manifest = app::findAppById(id);
|
||||
auto manifest = app::findAppById(id);
|
||||
if (manifest == nullptr) {
|
||||
TT_LOG_E(TAG, "App not found: %s", id.c_str());
|
||||
return LoaderStatus::ErrorUnknownApp;
|
||||
@ -233,75 +219,75 @@ static void stopAppInternal() {
|
||||
}
|
||||
|
||||
// Stop current app
|
||||
app::AppInstance* app_to_stop = loader_singleton->appStack.top();
|
||||
auto app_to_stop = loader_singleton->appStack.top();
|
||||
|
||||
if (original_stack_size == 1 && app_to_stop->getManifest().type != app::TypeBoot) {
|
||||
if (original_stack_size == 1 && app_to_stop->getManifest().type != app::Type::Boot) {
|
||||
TT_LOG_E(TAG, "Stop app: can't stop root app");
|
||||
return;
|
||||
}
|
||||
|
||||
auto result_holder = std::move(app_to_stop->getResult());
|
||||
bool result_set = false;
|
||||
app::Result result;
|
||||
std::unique_ptr<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::StateStopped);
|
||||
transitionAppToState(app_to_stop, app::StateHiding);
|
||||
transitionAppToState(app_to_stop, app::StateStopped);
|
||||
|
||||
loader_singleton->appStack.pop();
|
||||
delete app_to_stop;
|
||||
|
||||
// We only expect the app to be referenced within the current scope
|
||||
if (app_to_stop.use_count() > 1) {
|
||||
TT_LOG_W(TAG, "Memory leak: Stopped %s, but use count is %ld", app_to_stop->getManifest().id.c_str(), app_to_stop.use_count() - 1);
|
||||
}
|
||||
|
||||
// Refcount is expected to be 2: 1 within app_to_stop and 1 within the current scope
|
||||
if (app_to_stop->getApp().use_count() > 2) {
|
||||
TT_LOG_W(TAG, "Memory leak: Stopped %s, but use count is %ld", app_to_stop->getManifest().id.c_str(), app_to_stop->getApp().use_count() - 2);
|
||||
}
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
TT_LOG_I(TAG, "Free heap: %zu", heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
|
||||
#endif
|
||||
|
||||
app::AppOnResult on_result = nullptr;
|
||||
app::AppInstance* app_to_resume = nullptr;
|
||||
std::shared_ptr<app::AppInstance> instance_to_resume;
|
||||
// If there's a previous app, resume it
|
||||
if (!loader_singleton->appStack.empty()) {
|
||||
app_to_resume = loader_singleton->appStack.top();
|
||||
tt_assert(app_to_resume);
|
||||
transitionAppToState(*app_to_resume, app::StateShowing);
|
||||
|
||||
on_result = app_to_resume->getManifest().onResult;
|
||||
instance_to_resume = loader_singleton->appStack.top();
|
||||
tt_assert(instance_to_resume);
|
||||
transitionAppToState(instance_to_resume, app::StateShowing);
|
||||
}
|
||||
|
||||
// Unlock so that we can send results to app and they can also start/stop new apps while processing these results
|
||||
scoped_lock->unlock();
|
||||
// WARNING: After this point we cannot change the app states from this method directly anymore as we don't have a lock!
|
||||
|
||||
LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStopped};
|
||||
tt_pubsub_publish(loader_singleton->pubsubInternal, &event_internal);
|
||||
|
||||
LoaderEvent event_external = {
|
||||
.type = LoaderEventTypeApplicationStopped,
|
||||
.app_stopped = {
|
||||
.manifest = manifest
|
||||
}
|
||||
};
|
||||
LoaderEvent event_external = { .type = LoaderEventTypeApplicationStopped };
|
||||
tt_pubsub_publish(loader_singleton->pubsubExternal, &event_external);
|
||||
|
||||
if (on_result != nullptr && app_to_resume != nullptr) {
|
||||
if (result_holder != nullptr) {
|
||||
auto result_bundle = result_holder->resultData.get();
|
||||
if (instance_to_resume != nullptr) {
|
||||
if (result_set) {
|
||||
if (result_bundle != nullptr) {
|
||||
on_result(
|
||||
*app_to_resume,
|
||||
result_holder->result,
|
||||
*result_bundle
|
||||
instance_to_resume->getApp()->onResult(
|
||||
*instance_to_resume,
|
||||
result,
|
||||
std::move(result_bundle)
|
||||
);
|
||||
} else {
|
||||
const Bundle empty_bundle;
|
||||
on_result(
|
||||
*app_to_resume,
|
||||
result_holder->result,
|
||||
empty_bundle
|
||||
instance_to_resume->getApp()->onResult(
|
||||
*instance_to_resume,
|
||||
result,
|
||||
nullptr
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const Bundle empty_bundle;
|
||||
on_result(
|
||||
*app_to_resume,
|
||||
app::ResultCancelled,
|
||||
empty_bundle
|
||||
instance_to_resume->getApp()->onResult(
|
||||
*instance_to_resume,
|
||||
app::Result::Cancelled,
|
||||
nullptr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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. */
|
||||
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) */
|
||||
app::AppContext* _Nullable getCurrentApp();
|
||||
std::shared_ptr<app::App> _Nullable getCurrentApp();
|
||||
|
||||
/**
|
||||
* @brief PubSub for LoaderEvent
|
||||
|
||||
@ -87,9 +87,9 @@ void ScreenshotTask::taskMain() {
|
||||
}
|
||||
}
|
||||
} else if (work.type == TASK_WORK_TYPE_APPS) {
|
||||
app::AppContext* _Nullable app = loader::getCurrentApp();
|
||||
if (app) {
|
||||
const app::AppManifest& manifest = app->getManifest();
|
||||
auto appContext = loader::getCurrentAppContext();
|
||||
if (appContext != nullptr) {
|
||||
const app::AppManifest& manifest = appContext->getManifest();
|
||||
if (manifest.id != last_app_id) {
|
||||
kernel::delayMillis(100);
|
||||
last_app_id = manifest.id;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "tt_app_context.h"
|
||||
#include <app/App.h>
|
||||
#include <app/AppContext.h>
|
||||
|
||||
struct AppContextDataWrapper {
|
||||
@ -9,28 +10,17 @@ extern "C" {
|
||||
|
||||
#define HANDLE_AS_APP_CONTEXT(handle) ((tt::app::AppContext*)(handle))
|
||||
|
||||
void* _Nullable tt_app_context_get_data(AppContextHandle handle) {
|
||||
auto wrapper = std::reinterpret_pointer_cast<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) {
|
||||
return (BundleHandle)HANDLE_AS_APP_CONTEXT(handle)->getParameters().get();
|
||||
}
|
||||
|
||||
void tt_app_context_set_result(AppContextHandle handle, Result result, BundleHandle _Nullable bundle) {
|
||||
auto shared_bundle = std::shared_ptr<tt::Bundle>((tt::Bundle*)bundle);
|
||||
HANDLE_AS_APP_CONTEXT(handle)->setResult((tt::app::Result)result, std::move(shared_bundle));
|
||||
auto shared_bundle = std::unique_ptr<tt::Bundle>((tt::Bundle*)bundle);
|
||||
HANDLE_AS_APP_CONTEXT(handle)->getApp()->setResult((tt::app::Result)result, std::move(shared_bundle));
|
||||
}
|
||||
|
||||
bool tt_app_context_has_result(AppContextHandle handle) {
|
||||
return HANDLE_AS_APP_CONTEXT(handle)->hasResult();
|
||||
return HANDLE_AS_APP_CONTEXT(handle)->getApp()->hasResult();
|
||||
}
|
||||
|
||||
}
|
||||
@ -11,14 +11,6 @@ typedef void* AppContextHandle;
|
||||
/** @return the data that was attached to this app context */
|
||||
void* _Nullable tt_app_context_get_data(AppContextHandle handle);
|
||||
|
||||
/**
|
||||
* Attach data to an application context.
|
||||
* Don't forget to manually delete allocated memory when onStopped() is called.
|
||||
* @param[in] handle the app context handle
|
||||
* @param[in] data the data to attach
|
||||
*/
|
||||
void tt_app_context_set_data(AppContextHandle handle, void* _Nullable data);
|
||||
|
||||
/** @return the bundle that belongs to this application, or null */
|
||||
BundleHandle _Nullable tt_app_context_get_parameters(AppContextHandle handle);
|
||||
|
||||
|
||||
@ -1,105 +1,29 @@
|
||||
#include "tt_app_manifest.h"
|
||||
|
||||
#include <Check.h>
|
||||
#include <Log.h>
|
||||
#include <app/ElfApp.h>
|
||||
#include <app/AppCompatC.h>
|
||||
|
||||
#define TAG "tt_app"
|
||||
|
||||
AppOnStart elfOnStart = nullptr;
|
||||
AppOnStop elfOnStop = nullptr;
|
||||
AppOnShow elfOnShow = nullptr;
|
||||
AppOnHide elfOnHide = nullptr;
|
||||
AppOnResult elfOnResult = nullptr;
|
||||
|
||||
static void onStartWrapper(tt::app::AppContext& context) {
|
||||
if (elfOnStart != nullptr) {
|
||||
TT_LOG_I(TAG, "onStartWrapper");
|
||||
elfOnStart(&context);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "onStartWrapper not set");
|
||||
}
|
||||
}
|
||||
|
||||
static void onStopWrapper(tt::app::AppContext& context) {
|
||||
if (elfOnStop != nullptr) {
|
||||
TT_LOG_I(TAG, "onStopWrapper");
|
||||
elfOnStop(&context);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "onStopWrapper not set");
|
||||
}
|
||||
}
|
||||
|
||||
static void onShowWrapper(tt::app::AppContext& context, lv_obj_t* parent) {
|
||||
if (elfOnShow != nullptr) {
|
||||
TT_LOG_I(TAG, "onShowWrapper");
|
||||
elfOnShow(&context, parent);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "onShowWrapper not set");
|
||||
}
|
||||
}
|
||||
|
||||
static void onHideWrapper(tt::app::AppContext& context) {
|
||||
if (elfOnHide != nullptr) {
|
||||
TT_LOG_I(TAG, "onHideWrapper");
|
||||
elfOnHide(&context);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "onHideWrapper not set");
|
||||
}
|
||||
}
|
||||
|
||||
static void onResultWrapper(tt::app::AppContext& context, tt::app::Result result, const tt::Bundle& resultData) {
|
||||
if (elfOnResult != nullptr) {
|
||||
TT_LOG_I(TAG, "onResultWrapper");
|
||||
Result convertedResult = AppResultError;
|
||||
switch (result) {
|
||||
case tt::app::ResultOk:
|
||||
convertedResult = AppResultOk;
|
||||
break;
|
||||
case tt::app::ResultCancelled:
|
||||
convertedResult = AppResultCancelled;
|
||||
break;
|
||||
case tt::app::ResultError:
|
||||
convertedResult = AppResultError;
|
||||
break;
|
||||
}
|
||||
elfOnResult(&context, convertedResult, (BundleHandle)&resultData);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "onResultWrapper not set");
|
||||
}
|
||||
}
|
||||
|
||||
tt::app::AppManifest manifest = {
|
||||
.id = "ElfWrapperInTactilityC",
|
||||
.name = "",
|
||||
.icon = "",
|
||||
.onStart = onStartWrapper,
|
||||
.onStop = onStopWrapper,
|
||||
.onShow = onShowWrapper,
|
||||
.onHide = onHideWrapper,
|
||||
.onResult = onResultWrapper
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
void tt_set_app_manifest(
|
||||
const char* name,
|
||||
const char* _Nullable icon,
|
||||
AppOnStart onStart,
|
||||
AppOnStop _Nullable onStop,
|
||||
AppOnShow _Nullable onShow,
|
||||
AppOnHide _Nullable onHide,
|
||||
AppOnResult _Nullable onResult
|
||||
void tt_app_register(
|
||||
const ExternalAppManifest* manifest
|
||||
) {
|
||||
#ifdef ESP_PLATFORM
|
||||
manifest.name = name;
|
||||
manifest.icon = icon ? icon : "";
|
||||
elfOnStart = onStart;
|
||||
elfOnStop = onStop;
|
||||
elfOnShow = onShow;
|
||||
elfOnHide = onHide;
|
||||
elfOnResult = onResult;
|
||||
tt::app::setElfAppManifest(manifest);
|
||||
tt_assert((manifest->createData == nullptr) == (manifest->destroyData == nullptr));
|
||||
tt::app::setElfAppManifest(
|
||||
manifest->name,
|
||||
manifest->icon,
|
||||
(tt::app::CreateData)manifest->createData,
|
||||
(tt::app::DestroyData)manifest->destroyData,
|
||||
(tt::app::OnStart)manifest->onStart,
|
||||
(tt::app::OnStop)manifest->onStop,
|
||||
(tt::app::OnShow)manifest->onShow,
|
||||
(tt::app::OnHide)manifest->onHide,
|
||||
(tt::app::OnResult)manifest->onResult
|
||||
);
|
||||
#else
|
||||
tt_crash("TactilityC is intended for PC/Simulator");
|
||||
#endif
|
||||
|
||||
@ -7,39 +7,47 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Important: These values must map to tt::app::Result values exactly */
|
||||
typedef enum {
|
||||
AppResultOk,
|
||||
AppResultCancelled,
|
||||
AppResultError
|
||||
AppResultOk = 0,
|
||||
AppResultCancelled = 1,
|
||||
AppResultError = 2
|
||||
} Result;
|
||||
|
||||
typedef void* AppContextHandle;
|
||||
|
||||
typedef void (*AppOnStart)(AppContextHandle app);
|
||||
typedef void (*AppOnStop)(AppContextHandle app);
|
||||
typedef void (*AppOnShow)(AppContextHandle app, lv_obj_t* parent);
|
||||
typedef void (*AppOnHide)(AppContextHandle app);
|
||||
typedef void (*AppOnResult)(AppContextHandle app, Result result, BundleHandle resultData);
|
||||
/** Important: These function types must map to t::app types exactly */
|
||||
typedef void* (*AppCreateData)();
|
||||
typedef void (*AppDestroyData)(void* data);
|
||||
typedef void (*AppOnStart)(AppContextHandle app, void* _Nullable data);
|
||||
typedef void (*AppOnStop)(AppContextHandle app, void* _Nullable data);
|
||||
typedef void (*AppOnShow)(AppContextHandle app, void* _Nullable data, lv_obj_t* parent);
|
||||
typedef void (*AppOnHide)(AppContextHandle app, void* _Nullable data);
|
||||
typedef void (*AppOnResult)(AppContextHandle app, void* _Nullable data, Result result, BundleHandle resultData);
|
||||
|
||||
/**
|
||||
* This is used to register the manifest of an external app.
|
||||
* @param[in] name the application's human-readable name
|
||||
* @param[in] icon the optional application icon (you can use LV_SYMBOL_* too)
|
||||
* @param[in] onStart called when the app is launched (started)
|
||||
* @param[in] onStop called when the app is exited (stopped)
|
||||
* @param[in] onShow called when the app is about to be shown to the user (app becomes visible)
|
||||
* @param[in] onHide called when the app is about to be invisible to the user (e.g. other app was launched by this app, and this app goes to the background)
|
||||
* @param[in] onResult called when the app receives a result after launching another app
|
||||
*/
|
||||
void tt_set_app_manifest(
|
||||
const char* name,
|
||||
const char* _Nullable icon,
|
||||
AppOnStart onStart,
|
||||
AppOnStop _Nullable onStop,
|
||||
AppOnShow _Nullable onShow,
|
||||
AppOnHide _Nullable onHide,
|
||||
AppOnResult _Nullable onResult
|
||||
);
|
||||
typedef struct {
|
||||
/** The application's human-readable name */
|
||||
const char* name;
|
||||
/** The application icon (you can use LV_SYMBOL_* too) */
|
||||
const char* _Nullable icon;
|
||||
/** The application can allocate data to re-use later (e.g. struct with state) */
|
||||
AppCreateData _Nullable createData;
|
||||
/** If createData is specified, this one must be specified too */
|
||||
AppDestroyData _Nullable destroyData;
|
||||
/** Called when the app is launched (started) */
|
||||
AppOnStart _Nullable onStart;
|
||||
/** Called when the app is exited (stopped) */
|
||||
AppOnStop _Nullable onStop;
|
||||
/** Called when the app is about to be shown to the user (app becomes visible) */
|
||||
AppOnShow _Nullable onShow;
|
||||
/** Called when the app is about to be invisible to the user (e.g. other app was launched by this app, and this app goes to the background) */
|
||||
AppOnHide _Nullable onHide;
|
||||
/** Called when the app receives a result after launching another app */
|
||||
AppOnResult _Nullable onResult;
|
||||
} ExternalAppManifest;
|
||||
|
||||
/** This is used to register the manifest of an external app. */
|
||||
void tt_app_register(const ExternalAppManifest* manifest);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@ -22,8 +22,7 @@ extern "C" {
|
||||
|
||||
const struct esp_elfsym elf_symbols[] {
|
||||
// Tactility
|
||||
ESP_ELFSYM_EXPORT(tt_app_context_get_data),
|
||||
ESP_ELFSYM_EXPORT(tt_app_context_set_data),
|
||||
ESP_ELFSYM_EXPORT(tt_app_register),
|
||||
ESP_ELFSYM_EXPORT(tt_app_context_get_parameters),
|
||||
ESP_ELFSYM_EXPORT(tt_app_context_set_result),
|
||||
ESP_ELFSYM_EXPORT(tt_app_context_has_result),
|
||||
@ -39,7 +38,6 @@ const struct esp_elfsym elf_symbols[] {
|
||||
ESP_ELFSYM_EXPORT(tt_bundle_put_bool),
|
||||
ESP_ELFSYM_EXPORT(tt_bundle_put_int32),
|
||||
ESP_ELFSYM_EXPORT(tt_bundle_put_string),
|
||||
ESP_ELFSYM_EXPORT(tt_set_app_manifest),
|
||||
ESP_ELFSYM_EXPORT(tt_hal_i2c_start),
|
||||
ESP_ELFSYM_EXPORT(tt_hal_i2c_stop),
|
||||
ESP_ELFSYM_EXPORT(tt_hal_i2c_is_started),
|
||||
|
||||
@ -14,7 +14,7 @@ void tt_service_loader_stop_app() {
|
||||
}
|
||||
|
||||
AppContextHandle tt_service_loader_get_current_app() {
|
||||
return tt::service::loader::getCurrentApp();
|
||||
return tt::service::loader::getCurrentAppContext().get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,7 +2,14 @@
|
||||
|
||||
#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_MESSAGE_SIZE 128
|
||||
#else
|
||||
@ -35,12 +42,6 @@ LogEntry* copyLogEntries(unsigned int& outIndex);
|
||||
|
||||
} // namespace tt
|
||||
|
||||
#ifdef ESP_TARGET
|
||||
#include "esp_log.h"
|
||||
#else
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#endif
|
||||
|
||||
#ifdef ESP_TARGET
|
||||
|
||||
|
||||
@ -78,4 +78,13 @@ std::string join(const std::vector<std::string>& input, const std::string& delim
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string removeFileExtension(const std::string& input) {
|
||||
auto index = input.find('.');
|
||||
if (index != std::string::npos) {
|
||||
return input.substr(0, index);
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -66,5 +66,9 @@ std::basic_string<T> lowercase(const std::basic_string<T>& input) {
|
||||
return std::move(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the first part of a file name right up (and excluding) the first period character.
|
||||
*/
|
||||
std::string removeFileExtension(const std::string& input);
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -34,7 +34,7 @@ public:
|
||||
* @param[in] callback The callback function
|
||||
* @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();
|
||||
|
||||
|
||||
@ -2,12 +2,9 @@
|
||||
#include "Timer.h"
|
||||
|
||||
#include "service/ServiceContext.h"
|
||||
#include "TactilityCore.h"
|
||||
#include "TactilityHeadless.h"
|
||||
#include "service/ServiceRegistry.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#define TAG "sdcard_service"
|
||||
|
||||
namespace tt::service::sdcard {
|
||||
@ -28,7 +25,6 @@ struct ServiceData {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
static void onUpdate(std::shared_ptr<void> context) {
|
||||
auto sdcard = tt::hal::getConfiguration()->sdcard;
|
||||
if (sdcard == nullptr) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user