Compare commits

..

No commits in common. "98bbea3e9ca780f5760cb87978b0852a178f528a" and "dcf28d0868ce8d718b545e88600c2d28274b05a2" have entirely different histories.

49 changed files with 342 additions and 520 deletions

View File

@ -1,9 +1,8 @@
#include "UnPhoneFeatures.h"
#include <Tactility/app/App.h>
#include <Tactility/Log.h>
#include <Tactility/kernel/Kernel.h>
#include <Tactility/service/loader/Loader.h>
#include <driver/gpio.h>
#include <driver/rtc_io.h>
#include <esp_sleep.h>
@ -44,7 +43,7 @@ static int32_t buttonHandlingThreadMain(void* context) {
// The buttons might generate more than 1 click because of how they are built
TT_LOG_I(TAG, "Pressed button %d", pinNumber);
if (pinNumber == pin::BUTTON1) {
tt::app::stop();
tt::service::loader::stopApp();
}
// Debounce all events for a short period of time

View File

@ -5,6 +5,5 @@
# This deployment is used when compiling apps in ./ExternalApps
#
rm -rf release/TactilitySDK
./Buildscripts/release-sdk.sh release/TactilitySDK

View File

@ -2,12 +2,14 @@
## Higher Priority
- External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility
Check during installation process, but also when starting (SD card might have old app install from before Tactility OS update)
- Show a warning in the web installer when flashing CYD 28R board regarding v1/v2/v3
- External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility.
- Make a URL handler. Use it for handling local files. Match file types with apps.
Create some kind of "intent" handler like on Android.
The intent can have an action (e.g. view), a URL and an optional bundle.
The manifest can provide the intent handler
- CrowPanel Basic 3.5": check why GraphicsDemo fails
- CrowPanel Basic 3.5": check why System Info doesn't show storage info
- When an SD card is detected, check if it has been initialized and assigned as data partition.
If the user choses to select it, then copy files from /data over to it.
Write the user choice to a file on the card.
@ -15,6 +17,14 @@
The latter is used for auto-selecting it as data partition.
- Support direct installation of an `.app` file with `tactility.py install helloworld.app <ip>`
- Support `tactility.py target <ip>` to remember the device IP address.
- External app error code 22 should warn that the user might've forgotten a `main()` entry point
- Bug: `Buildscript/release-sdk-current.sh` should delete the currently released SDK. It should probably also output it with versioning and target platform naming so it can be referred to as if it is a real release.
- Tactility docs: external app dev guide should explain [debugging](https://docs.zephyrproject.org/latest/services/llext/debug.html)
- elf_loader changes/suggestions:
- Make entry-point optional (so we can build libraries, or have the `manifest` as a global symbol)
- Implement support for alternative symbol lists. e.g. a function pointer that resolves a single symbol.
- Implement the entire list of [soft-float library functions](https://gcc.gnu.org/onlinedocs/gccint/Soft-float-library-routines.html) to `tt_init.cpp`
- `tactility.py` should stop running applications when it is: uninstalling, installing, or running an application that is already running.
## Medium Priority
@ -30,11 +40,9 @@
- Bug: Crash handling app cannot be exited with an EncoderDevice. (current work-around is to manually reset the device)
- I2C app should show error when I2C port is disabled when the scan button was manually pressed
- TactilitySDK: Support automatic scanning of header files so that we can generate the `tt_init.cpp` symbols list.
- elf_loader: split up symbol lists further (after radio support is implemented)
## Lower Priority
- elf_loader: make main() entry-point optional (so we can build libraries, or have the `manifest` as a global symbol)
- Implement system suspend that turns off the screen
- The boot button on some devices can be used as GPIO_NUM_0 at runtime
- Localize all apps

View File

@ -88,20 +88,6 @@ LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable pa
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
void stop();
/** @brief Stop a specific app and any apps it might have launched on the stack.
* @param[in] id the app id
*/
void stop(const std::string& id);
/** @brief Stop all app instances that match with this identifier and also stop the apps they started.
* @warning onResult() will only be called for the resulting app that gets shown (if any)
* @param[in] id the id of the app to stop
*/
void stopAll(const std::string& id);
/** @return true if the app is running somewhere in the app stack (doesn't have to be the top-most app) */
bool isRunning(const std::string& id);
/** @return the currently running app context (it is only ever null before the splash screen is shown) */
std::shared_ptr<AppContext> _Nullable getCurrentAppContext();

View File

@ -1,9 +1,7 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::imageviewer {
LaunchId start(const std::string& file);
void start(const std::string& file);
}
};

View File

@ -1,6 +1,5 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h>
#include <string>
@ -12,7 +11,7 @@
*/
namespace tt::app::inputdialog {
LaunchId start(const std::string& title, const std::string& message, const std::string& prefilled = "");
void start(const std::string& title, const std::string& message, const std::string& prefilled = "");
/**
* @return the text that was in the field when OK was pressed, or otherwise empty string

View File

@ -1,14 +1,11 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::notes {
/**
* Start the notes app with the specified text file.
* @param[in] filePath the path to the text file to open
* @return the launch id
*/
LaunchId start(const std::string& filePath);
void start(const std::string& filePath);
}

View File

@ -1,6 +1,5 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h>
#include <string>
@ -16,7 +15,7 @@
*/
namespace tt::app::selectiondialog {
LaunchId start(const std::string& title, const std::vector<std::string>& items);
void start(const std::string& title, const std::vector<std::string>& items);
/**
* Get the index of the item that the user selected.

View File

@ -1,9 +1,7 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::wifimanage {
LaunchId start();
void start();
} // namespace

View File

@ -1,99 +1,45 @@
#pragma once
#include <Tactility/app/AppInstance.h>
#include <Tactility/app/AppManifest.h>
#include "Tactility/app/AppManifest.h"
#include <Tactility/Bundle.h>
#include <Tactility/DispatcherThread.h>
#include <Tactility/PubSub.h>
#include <Tactility/service/Service.h>
#include <memory>
namespace tt::service::loader {
// region LoaderEvent for PubSub
class LoaderService final : public Service {
public:
enum class Event {
ApplicationStarted,
ApplicationShowing,
ApplicationHiding,
ApplicationStopped
};
private:
std::shared_ptr<PubSub<Event>> pubsubExternal = std::make_shared<PubSub<Event>>();
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::vector<std::shared_ptr<app::AppInstance>> appStack;
app::LaunchId nextLaunchId = 0;
/** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods.
* This includes full LVGL redraw via Gui::redraw()
*/
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
void onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters);
void onStopTopAppMessage(const std::string& id);
void onStopAllAppMessage(const std::string& id);
void transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state);
int findAppInStack(const std::string& id) const;
bool onStart(TT_UNUSED ServiceContext& service) override {
dispatcherThread->start();
return true;
}
void onStop(TT_UNUSED ServiceContext& service) override {
// Send stop signal to thread and wait for thread to finish
mutex.withLock([this] {
dispatcherThread->stop();
});
}
public:
/**
* @brief Start an app given an app id and an optional bundle with parameters
* @param id the app identifier
* @param parameters optional parameter bundle
* @return the launch id
*/
app::LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters);
/**
* @brief Stops the top-most app (the one that is currently active shown to the user
* @warning Avoid calling this directly and use stopTop(id) instead
*/
void stopTop();
/**
* @brief Stops the top-most app if the id is still matching by the time the stop event arrives.
* @param id the id of the app to stop
*/
void stopTop(const std::string& id);
/**
* @brief Stops all apps with the provided id and any apps that were pushed on top of the stack after the original app was started.
* @param id the id of the app to stop
*/
void stopAll(const std::string& id);
/** @return the AppContext of the top-most application */
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
/** @return true if the app is running anywhere in the app stack (the app does not have to be the top-most one for this to return true) */
bool isRunning(const std::string& id) const;
/** @return the PubSub object that is responsible for event publishing */
std::shared_ptr<PubSub<Event>> getPubsub() const { return pubsubExternal; }
enum class LoaderEvent{
ApplicationStarted,
ApplicationShowing,
ApplicationHiding,
ApplicationStopped
};
std::shared_ptr<LoaderService> _Nullable findLoaderService();
// endregion LoaderEvent for PubSub
/**
* @brief Start an app
* @param[in] id application name or id
* @param[in] parameters optional parameters to pass onto the application
* @return the launch id
*/
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
/** @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) */
std::shared_ptr<app::App> _Nullable getCurrentApp();
/**
* @brief PubSub for LoaderEvent
*/
std::shared_ptr<PubSub<LoaderEvent>> getPubsub();
} // namespace

