Refactor app loading and window management (#609)

This commit is contained in:
Ken Van Hoeylandt
2026-08-11 23:40:59 +02:00
committed by GitHub
parent dc3f6104b8
commit 37c507544b
243 changed files with 16034 additions and 10865 deletions
@@ -1,22 +1,22 @@
#ifdef ESP_PLATFORM
#include <Tactility/service/development/DevelopmentService.h>
#include <app/install.h>
#include <app/manager.h>
#include <Tactility/app/App.h>
#include <Tactility/app/AppRegistration.h>
#include <tactility/log.h>
#include <Tactility/DeprecatedPaths.h>
#include <Tactility/StringUtils.h>
#include <Tactility/file/File.h>
#include <Tactility/network/HttpdReq.h>
#include <Tactility/network/Url.h>
#include <Tactility/Paths.h>
#include <Tactility/service/development/DevelopmentSettings.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/StringUtils.h>
#include <Tactility/service/development/DevelopmentService.h>
#include <Tactility/service/development/DevelopmentSettings.h>
#include <ranges>
#include <sstream>
#include <tactility/log.h>
namespace tt::service::development {
extern const ServiceManifest manifest;
@@ -101,12 +101,19 @@ 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);
char app_id[32];
AppInstanceId instance_id;
// Warning: possible app closure between getting app id and instance id
if (
app_manager_get_topmost_app_id(app_id, sizeof(app_id)) == ERROR_NONE &&
app_manager_get_topmost_instance_id(&instance_id) == ERROR_NONE
) {
if (strcmp(id_key_pos->second.c_str(), app_id) == 0) {
app_manager_stop(instance_id);
}
}
app::start(app_id);
app_manager_start(id_key_pos->second.c_str(), &instance_id);
LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str());
httpd_resp_send(request, nullptr, 0);
@@ -186,7 +193,7 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
LOG_W(TAG, "We have more bytes at the end of the request parsing?!");
}
if (!app::install(file_path)) {
if (app_install(file_path.c_str()) != ERROR_NONE) {
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to install");
return ESP_FAIL;
}
@@ -218,13 +225,13 @@ esp_err_t DevelopmentService::handleAppUninstall(httpd_req_t* request) {
return ESP_FAIL;
}
if (!app::findAppManifestById(id_key_pos->second)) {
if (!app_manager_find_manifest(id_key_pos->second.c_str())) {
LOG_I(TAG, "[200] /app/uninstall %s (app wasn't installed)", id_key_pos->second.c_str());
httpd_resp_send(request, nullptr, 0);
return ESP_OK;
}
if (app::uninstall(id_key_pos->second)) {
if (app_uninstall(id_key_pos->second.c_str()) == ERROR_NONE) {
LOG_I(TAG, "[200] /app/uninstall %s", id_key_pos->second.c_str());
httpd_resp_send(request, nullptr, 0);
return ESP_OK;
@@ -1,7 +1,7 @@
#ifdef ESP_PLATFORM
#include <Tactility/DeprecatedPaths.h>
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Paths.h>
#include <Tactility/service/development/DevelopmentSettings.h>
#include <map>
#include <string>
-375
View File
@@ -1,375 +0,0 @@
#include <Tactility/service/gui/GuiService.h>
#include "lvgl/devices/keyboard.h"
#include <Tactility/LogMessages.h>
#include <Tactility/Tactility.h>
#include <Tactility/app/AppInstance.h>
#include <Tactility/lvgl/Statusbar.h>
#include <Tactility/lvgl/UsbHidInput.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/loader/Loader.h>
#include <tactility/log.h>
#include <lvgl/lvgl.h>
#include <cstring>
namespace tt::service::gui {
extern const ServiceManifest manifest;
constexpr auto* TAG = "GuiService";
using namespace loader;
constexpr auto* GUI_TASK_NAME = "gui";
void warnIfRunningOnGuiTask(const char* context) {
const char* task_name = pcTaskGetName(nullptr);
if (strcmp(GUI_TASK_NAME, task_name) == 0) {
LOG_W(TAG, "%s shouldn't run on the GUI task", context);
}
}
namespace {
enum class GuiDispatchType { Show, Hide, Exit };
struct GuiDispatchItem {
GuiService* service;
GuiDispatchType type;
std::shared_ptr<app::AppInstance> appInstance; // only used for Show
};
} // namespace
// region AppManifest
void GuiService::onGuiDispatch(void* context) {
std::unique_ptr<GuiDispatchItem> item(static_cast<GuiDispatchItem*>(context));
switch (item->type) {
case GuiDispatchType::Show:
item->service->showApp(item->appInstance);
break;
case GuiDispatchType::Hide:
item->service->hideApp();
break;
case GuiDispatchType::Exit:
item->service->exitRequested = true;
break;
}
}
void GuiService::onLoaderEvent(LoaderService::Event event) {
GuiDispatchItem* item;
if (event == LoaderService::Event::ApplicationShowing) {
auto app_instance = std::static_pointer_cast<app::AppInstance>(app::getCurrentAppContext());
item = new GuiDispatchItem{this, GuiDispatchType::Show, app_instance};
} else if (event == LoaderService::Event::ApplicationHiding) {
// hideDoneSem is a binary semaphore signaled by every hideApp() completion,
// including the one showApp() triggers internally (GuiDispatchType::Show, when an
// app is already being shown) - that release has no waiter and leaves a stale
// permit sitting available. Drain it before dispatching, or the acquire() below
// could consume that leftover permit instead of the one this specific Hide
// dispatch is about to produce, letting Destroyed run before the real onHide()
// for this app has finished.
hideDoneSem.acquire(0);
item = new GuiDispatchItem{this, GuiDispatchType::Hide, nullptr};
} else {
return;
}
if (dispatcher_dispatch(dispatcher, item, onGuiDispatch) != ERROR_NONE) {
LOG_E(TAG, "Failed to dispatch gui event");
delete item;
return;
}
if (event == LoaderService::Event::ApplicationHiding) {
// Block here (still on the Loader thread, inside publish()'s synchronous
// subscriber call) until hideApp() has actually run to completion on the GUI
// task. LoaderService::transitionAppToState(Hiding) must not return - and
// therefore the Destroyed transition right after it, which unloads an ELF app's
// code, must not run - until App::onHide() has fully finished. Bounded so a stuck
// GUI task can't wedge app shutdown forever.
if (!hideDoneSem.acquire(pdMS_TO_TICKS(5000))) {
LOG_E(TAG, "Timed out waiting for hideApp() to complete");
}
}
}
int32_t GuiService::guiMain() {
auto service = findServiceById<GuiService>(manifest.id);
if (!lvgl_try_lock(5000)) {
LOG_E(TAG, "LVGL guiMain start failed as LVGL couldn't be locked");
return 0;
}
// The screen root is created in the main task instead of during onStart because
// it allows onStart() to succeed faster and allows widget creation to happen in the background
auto* screen_root = lv_screen_active();
if (screen_root == nullptr) {
LOG_E(TAG, "No display found, exiting GUI task");
lvgl_unlock();
return 0;
}
lv_obj_set_style_border_width(screen_root, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_all(screen_root, 0, LV_STATE_DEFAULT);
lv_obj_t* vertical_container = lv_obj_create(screen_root);
lv_obj_set_size(vertical_container, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(vertical_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_gap(vertical_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(vertical_container, lv_color_black(), LV_STATE_DEFAULT);
lv_obj_set_style_border_width(vertical_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_radius(vertical_container, 0, LV_STATE_DEFAULT);
service->statusbarWidget = lvgl::statusbar_create(vertical_container);
auto* app_container = lv_obj_create(vertical_container);
lv_obj_set_style_pad_all(app_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(app_container, 0, LV_STATE_DEFAULT);
lv_obj_set_width(app_container, LV_PCT(100));
lv_obj_set_flex_grow(app_container, 1);
lv_obj_set_flex_flow(app_container, LV_FLEX_FLOW_COLUMN);
service->appRootWidget = app_container;
lvgl_unlock();
while (!service->exitRequested) {
dispatcher_consume(service->dispatcher);
}
service->appRootWidget = nullptr;
service->statusbarWidget = nullptr;
return 0;
}
lv_obj_t* GuiService::createAppViews(lv_obj_t* parent) {
lv_obj_send_event(statusbarWidget, LV_EVENT_DRAW_MAIN, nullptr);
lv_obj_t* child_container = lv_obj_create(parent);
lv_obj_set_style_pad_all(child_container, 0, LV_STATE_DEFAULT);
lv_obj_set_width(child_container, LV_PCT(100));
lv_obj_set_style_border_width(child_container, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_grow(child_container, 1);
if (lvgl_software_keyboard_is_enabled()) {
lvgl_software_keyboard_construct(&software_keyboard, parent);
} else {
software_keyboard = {
nullptr
};
}
return child_container;
}
void GuiService::redraw() {
// Lock GUI and LVGL
lock();
if (appRootWidget == nullptr) {
LOG_W(TAG, "No root widget");
unlock();
return;
}
bool lvgl_locked = false;
while (lvgl_is_running() && !(lvgl_locked = lvgl_try_lock(1000))) {
LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "GuiService LVGL");
}
if (!lvgl_locked) {
unlock();
return;
}
if (!lvgl_is_running()) {
lvgl_unlock();
unlock();
return;
}
lv_obj_clean(appRootWidget);
if (appToRender != nullptr) {
// Create a default group which adds all objects automatically,
// and assign all indevs to it.
// This enables navigation with limited input, such as encoder wheels.
// The previous default group (if any) is no longer referenced by anything
// after lv_obj_clean() above, so it must be freed here or it leaks.
auto* previous_group = lv_group_get_default();
if (previous_group != nullptr) {
lv_group_delete(previous_group);
}
lv_group_t* group = lv_group_create();
auto* indev = lv_indev_get_next(nullptr);
while (indev) {
lv_indev_set_group(indev, group);
indev = lv_indev_get_next(indev);
}
lv_group_set_default(group);
app::Flags flags = std::static_pointer_cast<app::AppInstance>(appToRender)->getFlags();
if (flags.hideStatusbar) {
lv_obj_add_flag(statusbarWidget, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_remove_flag(statusbarWidget, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t* container = createAppViews(appRootWidget);
appToRender->getApp()->onShow(*appToRender, container);
} else {
LOG_W(TAG, "Nothing to draw");
}
lvgl_unlock();
unlock();
}
bool GuiService::onStart(ServiceContext& service) {
exitRequested = false;
dispatcher = dispatcher_alloc();
thread = new Thread(
GUI_TASK_NAME,
4096, // Last known minimum was 2800 for launching desktop
guiMain
);
thread->setPriority(THREAD_PRIORITY_SERVICE);
const auto loader = findLoaderService();
assert(loader != nullptr);
loader_pubsub_subscription = loader->getPubsub()->subscribe([this](auto event) {
onLoaderEvent(event);
});
isStarted = true;
lvgl::startUsbHidInput();
thread->start();
return true;
}
void GuiService::onStop(ServiceContext& service) {
lvgl::stopUsbHidInput();
lock();
const auto loader = findLoaderService();
assert(loader != nullptr);
loader->getPubsub()->unsubscribe(loader_pubsub_subscription);
appToRender = nullptr;
isStarted = false;
unlock();
auto* exit_item = new GuiDispatchItem{this, GuiDispatchType::Exit, nullptr};
if (dispatcher_dispatch(dispatcher, exit_item, onGuiDispatch) != ERROR_NONE) {
LOG_E(TAG, "Failed to dispatch gui exit event");
check(false, "Failed to dispatch exit signal to thread.");
delete exit_item;
}
thread->join();
lvgl_lock();
if (software_keyboard.object != nullptr) {
lvgl_software_keyboard_destruct(&software_keyboard);
}
auto* default_group = lv_group_get_default();
if (default_group != nullptr) {
lv_group_delete(default_group);
lv_group_set_default(nullptr);
}
auto* screen_root = lv_screen_active();
if (screen_root != nullptr) {
lv_obj_clean(screen_root);
}
lvgl_unlock();
delete thread;
dispatcher_free(dispatcher);
dispatcher = nullptr;
}
void GuiService::showApp(std::shared_ptr<app::AppInstance> app) {
auto lock = mutex.asScopedLock();
lock.lock();
if (!isStarted) {
LOG_E(TAG, "Failed to show app %s: GUI not started", app->getManifest().appId.c_str());
return;
}
if (appToRender != nullptr && appToRender->getLaunchId() == app->getLaunchId()) {
LOG_W(TAG, "Already showing %s", app->getManifest().appId.c_str());
return;
}
LOG_I(TAG, "Showing %s", app->getManifest().appId.c_str());
// Ensure previous app triggers onHide() logic
if (appToRender != nullptr) {
hideApp();
}
appToRender = std::move(app);
redraw();
}
void GuiService::hideApp() {
// Signals hideDoneSem on every return path (including the early-return guards below) -
// onLoaderEvent() blocks on this to know App::onHide() has actually finished before
// Loader proceeds to destroy the app (see hideDoneSem's declaration for why).
struct SignalOnExit {
Semaphore& sem;
~SignalOnExit() { sem.release(); }
} signal_on_exit { hideDoneSem };
auto lock = mutex.asScopedLock();
lock.lock();
if (!isStarted) {
LOG_E(TAG, "Failed to hide app: GUI not started");
return;
}
if (appToRender == nullptr) {
LOG_W(TAG, "hideApp() called but no app is currently shown");
return;
}
// 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();
appToRender->getApp()->onHide(*appToRender);
lvgl_unlock();
appToRender = nullptr;
}
std::shared_ptr<GuiService> findService() {
return std::static_pointer_cast<GuiService>(
findServiceById(manifest.id)
);
}
extern const ServiceManifest manifest = {
.id = "Gui",
.createService = create<GuiService>
};
// endregion
} // namespace
-328
View File
@@ -1,328 +0,0 @@
#include <Tactility/service/loader/Loader.h>
#include <Tactility/app/AppInstance.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/LogMessages.h>
#include <Tactility/DispatcherThread.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <vector>
#include <tactility/log.h>
#include <tactility/memory.h>
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) {
switch (state) {
using enum app::State;
case Initial:
return "initial";
case Created:
return "started";
case Showing:
return "showing";
case Hiding:
return "hiding";
case Destroyed:
return "stopped";
default:
return "?";
}
}
void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters) {
LOG_I(TAG, "Start by id %s", id.c_str());
auto app_manifest = app::findAppManifestById(id);
if (app_manifest == nullptr) {
LOG_E(TAG, "App not found: %s", id.c_str());
return;
}
auto lock = mutex.asScopedLock();
if (!lock.lock(LOADER_TIMEOUT)) {
LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
return;
}
auto previous_app = !appStack.empty() ? appStack[appStack.size() - 1]: nullptr;
auto new_app = std::make_shared<app::AppInstance>(app_manifest, launchId, parameters);
new_app->mutableFlags().hideStatusbar = (app_manifest->appFlags & app::AppManifest::Flags::HideStatusBar);
// 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);
memory_print_stats();
}
void LoaderService::onStopTopAppMessage(const std::string& id) {
auto lock = mutex.asScopedLock();
if (!lock.lock(LOADER_TIMEOUT)) {
LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
return;
}
size_t original_stack_size = appStack.size();
if (original_stack_size == 0) {
LOG_E(TAG, "Stop app: no app running");
return;
}
// Stop current app
auto app_to_stop = appStack[appStack.size() - 1];
if (app_to_stop->getManifest().appId != id) {
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());
return;
}
if (original_stack_size == 1 && app_to_stop->getManifest().appName != "Boot") {
LOG_E(TAG, "Stop app: can't stop root app");
return;
}
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;
}
auto app_to_stop_launch_id = app_to_stop->getLaunchId();
transitionAppToState(app_to_stop, app::State::Hiding);
transitionAppToState(app_to_stop, app::State::Destroyed);
appStack.pop_back();
// We only expect the app to be referenced within the current scope
if (app_to_stop.use_count() > 1) {
LOG_W(TAG, "Memory leak: Stopped %s, but use count is %d", app_to_stop->getManifest().appId.c_str(), (int)(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) {
LOG_W(TAG, "Memory leak: Stopped %s, but use count is %d", app_to_stop->getManifest().appId.c_str(), (int)(app_to_stop->getApp().use_count() - 2));
}
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];
assert(instance_to_resume);
transitionAppToState(instance_to_resume, app::State::Showing);
}
// Unlock so that we can send results to app and they can also start/stop new apps while processing these results
lock.unlock();
// WARNING: After this point we cannot change the app states from this method directly anymore as we don't have a lock!
if (instance_to_resume != nullptr) {
if (result_set) {
if (result_bundle != nullptr) {
instance_to_resume->getApp()->onResult(
*instance_to_resume,
app_to_stop_launch_id,
result,
std::move(result_bundle)
);
} else {
instance_to_resume->getApp()->onResult(
*instance_to_resume,
app_to_stop_launch_id,
result,
nullptr
);
}
} else {
instance_to_resume->getApp()->onResult(
*instance_to_resume,
app_to_stop_launch_id,
app::Result::Cancelled,
nullptr
);
}
}
memory_print_stats();
}
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)) {
LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
return;
}
if (!isRunning(id)) {
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) {
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) {
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();
LOG_I(TAG, "App \"%s\" state: %s -> %s",
app_manifest.appId.c_str(),
appStateToString(old_state),
appStateToString(state)
);
switch (state) {
using enum app::State;
case Initial:
check(false, LOG_MESSAGE_ILLEGAL_STATE);
case Created:
assert(app->getState() == app::State::Initial);
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);
break;
}
case Hiding: {
assert(app->getState() == app::State::Showing);
pubsubExternal->publish(Event::ApplicationHiding);
break;
}
case Destroyed:
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++;
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) {
LOG_I(TAG, "dispatching stopTop(%s)", id.c_str());
dispatcherThread->dispatch([this, id] {
onStopTopAppMessage(id);
});
}
void LoaderService::stopAll(const std::string& id) {
LOG_I(TAG, "dispatching stopAll(%s)", id.c_str());
dispatcherThread->dispatch([this, id] {
onStopAllAppMessage(id);
});
}
std::shared_ptr<app::AppContext> LoaderService::getCurrentAppContext() {
const auto lock = mutex.asScopedLock();
lock.lock();
if (appStack.empty()) {
return nullptr;
} else {
return appStack[appStack.size() - 1];
}
}
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;
}
std::shared_ptr<LoaderService> findLoaderService() {
return service::findServiceById<LoaderService>(manifest.id);
}
extern const ServiceManifest manifest = {
.id = "Loader",
.createService = create<LoaderService>
};
} // namespace
@@ -120,7 +120,7 @@ bool RtcTimeService::onStart(ServiceContext& serviceContext) {
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
}
if (system_event_subscribe(KERNEL_EVENT_TIME_CHANGED, &RtcTimeService::onTimeChangedTrampoline, this) == ERROR_NONE) {
if (system_event_callback_add(KERNEL_EVENT_TIME_CHANGED, &RtcTimeService::onTimeChangedTrampoline, this) == ERROR_NONE) {
timeEventSubscribed = true;
}
@@ -129,7 +129,7 @@ bool RtcTimeService::onStart(ServiceContext& serviceContext) {
void RtcTimeService::onStop(ServiceContext& serviceContext) {
if (timeEventSubscribed) {
system_event_unsubscribe(KERNEL_EVENT_TIME_CHANGED, &RtcTimeService::onTimeChangedTrampoline);
system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, &RtcTimeService::onTimeChangedTrampoline);
timeEventSubscribed = false;
}
@@ -5,9 +5,10 @@
#include <Tactility/LogMessages.h>
#include <Tactility/CpuAffinity.h>
#include <Tactility/TactilityCore.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/service/screenshot/ScreenshotTask.h>
#include <app/manager.h>
#include <tactility/delay.h>
#include <tactility/log.h>
@@ -66,7 +67,7 @@ static void makeScreenshot(const std::string& filename) {
void ScreenshotTask::taskMain() {
uint8_t screenshots_taken = 0;
std::string last_app_id;
uint32_t last_app_instance_id = 0;
while (!isInterrupted()) {
if (work.type == TASK_WORK_TYPE_DELAY) {
@@ -85,15 +86,13 @@ void ScreenshotTask::taskMain() {
}
}
} else if (work.type == TASK_WORK_TYPE_APPS) {
auto appContext = app::getCurrentAppContext();
if (appContext != nullptr) {
const app::AppManifest& manifest = appContext->getManifest();
if (manifest.appId != last_app_id) {
delay_millis(100);
last_app_id = manifest.appId;
auto filename = std::format("{}/screenshot-{}.png", work.path, manifest.appId);
makeScreenshot(filename);
}
AppInstanceId app_instance_id = 0;
bool has_topmost = app_manager_get_topmost_instance_id(&app_instance_id) == ERROR_NONE;
if (has_topmost && app_instance_id != last_app_instance_id) {
delay_millis(100);
last_app_instance_id = app_instance_id;
auto filename = std::format("{}/screenshot-{}.png", work.path, app_instance_id);
makeScreenshot(filename);
}
// Ensure the LVGL widgets are rendered as the app just started
delay_millis(250);
@@ -8,19 +8,15 @@
#include <Tactility/lvgl/Statusbar.h>
#include <Tactility/Mutex.h>
#include <tactility/check.h>
#include <Tactility/DeprecatedPaths.h>
#include <Tactility/StringUtils.h>
#include <Tactility/TactilityConfig.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/App.h>
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/network/HttpdReq.h>
#include <Tactility/network/Url.h>
#include <Tactility/Paths.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/StringUtils.h>
#include <Tactility/service/wifi/Wifi.h>
#include <tactility/check.h>
#include <tactility/filesystem/file_system.h>
#include <tactility/log.h>
@@ -31,6 +27,10 @@
#include <lv_screenshot.h>
#endif
#include "app/install.h"
#include "app/manager.h"
#include <atomic>
#include <cctype>
#include <cerrno>
@@ -41,8 +41,8 @@
#include <esp_netif.h>
#include <esp_system.h>
#include <esp_vfs_fat.h>
#include <esp_wifi_default.h>
#include <esp_wifi.h>
#include <esp_wifi_default.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <iomanip>
@@ -50,6 +50,7 @@
#include <mbedtls/base64.h>
#include <ranges>
#include <sstream>
#include <vector>
namespace tt::service::webserver {
@@ -1215,32 +1216,30 @@ esp_err_t WebServerService::handleApiSysinfo(httpd_req_t* request) {
esp_err_t WebServerService::handleApiApps(httpd_req_t* request) {
LOG_I(TAG, "GET /api/apps");
auto manifests = app::getAppManifests();
std::vector<const ::AppManifest*> manifests;
app_manager_for_each_manifest([](const ::AppManifest* manifest, void* context) {
static_cast<std::vector<const ::AppManifest*>*>(context)->push_back(manifest);
}, &manifests);
std::ostringstream json;
json << "{\"apps\":[";
bool first = true;
for (const auto& manifest : manifests) {
for (const auto* manifest : manifests) {
if (!first) json << ",";
first = false;
json << "{";
json << "\"id\":\"" << escapeJson(manifest->appId) << "\",";
json << "\"name\":\"" << escapeJson(manifest->appName) << "\",";
json << "\"version\":\"" << escapeJson(manifest->appVersionName) << "\",";
json << "\"id\":\"" << escapeJson(manifest->id) << "\",";
json << "\"name\":\"" << escapeJson(manifest->name) << "\",";
const char* category = "user";
if (manifest->appCategory == app::Category::System) category = "system";
else if (manifest->appCategory == app::Category::Settings) category = "settings";
if (manifest->category == APP_CATEGORY_SYSTEM) category = "system";
else if (manifest->category == APP_CATEGORY_SETTINGS) category = "settings";
json << "\"category\":\"" << category << "\",";
json << "\"isExternal\":" << (manifest->appLocation.isExternal() ? "true" : "false") << ",";
json << "\"hidden\":" << ((manifest->appFlags & app::AppManifest::Flags::Hidden) ? "true" : "false");
if (!manifest->appIcon.empty()) {
json << ",\"icon\":\"" << escapeJson(manifest->appIcon) << "\"";
}
json << "\"isExternal\":" << (manifest->location.type == APP_LOCATION_PATH ? "true" : "false") << ",";
json << "\"hidden\":" << ((manifest->flags & APP_MANIFEST_FLAG_HIDDEN) ? "true" : "false");
json << "}";
}
@@ -1262,18 +1261,16 @@ esp_err_t WebServerService::handleApiAppsRun(httpd_req_t* request) {
return ESP_FAIL;
}
auto manifest = app::findAppManifestById(appId);
if (!manifest) {
auto* manifest = app_manager_find_manifest(appId.c_str());
if (manifest == nullptr) {
httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "app not found");
return ESP_FAIL;
}
// Stop if already running
if (app::isRunning(appId)) {
app::stopAll(appId);
}
app::start(appId);
// Every app instance gets its own task now, so there's no "stop the existing one first" -
// this just starts a fresh instance alongside whatever's already running.
AppInstanceId instance_id = 0;
app_manager_start(appId.c_str(), &instance_id);
LOG_I(TAG, "[200] /api/apps/run %s", appId.c_str());
httpd_resp_sendstr(request, "ok");
@@ -1290,20 +1287,20 @@ esp_err_t WebServerService::handleApiAppsUninstall(httpd_req_t* request) {
return ESP_FAIL;
}
auto manifest = app::findAppManifestById(appId);
if (!manifest) {
auto* manifest = app_manager_find_manifest(appId.c_str());
if (manifest == nullptr) {
LOG_I(TAG, "[200] /api/apps/uninstall %s (app wasn't installed)", appId.c_str());
httpd_resp_sendstr(request, "ok");
return ESP_OK;
}
// Only allow uninstalling external apps
if (manifest->appLocation.isInternal()) {
// Only allow uninstalling external (side-loaded) apps
if (manifest->location.type != APP_LOCATION_PATH) {
httpd_resp_send_err(request, HTTPD_403_FORBIDDEN, "cannot uninstall system apps");
return ESP_FAIL;
}
if (app::uninstall(appId)) {
if (app_uninstall(appId.c_str()) == ERROR_NONE) {
LOG_I(TAG, "[200] /api/apps/uninstall %s", appId.c_str());
httpd_resp_sendstr(request, "ok");
return ESP_OK;
@@ -1393,7 +1390,7 @@ esp_err_t WebServerService::handleApiAppsInstall(httpd_req_t* request) {
}
// Install the app
if (!app::install(file_path)) {
if (app_install(file_path.c_str()) != ERROR_NONE) {
file::deleteFile(file_path);
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "installation failed");
return ESP_FAIL;
+2 -2
View File
@@ -510,7 +510,7 @@ public:
LOG_W(TAG, "No WiFi device found");
}
if (system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted, nullptr) == ERROR_NONE) {
if (system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted, nullptr) == ERROR_NONE) {
state.bootEventSubscribed = true;
}
@@ -531,7 +531,7 @@ public:
state.autoConnectTimer = nullptr; // Must release as it holds a reference via its callback.
if (state.bootEventSubscribed) {
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted);
state.bootEventSubscribed = false;
}
@@ -138,20 +138,24 @@ bool contains(const std::string& ssid) {
bool load(const std::string& ssid, WifiApSettings& apSettings) {
auto service_context = findServiceContext();
if (service_context == nullptr) {
LOG_E(TAG, "No service context");
return false;
}
const auto file_path = getApPropertiesFilePath(service_context->getPaths(), ssid);
if (!file::isFile(file_path)) {
LOG_E(TAG, "Not a file: %s", file_path.c_str());
return false;
}
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(file_path, map)) {
LOG_E(TAG, "Failed to load properties from %s", file_path.c_str());
return false;
}
// SSID is required
if (!map.contains(AP_PROPERTIES_KEY_SSID)) {
LOG_E(TAG, "File does not contain SSID: %s", file_path.c_str());
return false;
}
@@ -166,6 +170,7 @@ bool load(const std::string& ssid, WifiApSettings& apSettings) {
} else if (decrypt(ssid, encrypted_password, password_decrypted)) {
apSettings.password = password_decrypted;
} else {
LOG_E(TAG, "Failed to decrypt password from %s", file_path.c_str());
return false;
}
} else {
@@ -9,7 +9,7 @@
#include <Tactility/file/File.h>
#include <Tactility/service/wifi/WifiApSettings.h>
#include <Tactility/Paths.h>
#include <Tactility/DeprecatedPaths.h>
#include <Tactility/Tactility.h>
#include <tactility/log.h>