Refactor app loading and window management (#609)
This commit is contained in:
committed by
GitHub
parent
dc3f6104b8
commit
37c507544b
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user