View File

@ -13,11 +13,11 @@
namespace tt::app {
enum class State {
Initial, // AppInstance was created, but the state hasn't advanced yet
Created, // App was placed into memory
Showing, // App view was created
Hiding, // App view was destroyed
Destroyed // App was removed from memory
Initial, // App is being activated in loader
Started, // App is in memory
Showing, // App view is created
Hiding, // App view is destroyed
Stopped // App is not in memory
};
/**

View File

@ -1,9 +1,7 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::i2cscanner {
LaunchId start();
void start();
}

View File

@ -1,9 +1,7 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::launcher {
LaunchId start();
void start();
}

View File

@ -1,9 +1,7 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::localesettings {
LaunchId start();
void start();
}

View File

@ -1,9 +1,7 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::timedatesettings {
LaunchId start();
void start();
}

View File

@ -1,11 +1,10 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h>
namespace tt::app::timezone {
LaunchId start();
void start();
std::string getResultName(const Bundle& bundle);
std::string getResultCode(const Bundle& bundle);

View File

@ -45,7 +45,7 @@ public:
/**
* Start the app with optional pre-filled fields.
*/
LaunchId start(const std::string& ssid = "", const std::string& password = "");
void start(const std::string& ssid = "", const std::string& password = "");
bool optSsidParameter(const std::shared_ptr<const Bundle>& bundle, std::string& ssid);

View File

@ -1,7 +1,7 @@
#pragma once
#ifdef ESP_PLATFORM
#include <Tactility/service/Service.h>
#include "Tactility/service/Service.h"
#include <Tactility/Mutex.h>

View File

@ -17,19 +17,19 @@ namespace tt::service::gui {
#define GUI_THREAD_FLAG_EXIT (1 << 2)
#define GUI_THREAD_FLAG_ALL (GUI_THREAD_FLAG_DRAW | GUI_THREAD_FLAG_INPUT | GUI_THREAD_FLAG_EXIT)
class GuiService final : public Service {
class GuiService : public Service {
// Thread and lock
Thread* thread = nullptr;
Mutex mutex = Mutex(Mutex::Type::Recursive);
PubSub<loader::LoaderService::Event>::SubscriptionHandle loader_pubsub_subscription = nullptr;
PubSub<loader::LoaderEvent>::SubscriptionHandle loader_pubsub_subscription = nullptr;
// Layers and Canvas
lv_obj_t* appRootWidget = nullptr;
lv_obj_t* statusbarWidget = nullptr;
// App-specific
std::shared_ptr<app::AppInstance> appToRender = nullptr;
std::shared_ptr<app::AppContext> appToRender = nullptr;
lv_obj_t* _Nullable keyboard = nullptr;
lv_group_t* keyboardGroup = nullptr;
@ -38,7 +38,7 @@ class GuiService final : public Service {
static int32_t guiMain();
void onLoaderEvent(loader::LoaderService::Event event);
void onLoaderEvent(loader::LoaderEvent event);
lv_obj_t* createAppViews(lv_obj_t* parent);
@ -52,7 +52,7 @@ class GuiService final : public Service {
tt_check(mutex.unlock());
}
void showApp(std::shared_ptr<app::AppInstance> app);
void showApp(std::shared_ptr<app::AppContext> app);
void hideApp();

View File

@ -284,7 +284,7 @@ void run(const Configuration& config) {
TT_LOG_I(TAG, "Starting boot app");
// The boot app takes care of registering system apps, user services and user apps
addApp(app::boot::manifest);
app::start(app::boot::manifest.appId);
service::loader::startApp(app::boot::manifest.appId);
TT_LOG_I(TAG, "Main dispatcher ready");
while (true) {

View File

@ -3,47 +3,20 @@
namespace tt::app {
constexpr auto* TAG = "App";
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) {
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
return service->start(id, std::move(parameters));
return service::loader::startApp(id, std::move(parameters));
}
void stop() {
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
service->stopTop();
}
void stop(const std::string& id) {
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
service->stopTop(id);
}
void stopAll(const std::string& id) {
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
service->stopAll(id);
}
bool isRunning(const std::string& id) {
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
return service->isRunning(id);
service::loader::stopApp();
}
std::shared_ptr<AppContext> _Nullable getCurrentAppContext() {
const auto service = service::loader::findLoaderService();
assert(service != nullptr);
return service->getCurrentAppContext();
return service::loader::getCurrentAppContext();
}
std::shared_ptr<App> _Nullable getCurrentApp() {
const auto app_context = getCurrentAppContext();
return (app_context != nullptr) ? app_context->getApp() : nullptr;
return service::loader::getCurrentApp();
}
}

View File

@ -174,11 +174,6 @@ bool install(const std::string& path) {
return false;
}
// If the app was already running, then stop it
if (isRunning(manifest.appId)) {
stopAll(manifest.appId);
}
target_path_lock.lock();
const std::string renamed_target_path = std::format("{}/{}", app_parent_path, manifest.appId);
if (file::isDirectory(renamed_target_path)) {
@ -207,12 +202,6 @@ bool install(const std::string& path) {
bool uninstall(const std::string& appId) {
TT_LOG_I(TAG, "Uninstalling app %s", appId.c_str());
// If the app was running, then stop it
if (isRunning(appId)) {
stopAll(appId);
}
auto app_path = getAppInstallPath(appId);
return file::withLock<bool>(app_path, [&app_path, &appId] {
if (!file::isDirectory(app_path)) {

View File

@ -23,8 +23,6 @@ static std::string getErrorCodeString(int error_code) {
return "out of memory";
case ENOSYS:
return "missing symbol";
case EINVAL:
return "invalid argument or main() missing";
default:
return std::format("code {}", error_code);
}
@ -139,14 +137,14 @@ public:
staticParametersSetCount = 0;
if (!startElf()) {
stop();
service::loader::stopApp();
auto message = lastError.empty() ? "Application failed to start." : std::format("Application failed to start: {}", lastError);
alertdialog::start("Error", message);
return;
}
if (staticParametersSetCount == 0) {
stop();
service::loader::stopApp();
alertdialog::start("Error", "Application failed to start: application failed to register itself");
return;
}

View File

@ -28,7 +28,7 @@ LaunchId start(const std::string& title, const std::string& message, const std::
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_joined);
return app::start(manifest.appId, bundle);
return service::loader::startApp(manifest.appId, bundle);
}
LaunchId start(const std::string& title, const std::string& message) {
@ -36,7 +36,7 @@ LaunchId start(const std::string& title, const std::string& message) {
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, "OK");
return app::start(manifest.appId, bundle);
return service::loader::startApp(manifest.appId, bundle);
}
int32_t getResultIndex(const Bundle& bundle) {
@ -72,9 +72,9 @@ private:
auto bundle = std::make_unique<Bundle>();
bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index);
setResult(Result::Ok, std::move(bundle));
setResult(app::Result::Ok, std::move(bundle));
stop(manifest.appId);
service::loader::stopApp();
}
static void createButton(lv_obj_t* parent, const std::string& text, size_t index) {

View File

@ -1,6 +1,6 @@
#include <Tactility/app/AppRegistration.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/lvgl/Toolbar.h>
#include "Tactility/app/AppRegistration.h"
#include "Tactility/service/loader/Loader.h"
#include "Tactility/lvgl/Toolbar.h"
#include <Tactility/Assets.h>
@ -9,11 +9,11 @@
namespace tt::app::applist {
class AppListApp final : public App {
class AppListApp : public App {
static void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
start(manifest->appId);
service::loader::startApp(manifest->appId);
}
static void createAppWidget(const std::shared_ptr<AppManifest>& manifest, lv_obj_t* list) {

View File

@ -24,7 +24,6 @@
namespace tt::app::boot {
constexpr auto* TAG = "Boot";
extern const AppManifest manifest;
static std::shared_ptr<hal::display::DisplayDevice> getHalDisplay() {
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
@ -108,7 +107,7 @@ class BootApp : public App {
TT_LOG_I(TAG, "initFromBootApp");
initFromBootApp();
waitForMinimalSplashDuration(start_time);
stop(manifest.appId);
service::loader::stopApp();
startNextApp();
}
@ -129,7 +128,7 @@ class BootApp : public App {
return;
}
start(boot_properties.launcherAppId);
service::loader::startApp(boot_properties.launcherAppId);
}
static int getSmallestDimension() {

View File

@ -15,10 +15,8 @@
namespace tt::app::crashdiagnostics {
extern const AppManifest manifest;
void onContinuePressed(TT_UNUSED lv_event_t* event) {
stop(manifest.appId);
service::loader::stopApp();
launcher::start();
}
@ -50,7 +48,7 @@ public:
int qr_version;
if (!getQrVersionForBinaryDataLength(url_length, qr_version)) {
TT_LOG_E(TAG, "QR is too large");
stop(manifest.appId);
service::loader::stopApp();
return;
}
@ -58,7 +56,7 @@ public:
auto qrcodeData = std::make_shared<uint8_t[]>(qrcode_getBufferSize(qr_version));
if (qrcodeData == nullptr) {
TT_LOG_E(TAG, "Failed to allocate QR buffer");
stop();
service::loader::stopApp();
return;
}
@ -66,7 +64,7 @@ public:
TT_LOG_I(TAG, "QR init text");
if (qrcode_initText(&qrcode, qrcodeData.get(), qr_version, ECC_LOW, url.c_str()) != 0) {
TT_LOG_E(TAG, "QR init text failed");
stop(manifest.appId);
service::loader::stopApp();
return;
}
@ -86,7 +84,7 @@ public:
pixel_size = 1;
} else {
TT_LOG_E(TAG, "QR code won't fit screen");
stop(manifest.appId);
service::loader::stopApp();
return;
}
@ -101,7 +99,7 @@ public:
auto* draw_buf = lv_draw_buf_create(pixel_size * qrcode.size, pixel_size * qrcode.size, LV_COLOR_FORMAT_RGB565, LV_STRIDE_AUTO);
if (draw_buf == nullptr) {
TT_LOG_E(TAG, "Draw buffer alloc");
stop(manifest.appId);
service::loader::stopApp();
return;
}
@ -132,7 +130,7 @@ extern const AppManifest manifest = {
};
void start() {
app::start(manifest.appId);
service::loader::startApp(manifest.appId);
}
} // namespace

View File

@ -1,7 +1,10 @@
#ifdef ESP_PLATFORM
#include "Tactility/lvgl/Lvgl.h"
#include <Tactility/Tactility.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/lvgl/Toolbar.h>
@ -9,7 +12,6 @@
#include <Tactility/service/development/DevelopmentSettings.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/Tactility.h>
#include <Tactility/Timer.h>
#include <cstring>
@ -18,7 +20,6 @@
namespace tt::app::development {
constexpr const char* TAG = "Development";
extern const AppManifest manifest;
class DevelopmentApp final : public App {
@ -84,7 +85,7 @@ public:
service = service::development::findService();
if (service == nullptr) {
TT_LOG_E(TAG, "Service not found");
stop(manifest.appId);
service::loader::stopApp();
}
}

View File

@ -42,7 +42,7 @@ extern const AppManifest manifest = {
};
void start() {
app::start(manifest.appId);
service::loader::startApp(manifest.appId);
}
} // namespace

View File

@ -45,7 +45,7 @@ public:
auto bundle = std::make_unique<Bundle>();
bundle->putString("path", path);
setResult(Result::Ok, std::move(bundle));
stop(manifest.appId);
service::loader::stopApp();
});
}
@ -67,13 +67,13 @@ extern const AppManifest manifest = {
LaunchId startForExistingFile() {
auto bundle = std::make_shared<Bundle>();
setMode(*bundle, Mode::Existing);
return start(manifest.appId, bundle);
return service::loader::startApp(manifest.appId, bundle);
}
LaunchId startForExistingOrNewFile() {
auto bundle = std::make_shared<Bundle>();
setMode(*bundle, Mode::ExistingOrNew);
return start(manifest.appId, bundle);
return service::loader::startApp(manifest.appId, bundle);
}
} // namespace

View File

@ -410,8 +410,8 @@ extern const AppManifest manifest = {
.createApp = create<I2cScannerApp>
};
LaunchId start() {
return app::start(manifest.appId);
void start() {
service::loader::startApp(manifest.appId);
}
} // namespace

View File

@ -1,6 +1,7 @@
#include <Tactility/lvgl/Style.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/loader/Loader.h>
#include "Tactility/lvgl/Style.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/TactilityCore.h>
#include <Tactility/StringUtils.h>
@ -10,10 +11,10 @@ namespace tt::app::imageviewer {
extern const AppManifest manifest;
constexpr auto* TAG = "ImageViewer";
constexpr auto* IMAGE_VIEWER_FILE_ARGUMENT = "file";
#define TAG "image_viewer"
#define IMAGE_VIEWER_FILE_ARGUMENT "file"
class ImageViewerApp final : public App {
class ImageViewerApp : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
auto wrapper = lv_obj_create(parent);
@ -66,10 +67,10 @@ extern const AppManifest manifest = {
.createApp = create<ImageViewerApp>
};
LaunchId start(const std::string& file) {
void start(const std::string& file) {
auto parameters = std::make_shared<Bundle>();
parameters->putString(IMAGE_VIEWER_FILE_ARGUMENT, file);
return app::start(manifest.appId, parameters);
service::loader::startApp(manifest.appId, parameters);
}
} // namespace

View File

@ -1,31 +1,32 @@
#include <Tactility/app/inputdialog/InputDialog.h>
#include "Tactility/app/inputdialog/InputDialog.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/TactilityCore.h>
#include <lvgl.h>
namespace tt::app::inputdialog {
constexpr auto* PARAMETER_BUNDLE_KEY_TITLE = "title";
constexpr auto* PARAMETER_BUNDLE_KEY_MESSAGE = "message";
constexpr auto* PARAMETER_BUNDLE_KEY_PREFILLED = "prefilled";
constexpr auto* RESULT_BUNDLE_KEY_RESULT = "result";
#define PARAMETER_BUNDLE_KEY_TITLE "title"
#define PARAMETER_BUNDLE_KEY_MESSAGE "message"
#define PARAMETER_BUNDLE_KEY_PREFILLED "prefilled"
#define RESULT_BUNDLE_KEY_RESULT "result"
constexpr auto* DEFAULT_TITLE = "Input";
#define DEFAULT_TITLE "Input"
constexpr auto* TAG = "InputDialog";
#define TAG "input_dialog"
extern const AppManifest manifest;
class InputDialogApp;
LaunchId start(const std::string& title, const std::string& message, const std::string& prefilled) {
void start(const std::string& title, const std::string& message, const std::string& prefilled) {
auto bundle = std::make_shared<Bundle>();
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_PREFILLED, prefilled);
return app::start(manifest.appId, bundle);
service::loader::startApp(manifest.appId, bundle);
}
std::string getResult(const Bundle& bundle) {
@ -43,7 +44,7 @@ static std::string getTitleParameter(const std::shared_ptr<const Bundle>& bundle
}
}
class InputDialogApp final : public App {
class InputDialogApp : public App {
static void createButton(lv_obj_t* parent, const std::string& text, void* callbackContext) {
lv_obj_t* button = lv_button_create(parent);
@ -72,7 +73,7 @@ class InputDialogApp final : public App {
setResult(Result::Cancelled);
}
stop(manifest.appId);
service::loader::stopApp();
}
public:

View File

@ -60,7 +60,7 @@ class LauncherApp final : public App {
static void onAppPressed(TT_UNUSED lv_event_t* e) {
auto* appId = static_cast<const char*>(lv_event_get_user_data(e));
start(appId);
service::loader::startApp(appId);
}
static void onPowerOffPressed(lv_event_t* e) {
@ -76,7 +76,7 @@ public:
settings::BootSettings boot_properties;
if (settings::loadBootSettings(boot_properties) && !boot_properties.autoStartAppId.empty()) {
TT_LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str());
start(boot_properties.autoStartAppId);
service::loader::startApp(boot_properties.autoStartAppId);
}
}
@ -144,8 +144,8 @@ extern const AppManifest manifest = {
.createApp = create<LauncherApp>
};
LaunchId start() {
return app::start(manifest.appId);
void start() {
service::loader::startApp(manifest.appId);
}
} // namespace

View File

@ -5,12 +5,12 @@
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/settings/Time.h>
#include <Tactility/StringUtils.h>
#include <Tactility/settings/Language.h>
#include <lvgl.h>
#include <map>
#include <sstream>
#include <Tactility/StringUtils.h>
#include <Tactility/settings/Language.h>
namespace tt::app::localesettings {
@ -18,7 +18,7 @@ constexpr auto* TAG = "LocaleSettings";
extern const AppManifest manifest;
class LocaleSettingsApp final : public App {
class LocaleSettingsApp : public App {
tt::i18n::TextResources textResources = tt::i18n::TextResources("/system/app/LocaleSettings/i18n");
Mutex mutex = Mutex(Mutex::Type::Recursive);
lv_obj_t* timeZoneLabel = nullptr;
@ -169,8 +169,8 @@ extern const AppManifest manifest = {
.createApp = create<LocaleSettingsApp>
};
LaunchId start() {
return app::start(manifest.appId);
void start() {
service::loader::startApp(manifest.appId);
}
} // namespace

View File

@ -1,10 +1,10 @@
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/fileselection/FileSelection.h>
#include <Tactility/file/FileLock.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/Assets.h>
#include "Tactility/app/AppManifest.h"
#include "Tactility/app/fileselection/FileSelection.h"
#include "Tactility/file/FileLock.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/service/loader/Loader.h"
#include "Tactility/Assets.h"
#include <Tactility/file/File.h>
@ -15,7 +15,7 @@ namespace tt::app::notes {
constexpr auto* TAG = "Notes";
constexpr auto* NOTES_FILE_ARGUMENT = "file";
class NotesApp final : public App {
class NotesApp : public App {
lv_obj_t* uiCurrentFileName;
lv_obj_t* uiDropDownMenu;
@ -213,10 +213,9 @@ extern const AppManifest manifest = {
.createApp = create<NotesApp>
};
LaunchId start(const std::string& filePath) {
void start(const std::string& filePath) {
auto parameters = std::make_shared<Bundle>();
parameters->putString(NOTES_FILE_ARGUMENT, filePath);
return app::start(manifest.appId, parameters);
service::loader::startApp(manifest.appId, parameters);
}
} // namespace tt::app::notes

View File

@ -1,7 +1,8 @@
#include <Tactility/app/selectiondialog/SelectionDialog.h>
#include "Tactility/app/selectiondialog/SelectionDialog.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/StringUtils.h>
#include <Tactility/TactilityCore.h>
@ -9,23 +10,23 @@
namespace tt::app::selectiondialog {
constexpr auto* PARAMETER_BUNDLE_KEY_TITLE = "title";
constexpr auto* PARAMETER_BUNDLE_KEY_ITEMS = "items";
constexpr auto* RESULT_BUNDLE_KEY_INDEX = "index";
#define PARAMETER_BUNDLE_KEY_TITLE "title"
#define PARAMETER_BUNDLE_KEY_ITEMS "items"
#define RESULT_BUNDLE_KEY_INDEX "index"
constexpr auto* PARAMETER_ITEM_CONCATENATION_TOKEN = ";;";
constexpr auto* DEFAULT_TITLE = "Select...";
#define PARAMETER_ITEM_CONCATENATION_TOKEN ";;"
#define DEFAULT_TITLE "Select..."
constexpr auto* TAG = "SelectionDialog";
#define TAG "selection_dialog"
extern const AppManifest manifest;
LaunchId start(const std::string& title, const std::vector<std::string>& items) {
void start(const std::string& title, const std::vector<std::string>& items) {
std::string items_joined = string::join(items, PARAMETER_ITEM_CONCATENATION_TOKEN);
auto bundle = std::make_shared<Bundle>();
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_ITEMS, items_joined);
return app::start(manifest.appId, bundle);
service::loader::startApp(manifest.appId, bundle);
}
int32_t getResultIndex(const Bundle& bundle) {
@ -43,7 +44,9 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
}
}
class SelectionDialogApp final : public App {
class SelectionDialogApp : public App {
private:
static void onListItemSelectedCallback(lv_event_t* e) {
auto app = std::static_pointer_cast<SelectionDialogApp>(getCurrentApp());
@ -56,8 +59,8 @@ class SelectionDialogApp final : public App {
TT_LOG_I(TAG, "Selected item at index %d", index);
auto bundle = std::make_unique<Bundle>();
bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, (int32_t)index);
setResult(Result::Ok, std::move(bundle));
stop(manifest.appId);
setResult(app::Result::Ok, std::move(bundle));
service::loader::stopApp();
}
static void createChoiceItem(void* parent, const std::string& title, size_t index) {
@ -87,12 +90,12 @@ public:
if (items.empty() || items.front().empty()) {
TT_LOG_E(TAG, "No items provided");
setResult(Result::Error);
stop(manifest.appId);
service::loader::stopApp();
} else if (items.size() == 1) {
auto result_bundle = std::make_unique<Bundle>();
result_bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, 0);
setResult(Result::Ok, std::move(result_bundle));
stop(manifest.appId);
service::loader::stopApp();
TT_LOG_W(TAG, "Auto-selecting single item");
} else {
size_t index = 0;
@ -103,7 +106,7 @@ public:
} else {
TT_LOG_E(TAG, "No items provided");
setResult(Result::Error);
stop(manifest.appId);
service::loader::stopApp();
}
}
};

View File

@ -1,6 +1,6 @@
#include <Tactility/app/AppRegistration.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/loader/Loader.h>
#include "Tactility/app/AppRegistration.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/Assets.h>
#include <Tactility/Check.h>
@ -12,7 +12,7 @@ namespace tt::app::settings {
static void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
start(manifest->appId);
service::loader::startApp(manifest->appId);
}
static void createWidget(const std::shared_ptr<AppManifest>& manifest, void* parent) {
@ -23,7 +23,7 @@ static void createWidget(const std::shared_ptr<AppManifest>& manifest, void* par
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest.get());
}
class SettingsApp final : public App {
class SettingsApp : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
@ -36,7 +36,7 @@ class SettingsApp final : public App {
lv_obj_set_flex_grow(list, 1);
auto manifests = getApps();
std::ranges::sort(manifests, SortAppManifestByName);
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
for (const auto& manifest: manifests) {
if (manifest->appCategory == Category::Settings) {
createWidget(manifest, list);

View File

@ -12,7 +12,7 @@ constexpr auto* TAG = "TimeDate";
extern const AppManifest manifest;
class TimeDateSettingsApp final : public App {
class TimeDateSettingsApp : public App {
Mutex mutex = Mutex(Mutex::Type::Recursive);
@ -64,8 +64,8 @@ extern const AppManifest manifest = {
.createApp = create<TimeDateSettingsApp>
};
LaunchId start() {
return app::start(manifest.appId);
void start() {
service::loader::startApp(manifest.appId);
}
} // namespace

View File

@ -104,7 +104,8 @@ class TimeZoneApp final : public App {
setResultCode(*bundle, entry.code);
setResult(Result::Ok, std::move(bundle));
stop(manifest.appId);
service::loader::stopApp();
}
static void createListItem(lv_obj_t* list, const std::string& title, size_t index) {
@ -233,8 +234,8 @@ extern const AppManifest manifest = {
.createApp = create<TimeZoneApp>
};
LaunchId start() {
return app::start(manifest.appId);
void start() {
service::loader::startApp(manifest.appId);
}
}

View File

@ -33,7 +33,7 @@ void WifiConnect::onWifiEvent(service::wifi::WifiEvent event) {
case service::wifi::WifiEvent::ConnectionSuccess:
if (getState().isConnecting()) {
state.setConnecting(false);
stop(manifest.appId);
service::loader::stopApp();
}
break;
default:
@ -102,11 +102,11 @@ extern const AppManifest manifest = {
.createApp = create<WifiConnect>
};
LaunchId start(const std::string& ssid, const std::string& password) {
void start(const std::string& ssid, const std::string& password) {
auto parameters = std::make_shared<Bundle>();
parameters->putString(WIFI_CONNECT_PARAM_SSID, ssid);
parameters->putString(WIFI_CONNECT_PARAM_PASSWORD, password);
return app::start(manifest.appId, parameters);
service::loader::startApp(manifest.appId, parameters);
}
bool optSsidParameter(const std::shared_ptr<const Bundle>& bundle, std::string& ssid) {

View File

@ -140,8 +140,8 @@ extern const AppManifest manifest = {
.createApp = create<WifiManage>
};
LaunchId start() {
return app::start(manifest.appId);
void start() {
service::loader::startApp(manifest.appId);
}
} // namespace

View File

@ -65,7 +65,7 @@ static const lv_obj_class_t toolbar_class = {
};
static void stop_app(TT_UNUSED lv_event_t* event) {
app::stop();
service::loader::stopApp();
}
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {

View File

@ -1,4 +1,4 @@
#include <Tactility/network/HttpdReq.h>
#include "Tactility/network/HttpdReq.h"
#include <memory>
#include <ranges>
@ -8,9 +8,9 @@
#ifdef ESP_PLATFORM
namespace tt::network {
#define TAG "network"
constexpr auto* TAG = "HttpdReq";
namespace tt::network {
bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value) {
size_t header_size = httpd_req_get_hdr_value_len(request, name.c_str());
@ -73,17 +73,11 @@ std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, si
assert(length > 0);
bytesRead = 0;
// We have to use malloc() because make_unique() throws an exception
// and we don't have exceptions enabled in the compiler settings
auto* buffer = static_cast<char*>(malloc(length));
if (buffer == nullptr) {
TT_LOG_E(TAG, LOG_MESSAGE_ALLOC_FAILED_FMT, length);
return nullptr;
}
auto result = std::make_unique<char[]>(length);
while (bytesRead < length) {
size_t read_size = length - bytesRead;
size_t bytes_received = httpd_req_recv(request, buffer + bytesRead, read_size);
size_t bytes_received = httpd_req_recv(request, result.get() + bytesRead, read_size);
if (bytes_received <= 0) {
TT_LOG_W(TAG, "Received %zu / %zu", bytesRead + bytes_received, length);
return nullptr;
@ -92,7 +86,7 @@ std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, si
bytesRead += bytes_received;
}
return std::unique_ptr<char[]>(std::move(buffer));
return result;
}
std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator) {

View File

@ -194,16 +194,9 @@ esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) {
return ESP_FAIL;
}
const auto& app_id = id_key_pos->second;
if (app::isRunning(app_id)) {
app::stopAll(app_id);
}
app::start(app_id);
app::start(id_key_pos->second);
TT_LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str());
httpd_resp_send(request, nullptr, 0);
return ESP_OK;
}

View File

@ -1,25 +1,25 @@
#include <Tactility/service/gui/GuiService.h>
#include "Tactility/service/gui/GuiService.h"
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/lvgl/Statusbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/app/AppInstance.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Statusbar.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/Tactility.h>
#include <Tactility/app/AppInstance.h>
#include <Tactility/service/ServiceRegistration.h>
namespace tt::service::gui {
extern const ServiceManifest manifest;
constexpr auto* TAG = "GuiService";
using namespace loader;
// region AppManifest
void GuiService::onLoaderEvent(LoaderService::Event event) {
if (event == LoaderService::Event::ApplicationShowing) {
auto app_instance = std::static_pointer_cast<app::AppInstance>(app::getCurrentAppContext());
void GuiService::onLoaderEvent(loader::LoaderEvent event) {
if (event == loader::LoaderEvent::ApplicationShowing) {
auto app_instance = app::getCurrentAppContext();
showApp(app_instance);
} else if (event == LoaderService::Event::ApplicationHiding) {
} else if (event == loader::LoaderEvent::ApplicationHiding) {
hideApp();
}
}
@ -125,9 +125,7 @@ bool GuiService::onStart(TT_UNUSED ServiceContext& service) {
[]() { return guiMain(); }
);
const auto loader = findLoaderService();
assert(loader != nullptr);
loader_pubsub_subscription = loader->getPubsub()->subscribe([this](auto event) {
loader_pubsub_subscription = loader::getPubsub()->subscribe([this](auto event) {
onLoaderEvent(event);
});
@ -170,9 +168,7 @@ bool GuiService::onStart(TT_UNUSED ServiceContext& service) {
void GuiService::onStop(TT_UNUSED ServiceContext& service) {
lock();
const auto loader = findLoaderService();
assert(loader != nullptr);
loader->getPubsub()->unsubscribe(loader_pubsub_subscription);
loader::getPubsub()->unsubscribe(loader_pubsub_subscription);
appToRender = nullptr;
isStarted = false;
@ -194,50 +190,36 @@ void GuiService::requestDraw() {
Thread::setFlags(thread_id, GUI_THREAD_FLAG_DRAW);
}
void GuiService::showApp(std::shared_ptr<app::AppInstance> app) {
auto lock = mutex.asScopedLock();
lock.lock();
void GuiService::showApp(std::shared_ptr<app::AppContext> app) {
lock();
if (!isStarted) {
TT_LOG_E(TAG, "Failed to show app %s: GUI not started", app->getManifest().appId.c_str());
return;
TT_LOG_W(TAG, "Failed to show app %s: GUI not started", app->getManifest().appId.c_str());
} else {
// Ensure previous app triggers onHide() logic
if (appToRender != nullptr) {
hideApp();
}
appToRender = std::move(app);
}
if (appToRender != nullptr && appToRender->getLaunchId() == app->getLaunchId()) {
TT_LOG_W(TAG, "Already showing %s", app->getManifest().appId.c_str());
return;
}
TT_LOG_I(TAG, "Showing %s", app->getManifest().appId.c_str());
// Ensure previous app triggers onHide() logic
if (appToRender != nullptr) {
hideApp();
}
appToRender = std::move(app);
unlock();
requestDraw();
}
void GuiService::hideApp() {
auto lock = mutex.asScopedLock();
lock.lock();
lock();
if (!isStarted) {
TT_LOG_E(TAG, "Failed to hide app: GUI not started");
return;
}
if (appToRender == nullptr) {
TT_LOG_W(TAG, "Failed to hide app: GUI not started");
} else if (appToRender == nullptr) {
TT_LOG_W(TAG, "hideApp() called but no app is currently shown");
return;
} else {
// 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));
appToRender->getApp()->onHide(*appToRender);
lvgl::unlock();
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)
lvgl::lock(portMAX_DELAY);
appToRender->getApp()->onHide(*appToRender);
lvgl::unlock();
appToRender = nullptr;
unlock();
}
std::shared_ptr<GuiService> findService() {

View File

@ -1,17 +1,19 @@
#include <Tactility/service/loader/Loader.h>
#include <Tactility/app/AppInstance.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/AppRegistration.h>
#include "Tactility/service/loader/Loader.h"
#include "Tactility/app/AppInstance.h"
#include "Tactility/app/AppManifest.h"
#include "Tactility/app/AppRegistration.h"
#include <Tactility/DispatcherThread.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <vector>
#include <stack>
#ifdef ESP_PLATFORM
#include <esp_heap_caps.h>
#include <utility>
#else
#include "Tactility/lvgl/LvglSync.h"
#endif
namespace tt::service::loader {
@ -19,7 +21,6 @@ namespace tt::service::loader {
constexpr auto* TAG = "Loader";
constexpr auto LOADER_TIMEOUT = (100 / portTICK_PERIOD_MS);
// Forward declaration
extern const ServiceManifest manifest;
static const char* appStateToString(app::State state) {
@ -27,19 +28,63 @@ static const char* appStateToString(app::State state) {
using enum app::State;
case Initial:
return "initial";
case Created:
case Started:
return "started";
case Showing:
return "showing";
case Hiding:
return "hiding";
case Destroyed:
case Stopped:
return "stopped";
default:
return "?";
}
}
// region AppManifest
class LoaderService final : public Service {
std::shared_ptr<PubSub<LoaderEvent>> pubsubExternal = std::make_shared<PubSub<LoaderEvent>>();
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::stack<std::shared_ptr<app::AppInstance>> appStack;
app::LaunchId nextLaunchId = 0;
/** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods.
* This includes full LVGL redraw via Gui::redraw()
*/
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
void onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters);
void onStopAppMessage(const std::string& id);
void transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state);
public:
bool onStart(TT_UNUSED ServiceContext& service) override {
dispatcherThread->start();
return true;
}
void onStop(TT_UNUSED ServiceContext& service) override {
// Send stop signal to thread and wait for thread to finish
mutex.withLock([this] {
dispatcherThread->stop();
});
}
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> parameters);
void stopApp();
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
std::shared_ptr<PubSub<LoaderEvent>> getPubsub() const { return pubsubExternal; }
};
std::shared_ptr<LoaderService> _Nullable optScreenshotService() {
return service::findServiceById<LoaderService>(manifest.id);
}
void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters) {
TT_LOG_I(TAG, "Start by id %s", id.c_str());
@ -55,22 +100,26 @@ void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launc
return;
}
auto previous_app = !appStack.empty() ? appStack[appStack.size() - 1]: nullptr;
auto previous_app = !appStack.empty() ? appStack.top() : nullptr;
auto new_app = std::make_shared<app::AppInstance>(app_manifest, launchId, parameters);
new_app->mutableFlags().hideStatusbar = (app_manifest->appFlags & app::AppManifest::Flags::HideStatusBar);
appStack.push(new_app);
transitionAppToState(new_app, app::State::Initial);
transitionAppToState(new_app, app::State::Started);
// We might have to hide the previous app first
if (previous_app != nullptr) {
transitionAppToState(previous_app, app::State::Hiding);
}
appStack.push_back(new_app);
transitionAppToState(new_app, app::State::Created);
transitionAppToState(new_app, app::State::Showing);
pubsubExternal->publish(LoaderEvent::ApplicationStarted);
}
void LoaderService::onStopTopAppMessage(const std::string& id) {
void LoaderService::onStopAppMessage(const std::string& id) {
auto lock = mutex.asScopedLock();
if (!lock.lock(LOADER_TIMEOUT)) {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
@ -85,7 +134,7 @@ void LoaderService::onStopTopAppMessage(const std::string& id) {
}
// Stop current app
auto app_to_stop = appStack[appStack.size() - 1];
auto app_to_stop = appStack.top();
if (app_to_stop->getManifest().appId != id) {
TT_LOG_E(TAG, "Stop app: id mismatch (wanted %s but found %s on top of stack)", id.c_str(), app_to_stop->getManifest().appId.c_str());
@ -107,9 +156,9 @@ void LoaderService::onStopTopAppMessage(const std::string& id) {
auto app_to_stop_launch_id = app_to_stop->getLaunchId();
transitionAppToState(app_to_stop, app::State::Hiding);
transitionAppToState(app_to_stop, app::State::Destroyed);
transitionAppToState(app_to_stop, app::State::Stopped);
appStack.pop_back();
appStack.pop();
// We only expect the app to be referenced within the current scope
if (app_to_stop.use_count() > 1) {
@ -128,7 +177,7 @@ void LoaderService::onStopTopAppMessage(const std::string& id) {
std::shared_ptr<app::AppInstance> instance_to_resume;
// If there's a previous app, resume it
if (!appStack.empty()) {
instance_to_resume = appStack[appStack.size() - 1];
instance_to_resume = appStack.top();
assert(instance_to_resume);
transitionAppToState(instance_to_resume, app::State::Showing);
}
@ -137,6 +186,8 @@ void LoaderService::onStopTopAppMessage(const std::string& id) {
lock.unlock();
// WARNING: After this point we cannot change the app states from this method directly anymore as we don't have a lock!
pubsubExternal->publish(LoaderEvent::ApplicationStopped);
if (instance_to_resume != nullptr) {
if (result_set) {
if (result_bundle != nullptr) {
@ -155,6 +206,7 @@ void LoaderService::onStopTopAppMessage(const std::string& id) {
);
}
} else {
const Bundle empty_bundle;
instance_to_resume->getApp()->onResult(
*instance_to_resume,
app_to_stop_launch_id,
@ -165,70 +217,6 @@ void LoaderService::onStopTopAppMessage(const std::string& id) {
}
}
int LoaderService::findAppInStack(const std::string& id) const {
auto lock = mutex.asScopedLock();
lock.lock();
for (size_t i = 0; i < appStack.size(); i++) {
if (appStack[i]->getManifest().appId == id) {
return i;
}
}
return -1;
}
void LoaderService::onStopAllAppMessage(const std::string& id) {
auto lock = mutex.asScopedLock();
if (!lock.lock(LOADER_TIMEOUT)) {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
return;
}
if (!isRunning(id)) {
TT_LOG_E(TAG, "Stop all: %s not running", id.c_str());
return;
}
int app_to_stop_index = findAppInStack(id);
if (app_to_stop_index < 0) {
TT_LOG_E(TAG, "Stop all: %s not found in stack", id.c_str());
return;
}
// Find an app to resume, if any
std::shared_ptr<app::AppInstance> instance_to_resume;
if (app_to_stop_index > 0) {
instance_to_resume = appStack[app_to_stop_index - 1];
assert(instance_to_resume);
}
// Stop all apps and find the LaunchId of the last-closed app, so we can call onResult() if needed
app::LaunchId last_launch_id = 0;
for (int i = appStack.size() - 1; i >= app_to_stop_index; i--) {
auto app_to_stop = appStack[i];
// Hide the app first in case it's still being shown
if (app_to_stop->getState() == app::State::Showing) {
transitionAppToState(app_to_stop, app::State::Hiding);
}
transitionAppToState(app_to_stop, app::State::Destroyed);
last_launch_id = app_to_stop->getLaunchId();
appStack.pop_back();
}
if (instance_to_resume != nullptr) {
TT_LOG_I(TAG, "Resuming %s", instance_to_resume->getManifest().appId.c_str());
transitionAppToState(instance_to_resume, app::State::Showing);
instance_to_resume->getApp()->onResult(
*instance_to_resume,
last_launch_id,
app::Result::Cancelled,
nullptr
);
}
}
void LoaderService::transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state) {
const app::AppManifest& app_manifest = app->getManifest();
const app::State old_state = app->getState();
@ -244,87 +232,89 @@ void LoaderService::transitionAppToState(const std::shared_ptr<app::AppInstance>
switch (state) {
using enum app::State;
case Initial:
tt_crash(LOG_MESSAGE_ILLEGAL_STATE);
case Created:
assert(app->getState() == app::State::Initial);
break;
case Started:
app->getApp()->onCreate(*app);
pubsubExternal->publish(Event::ApplicationStarted);
break;
case Showing: {
assert(app->getState() == app::State::Hiding || app->getState() == app::State::Created);
pubsubExternal->publish(Event::ApplicationShowing);
pubsubExternal->publish(LoaderEvent::ApplicationShowing);
break;
}
case Hiding: {
assert(app->getState() == app::State::Showing);
pubsubExternal->publish(Event::ApplicationHiding);
pubsubExternal->publish(LoaderEvent::ApplicationHiding);
break;
}
case Destroyed:
case Stopped:
// TODO: Verify manifest
app->getApp()->onDestroy(*app);
pubsubExternal->publish(Event::ApplicationStopped);
break;
}
app->setState(state);
}
app::LaunchId LoaderService::start(const std::string& id, std::shared_ptr<const Bundle> parameters) {
const auto launch_id = nextLaunchId++;
app::LaunchId LoaderService::startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
auto launch_id = nextLaunchId++;
dispatcherThread->dispatch([this, id, launch_id, parameters]() {
onStartAppMessage(id, launch_id, parameters);
});
return launch_id;
}
void LoaderService::stopTop() {
const auto& id = getCurrentAppContext()->getManifest().appId;
stopTop(id);
}
void LoaderService::stopTop(const std::string& id) {
TT_LOG_I(TAG, "dispatching stopTop(%s)", id.c_str());
dispatcherThread->dispatch([this, id] {
onStopTopAppMessage(id);
});
}
void LoaderService::stopAll(const std::string& id) {
TT_LOG_I(TAG, "dispatching stopAll(%s)", id.c_str());
dispatcherThread->dispatch([this, id] {
onStopAllAppMessage(id);
void LoaderService::stopApp() {
TT_LOG_I(TAG, "stopApp()");
auto id = getCurrentAppContext()->getManifest().appId;
dispatcherThread->dispatch([this, id]() {
onStopAppMessage(id);
});
TT_LOG_I(TAG, "dispatched");
}
std::shared_ptr<app::AppContext> _Nullable LoaderService::getCurrentAppContext() {
const auto lock = mutex.asScopedLock();
auto lock = mutex.asScopedLock();
lock.lock();
if (appStack.empty()) {
return nullptr;
} else {
return appStack[appStack.size() - 1];
}
return appStack.top();
}
bool LoaderService::isRunning(const std::string& id) const {
const auto lock = mutex.asScopedLock();
lock.lock();
for (const auto& app : appStack) {
if (app->getManifest().appId == id) {
return true;
}
}
return false;
// region Public API
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> parameters) {
TT_LOG_I(TAG, "Start app %s", id.c_str());
auto service = optScreenshotService();
assert(service);
return service->startApp(id, std::move(parameters));
}
std::shared_ptr<LoaderService> _Nullable findLoaderService() {
return service::findServiceById<LoaderService>(manifest.id);
void stopApp() {
TT_LOG_I(TAG, "Stop app");
auto service = optScreenshotService();
service->stopApp();
}
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext() {
auto service = optScreenshotService();
assert(service);
return service->getCurrentAppContext();
}
std::shared_ptr<app::App> _Nullable getCurrentApp() {
auto app_context = getCurrentAppContext();
return app_context != nullptr ? app_context->getApp() : nullptr;
}
std::shared_ptr<PubSub<LoaderEvent>> getPubsub() {
auto service = optScreenshotService();
assert(service);
return service->getPubsub();
}
// endregion Public API
extern const ServiceManifest manifest = {
.id = "Loader",
.createService = create<LoaderService>
};
// endregion
} // namespace

View File

@ -526,22 +526,6 @@ const esp_elfsym elf_symbols[] {
// lv_pct
ESP_ELFSYM_EXPORT(lv_pct),
ESP_ELFSYM_EXPORT(lv_pct_to_px),
// lv_spinbox
ESP_ELFSYM_EXPORT(lv_spinbox_create),
ESP_ELFSYM_EXPORT(lv_spinbox_decrement),
ESP_ELFSYM_EXPORT(lv_spinbox_get_rollover),
ESP_ELFSYM_EXPORT(lv_spinbox_get_step),
ESP_ELFSYM_EXPORT(lv_spinbox_get_value),
ESP_ELFSYM_EXPORT(lv_spinbox_increment),
ESP_ELFSYM_EXPORT(lv_spinbox_set_rollover),
ESP_ELFSYM_EXPORT(lv_spinbox_set_step),
ESP_ELFSYM_EXPORT(lv_spinbox_set_range),
ESP_ELFSYM_EXPORT(lv_spinbox_set_digit_format),
ESP_ELFSYM_EXPORT(lv_spinbox_set_digit_step_direction),
ESP_ELFSYM_EXPORT(lv_spinbox_set_value),
ESP_ELFSYM_EXPORT(lv_spinbox_set_cursor_pos),
ESP_ELFSYM_EXPORT(lv_spinbox_step_next),
ESP_ELFSYM_EXPORT(lv_spinbox_step_prev),
// delimiter
ESP_ELFSYM_END
};

View File

@ -4,12 +4,8 @@
*/
#pragma once
// Crashes
#define LOG_MESSAGE_ILLEGAL_STATE "Illegal state"
// Alloc
#define LOG_MESSAGE_ALLOC_FAILED "Out of memory"
#define LOG_MESSAGE_ALLOC_FAILED_FMT "Out of memory (failed to allocated %zu bytes)"
#define LOG_MESSAGE_ALLOC_FAILED "Memory allocation failed"
// Mutex
#define LOG_MESSAGE_MUTEX_LOCK_FAILED "Mutex acquisition timeout"