From 9299df004fa5bd8a990bf656b74b4340577d800a Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Tue, 18 Aug 2026 21:47:48 +0200 Subject: [PATCH 01/13] Move driver listing to Module definition --- Documentation/ideas.md | 3 - Platforms/platform-esp32/source/module.cpp | 130 ++++++--------------- Platforms/platform-posix/source/module.cpp | 18 +-- 3 files changed, 38 insertions(+), 113 deletions(-) diff --git a/Documentation/ideas.md b/Documentation/ideas.md index c1c302c05..5bee0f5d3 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -46,14 +46,11 @@ ## Medium Priority -- `platform-esp32`'s module drivers are declared in start/stop of the module but they should be set via `Module::drivers` - `struct Driver` has an `.owner`, but it's not always set. Either validate on Module construct that it matches, or otherwise set it during module start. The problem: NULL parent currently means that driver is not removable. This clashes with setting it dynamically. Consider some kind of flag to determine removability. - Consider moving certain drivers into separate modules: audio, bt, wifi, etc - Consider using https://github.com/Graphify-Labs/graphify - Consider implementing LVGL gridnav in apps https://lvgl.io/docs/open/9.3/details/auxiliary-modules/gridnav.html - Make USB host driver disabled by default, so it doesn't consume memory -- Filtering for apps in App Hub: - - apps that only work on a specific device - Diceware app has large "+" and "-' buttons on Cardputer. It should be smaller. - TactilityTool: Make API compatibility table (and check for compatibility in the tool itself) - Improve EspLcdDisplay to contain all the standard configuration options, and implement a default init function. Add a configuration class. diff --git a/Platforms/platform-esp32/source/module.cpp b/Platforms/platform-esp32/source/module.cpp index 8e2b492d3..750931ab6 100644 --- a/Platforms/platform-esp32/source/module.cpp +++ b/Platforms/platform-esp32/source/module.cpp @@ -2,9 +2,6 @@ #include #endif -#include -#include -#include #include #include @@ -60,122 +57,61 @@ extern Driver esp32_usb_midi_device_driver; extern Driver esp32_usb_cdc_device_driver; #endif -static error_t start() { - /* We crash when construct fails, because if a single driver fails to construct, - * there is no guarantee that the previously constructed drivers can be destroyed */ - check(driver_construct_add(&esp32_adc_oneshot_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_gpio_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_i2c_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_i2c_master_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_i2s_driver) == ERROR_NONE); +static Driver* const platform_esp32_drivers[] = { + &esp32_adc_oneshot_driver, + &esp32_gpio_driver, + &esp32_i2c_driver, + &esp32_i2c_master_driver, + &esp32_i2s_driver, #if SOC_LCD_I80_SUPPORTED - check(driver_construct_add(&esp32_i8080_driver) == ERROR_NONE); + &esp32_i8080_driver, #endif - check(driver_construct_add(&esp32_pwm_ledc_driver) == ERROR_NONE); + &esp32_pwm_ledc_driver, #if SOC_SDMMC_HOST_SUPPORTED - check(driver_construct_add(&esp32_sdmmc_driver) == ERROR_NONE); + &esp32_sdmmc_driver, #endif - check(driver_construct_add(&esp32_sdspi_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_spi_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_uart_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_grove_driver) == ERROR_NONE); + &esp32_sdspi_driver, + &esp32_spi_driver, + &esp32_uart_driver, + &esp32_grove_driver, #if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED) - check(driver_construct_add(&esp32_wifi_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_wifi_pinned_driver) == ERROR_NONE); + &esp32_wifi_driver, + &esp32_wifi_pinned_driver, #endif #if defined(CONFIG_BT_NIMBLE_ENABLED) - check(driver_construct_add(&esp32_bluetooth_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_ble_serial_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_ble_midi_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_ble_hid_device_driver) == ERROR_NONE); + &esp32_bluetooth_driver, + &esp32_ble_serial_driver, + &esp32_ble_midi_driver, + &esp32_ble_hid_device_driver, #endif #if SOC_USB_OTG_SUPPORTED - check(driver_construct_add(&esp32_usbhost_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_usbhost_hid_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_usbhost_hid_keyboard_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_usbhost_midi_driver) == ERROR_NONE); - check(driver_construct_add(&esp32_usbhost_msc_driver) == ERROR_NONE); -#endif - // usbdevice0 and its children (usbdevicehid0, usbdevicemsc0, ...) are declared per-board in - // .dts, same pattern as usbhost0 above - the devicetree compiler constructs/adds/starts their - // Device instances and wires parent/child relationships. Only driver registration happens - // here. + &esp32_usbhost_driver, + &esp32_usbhost_hid_driver, + &esp32_usbhost_hid_keyboard_driver, + &esp32_usbhost_midi_driver, + &esp32_usbhost_msc_driver, +#endif #if SOC_USB_OTG_SUPPORTED && (CONFIG_TINYUSB_HID_COUNT || CONFIG_TINYUSB_MSC_ENABLED || CONFIG_TINYUSB_MIDI_COUNT || CONFIG_TINYUSB_CDC_ENABLED) - check(driver_construct_add(&esp32_usb_device_controller_driver) == ERROR_NONE); + &esp32_usb_device_controller_driver, #endif #if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT - check(driver_construct_add(&esp32_usb_hid_device_driver) == ERROR_NONE); + &esp32_usb_hid_device_driver, #endif #if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED - check(driver_construct_add(&esp32_usb_msc_device_driver) == ERROR_NONE); + &esp32_usb_msc_device_driver, #endif #if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT - check(driver_construct_add(&esp32_usb_midi_device_driver) == ERROR_NONE); -#endif -#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED - check(driver_construct_add(&esp32_usb_cdc_device_driver) == ERROR_NONE); -#endif - return ERROR_NONE; -} - -static error_t stop() { - /* We crash when destruct fails, because if a single driver fails to destruct, - * there is no guarantee that the previously destroyed drivers can be recovered */ -#if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED) - check(driver_remove_destruct(&esp32_wifi_pinned_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_wifi_driver) == ERROR_NONE); + &esp32_usb_midi_device_driver, #endif #if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED - check(driver_remove_destruct(&esp32_usb_cdc_device_driver) == ERROR_NONE); -#endif -#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT - check(driver_remove_destruct(&esp32_usb_midi_device_driver) == ERROR_NONE); -#endif -#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED - check(driver_remove_destruct(&esp32_usb_msc_device_driver) == ERROR_NONE); -#endif -#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT - check(driver_remove_destruct(&esp32_usb_hid_device_driver) == ERROR_NONE); -#endif -#if SOC_USB_OTG_SUPPORTED && (CONFIG_TINYUSB_HID_COUNT || CONFIG_TINYUSB_MSC_ENABLED || CONFIG_TINYUSB_MIDI_COUNT || CONFIG_TINYUSB_CDC_ENABLED) - check(driver_remove_destruct(&esp32_usb_device_controller_driver) == ERROR_NONE); -#endif -#if SOC_USB_OTG_SUPPORTED - check(driver_remove_destruct(&esp32_usbhost_msc_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_usbhost_midi_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_usbhost_hid_keyboard_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_usbhost_hid_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_usbhost_driver) == ERROR_NONE); -#endif -#if defined(CONFIG_BT_NIMBLE_ENABLED) - check(driver_remove_destruct(&esp32_ble_hid_device_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_ble_midi_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_ble_serial_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_bluetooth_driver) == ERROR_NONE); -#endif - check(driver_remove_destruct(&esp32_adc_oneshot_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_gpio_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_i2c_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_i2c_master_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_i2s_driver) == ERROR_NONE); -#if SOC_LCD_I80_SUPPORTED - check(driver_remove_destruct(&esp32_i8080_driver) == ERROR_NONE); -#endif - check(driver_remove_destruct(&esp32_pwm_ledc_driver) == ERROR_NONE); -#if SOC_SDMMC_HOST_SUPPORTED - check(driver_remove_destruct(&esp32_sdmmc_driver) == ERROR_NONE); + &esp32_usb_cdc_device_driver, #endif - check(driver_remove_destruct(&esp32_sdspi_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_spi_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_uart_driver) == ERROR_NONE); - check(driver_remove_destruct(&esp32_grove_driver) == ERROR_NONE); - return ERROR_NONE; -} + nullptr +}; Module platform_esp32_module = { .name = "platform-esp32", - .start = start, - .stop = stop, + .drivers = platform_esp32_drivers, .symbols = nullptr, .internal = nullptr }; diff --git a/Platforms/platform-posix/source/module.cpp b/Platforms/platform-posix/source/module.cpp index 5c83f7787..9d9b8d0e0 100644 --- a/Platforms/platform-posix/source/module.cpp +++ b/Platforms/platform-posix/source/module.cpp @@ -1,26 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include #include extern "C" { extern Driver posix_wifi_driver; -static error_t start() { - check(driver_construct_add(&posix_wifi_driver) == ERROR_NONE); - return ERROR_NONE; -} - -static error_t stop() { - check(driver_remove_destruct(&posix_wifi_driver) == ERROR_NONE); - return ERROR_NONE; -} +static Driver* const platform_posix_drivers[] = { + &posix_wifi_driver, + nullptr +}; struct Module platform_posix_module = { .name = "platform-posix", - .start = start, - .stop = stop, + .drivers = platform_posix_drivers, .symbols = nullptr, .internal = nullptr }; From bede5f2aef61103d646b929e387c8ab5e4c19917 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Tue, 18 Aug 2026 22:13:04 +0200 Subject: [PATCH 02/13] Delete tt_time from TactilityKernel --- TactilityC/Include/tt_time.h | 42 ----------------------------------- TactilityC/Source/tt_init.cpp | 6 ----- TactilityC/Source/tt_time.cpp | 42 ----------------------------------- 3 files changed, 90 deletions(-) delete mode 100644 TactilityC/Include/tt_time.h delete mode 100644 TactilityC/Source/tt_time.cpp diff --git a/TactilityC/Include/tt_time.h b/TactilityC/Include/tt_time.h deleted file mode 100644 index db9f972d3..000000000 --- a/TactilityC/Include/tt_time.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include - -#define TT_TIMEZONE_NAME_BUFFER_LENGTH 32 -#define TT_TIMEZONE_CODE_BUFFER_LENGTH 48 - -/** - * Set the timezone - * @param[in] name human-readable name - * @param[in] code the technical code (from timezones.csv) - */ -void tt_timezone_set(const char* name, const char* code); - -/** - * Get the name of the timezone - * @param[out] buffer the output buffer which will include a null terminator (should be TT_TIMEZONE_NAME_BUFFER_LENGTH) - * @param[in] bufferSize the size of the output buffer - */ -bool tt_timezone_get_name(char* buffer, size_t bufferSize); - -/** - * Get the code of the timezone (see timezones.csv) - */ -bool tt_timezone_get_code(char* buffer, size_t bufferSize); - -/** @return true when clocks should be shown as a 24 hours one instead of 12 hours */ -bool tt_timezone_is_format_24_hour(); - -/** Set whether clocks should be shown as a 24 hours instead of 12 hours - * @param[in] show24Hour - */ -void tt_timezone_set_format_24_hour(bool show24Hour); - -#ifdef __cplusplus -} -#endif diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index 3a1007d57..533d057ee 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -3,7 +3,6 @@ #include "tt_app_alertdialog.h" #include "tt_app_fileselection.h" #include "tt_app_selectiondialog.h" -#include "tt_time.h" #include "symbols/cplusplus.h" #include "symbols/esp_event.h" @@ -276,11 +275,6 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(tt_app_fileselection_get_result_path), ESP_ELFSYM_EXPORT(tt_app_selectiondialog_start), ESP_ELFSYM_EXPORT(tt_app_alertdialog_start), - ESP_ELFSYM_EXPORT(tt_timezone_set), - ESP_ELFSYM_EXPORT(tt_timezone_get_name), - ESP_ELFSYM_EXPORT(tt_timezone_get_code), - ESP_ELFSYM_EXPORT(tt_timezone_is_format_24_hour), - ESP_ELFSYM_EXPORT(tt_timezone_set_format_24_hour), // stdio.h ESP_ELFSYM_EXPORT(rename), diff --git a/TactilityC/Source/tt_time.cpp b/TactilityC/Source/tt_time.cpp deleted file mode 100644 index ee97242be..000000000 --- a/TactilityC/Source/tt_time.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "tt_time.h" - -#include -#include - -using namespace tt; - -extern "C" { - -void tt_timezone_set(const char* name, const char* code) { - settings::setTimeZone(name, code); -} - -bool tt_timezone_get_name(char* buffer, size_t bufferSize) { - auto name = settings::getTimeZoneName(); - if (bufferSize < (name.length() + 1)) { - return false; - } else { - strcpy(buffer, name.c_str()); - return true; - } -} - -bool tt_timezone_get_code(char* buffer, size_t bufferSize) { - auto code = settings::getTimeZoneCode(); - if (bufferSize < (code.length() + 1)) { - return false; - } else { - strcpy(buffer, code.c_str()); - return true; - } -} - -bool tt_timezone_is_format_24_hour() { - return settings::isTimeFormat24Hour(); -} - -void tt_timezone_set_format_24_hour(bool show24Hour) { - return settings::setTimeFormat24Hour(show24Hour); -} - -} From 3275629e9a167469f555a15125b15d4926560b86 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 20:16:20 +0200 Subject: [PATCH 03/13] Use AppInstanceId instead of int --- Modules/app-module/include/app/event.h | 6 ++++-- Modules/app-module/source/event.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Modules/app-module/include/app/event.h b/Modules/app-module/include/app/event.h index 090d0ffc3..e5af8f119 100644 --- a/Modules/app-module/include/app/event.h +++ b/Modules/app-module/include/app/event.h @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include "instance.h" + #include #include @@ -54,7 +56,7 @@ struct AppEvent { */ struct AppEventSubscription { /** The app instance this subscription receives events for; set by the caller before app_event_subscribe(). */ - uint32_t app_instance_id; + AppInstanceId app_instance_id; TaskHandle_t task; @@ -89,7 +91,7 @@ error_t app_event_unsubscribe(struct AppEventSubscription* sub); * @retval ERROR_RESOURCE at least one matching subscription's queue was full; the event was * dropped for that subscription (still delivered to any other matching subscription) */ -error_t app_event_emit(uint32_t app_instance_id, const struct AppEvent* event); +error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event); /** * Pop the next event for @a sub, blocking up to @a timeout if the queue is currently empty. diff --git a/Modules/app-module/source/event.cpp b/Modules/app-module/source/event.cpp index 875fa2459..da24885c0 100644 --- a/Modules/app-module/source/event.cpp +++ b/Modules/app-module/source/event.cpp @@ -50,7 +50,7 @@ error_t app_event_unsubscribe(AppEventSubscription* sub) { return result; } -error_t app_event_emit(uint32_t app_instance_id, const AppEvent* event) { +error_t app_event_emit(AppInstanceId app_instance_id, const AppEvent* event) { AppEvent stamped_event = *event; stamped_event.timestamp = get_micros_since_boot(); From 222bd2adf048adff881f0049509a6107b4f1eb42 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 20:16:40 +0200 Subject: [PATCH 04/13] Update ideas.md for subscription changes --- Documentation/ideas.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 5bee0f5d3..75151dbd9 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -11,6 +11,7 @@ ## Higher Priority +- AppEventSubscription, SystemEventSubscription: should use TaskHandle_t notification. That way, there can be a single wait event for a task instead of X separate ones with each their timeout. - AppHubApp: Prevent download callbacks from accessing a destroyed view. - Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp - wifi: wifi_add_event_callback() and wifi_remove_event_callback() should be replaced by a subscribe/await pattern like system events. From b93acb1f99a8340539e190bfbfe445fa9992c55a Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 21:45:40 +0200 Subject: [PATCH 05/13] Fix for app uninstall --- Modules/app-module/include/app/manager.h | 8 +++ .../app-module/private/app/private/app_fs.h | 45 ++++++++++++ Modules/app-module/source/app_install.cpp | 72 +++++-------------- Modules/app-module/source/app_paths.cpp | 13 +++- Modules/app-module/source/manager.cpp | 26 +++++++ Modules/app-module/source/symbols.cpp | 1 + .../app/apphubdetails/AppHubDetailsApp.cpp | 20 ++---- 7 files changed, 115 insertions(+), 70 deletions(-) diff --git a/Modules/app-module/include/app/manager.h b/Modules/app-module/include/app/manager.h index f535282a8..2f2c1d76a 100644 --- a/Modules/app-module/include/app/manager.h +++ b/Modules/app-module/include/app/manager.h @@ -152,6 +152,14 @@ error_t app_manager_install_path_add(const char* path); */ void app_manager_install_path_scan(void); +/** + * Uninstalls an app that was registered via app_manager_install_path_scan() (i.e. discovered on + * disk, not installed via app_install()). Stops running instances, removes the manifest + * registration, and deletes the app directory. Returns ERROR_NOT_FOUND if the app id is not in + * the scan registry. + */ +error_t app_manager_install_path_uninstall(const char* app_id); + #ifdef __cplusplus } #endif diff --git a/Modules/app-module/private/app/private/app_fs.h b/Modules/app-module/private/app/private/app_fs.h index d4dff2a14..1c04faa00 100644 --- a/Modules/app-module/private/app/private/app_fs.h +++ b/Modules/app-module/private/app/private/app_fs.h @@ -12,6 +12,7 @@ #include #include #include +#include #include inline bool app_fs_is_directory(const std::string& path) { @@ -36,6 +37,50 @@ inline bool app_fs_is_file(const std::string& path) { // Appends the full path of every direct subdirectory of @a path to @a out. // No-op (not an error) if @a path can't be opened. +inline bool app_fs_delete_recursively(const std::string& path) { + if (path.empty() || path == "/" || path == "." || path == "..") { + return true; + } + + if (app_fs_is_directory(path)) { + FileMutex file_mutex; + file_mutex_get(&file_mutex, path.c_str()); + file_mutex_lock(&file_mutex); + + DIR* dir = opendir(path.c_str()); + if (dir == nullptr) { + file_mutex_unlock(&file_mutex); + return false; + } + + bool success = true; + struct dirent* entry; + while (success && (entry = readdir(dir)) != nullptr) { + if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) { + continue; + } + success = app_fs_delete_recursively(path + "/" + entry->d_name); + } + closedir(dir); + + if (!success) { + file_mutex_unlock(&file_mutex); + return false; + } + + bool result = rmdir(path.c_str()) == 0; + file_mutex_unlock(&file_mutex); + return result; + } + + FileMutex file_mutex; + file_mutex_get(&file_mutex, path.c_str()); + file_mutex_lock(&file_mutex); + bool result = unlink(path.c_str()) == 0; + file_mutex_unlock(&file_mutex); + return result; +} + inline void app_fs_list_direct_subdirectories(const std::string& path, std::vector& out) { // Collect child names while the directory lock is held, then release it before classifying // each one with app_fs_is_directory() - that function looks up and locks a FileMutex too, diff --git a/Modules/app-module/source/app_install.cpp b/Modules/app-module/source/app_install.cpp index fd2ea0298..8d7820099 100644 --- a/Modules/app-module/source/app_install.cpp +++ b/Modules/app-module/source/app_install.cpp @@ -66,57 +66,8 @@ bool ensure_directory_recursive(const std::string& path) { } bool delete_recursively(const std::string& path) { - LOG_D(TAG, "Deleting %s...", path.c_str()); - if (path.empty() || path == "/" || path == "." || path == "..") { - return true; - } - - if (app_fs_is_directory(path)) { - LOG_D(TAG, "Deleting dir %s", path.c_str()); - - FileMutex file_mutex; - file_mutex_get(&file_mutex, path.c_str()); - file_mutex_lock(&file_mutex); - - DIR* dir = opendir(path.c_str()); - if (dir == nullptr) { - LOG_E(TAG, "Failed to scan directory %s", path.c_str()); - file_mutex_unlock(&file_mutex); - return false; - } - - bool success = true; - dirent* entry; - while (success && (entry = readdir(dir)) != nullptr) { - if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) { - continue; - } - success = delete_recursively(path + "/" + entry->d_name); - } - closedir(dir); - - if (!success) { - file_mutex_unlock(&file_mutex); - return false; - } - - bool result = rmdir(path.c_str()) == 0; - file_mutex_unlock(&file_mutex); - return result; - } - - if (app_fs_is_file(path)) { - LOG_D(TAG, "Deleting file %s", path.c_str()); - FileMutex mutex {}; - file_mutex_get(&mutex, path.c_str()); - file_mutex_lock(&mutex); - bool result = remove(path.c_str()) == 0; - file_mutex_unlock(&mutex); - return result; - } - - LOG_D(TAG, "Deleting done"); - return true; + LOG_I(TAG, "Deleting %s...", path.c_str()); + return app_fs_delete_recursively(path); } bool get_app_install_directory(std::string& out_path) { @@ -277,6 +228,11 @@ error_t uninstall_locked(const std::string& app_id) { return ERROR_NOT_FOUND; } + // Can't uninstall in-memory apps + if (iterator->second->manifest.location.type != APP_LOCATION_PATH) { + return ERROR_NOT_SUPPORTED; + } + stop_all_instances_of(&iterator->second->manifest); app_manager_remove(app_id.c_str()); delete_recursively(iterator->second->path); @@ -404,10 +360,20 @@ error_t app_uninstall(const char* app_id) { auto& registry = install_registry(); mutex_lock(®istry.mutex); - error_t result = uninstall_locked(app_id); + error_t error = uninstall_locked(app_id); mutex_unlock(®istry.mutex); - return result; + if (error == ERROR_NOT_FOUND) { + error = app_manager_install_path_uninstall(app_id); + } + + if (error == ERROR_NONE) { + LOG_I(TAG, "Uninstalled %s", app_id); + } else { + LOG_I(TAG, "Uninstalling %s failed: %s", app_id, error_to_string(error)); + } + + return error; } } // extern "C" diff --git a/Modules/app-module/source/app_paths.cpp b/Modules/app-module/source/app_paths.cpp index fb102ac0a..8d4e1a9a1 100644 --- a/Modules/app-module/source/app_paths.cpp +++ b/Modules/app-module/source/app_paths.cpp @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 +#include +#include #include #include @@ -34,12 +36,17 @@ error_t app_paths_get_user_data_path(const char* app_id, const char* child_path, } error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_t out_path_size) { - char directory[224]; - error_t error = app_paths_get_user_data_directory(app_id, directory, sizeof(directory)); + AppManifest manifest; + error_t error = app_manager_find_manifest(app_id, &manifest); if (error != ERROR_NONE) { return error; } - int written = std::snprintf(out_path, out_path_size, "%s/assets", directory); + + if (manifest.location.type != APP_LOCATION_PATH) { + return ERROR_NOT_FOUND; + } + + int written = std::snprintf(out_path, out_path_size, "%s/assets", static_cast(manifest.location.location)); if (written < 0 || (size_t)written >= out_path_size) { return ERROR_BUFFER_OVERFLOW; } diff --git a/Modules/app-module/source/manager.cpp b/Modules/app-module/source/manager.cpp index 8db6489a0..3786f7d21 100644 --- a/Modules/app-module/source/manager.cpp +++ b/Modules/app-module/source/manager.cpp @@ -340,4 +340,30 @@ void app_manager_install_path_scan(void) { mutex_unlock(®istry.mutex); } +error_t app_manager_install_path_uninstall(const char* app_id) { + auto& registry = install_path_registry(); + + mutex_lock(®istry.mutex); + auto iterator = registry.scanned.find(app_id); + if (iterator == registry.scanned.end()) { + mutex_unlock(®istry.mutex); + return ERROR_NOT_FOUND; + } + + auto path = iterator->second->path; + mutex_unlock(®istry.mutex); + + // app_manager_remove takes ledger.mutex internally - call outside registry.mutex + // to match the lock ordering in app_manager_install_path_scan(). + app_manager_remove(app_id); + + mutex_lock(®istry.mutex); + registry.scanned.erase(app_id); + mutex_unlock(®istry.mutex); + + app_fs_delete_recursively(path); + + return ERROR_NONE; +} + } // extern "C" diff --git a/Modules/app-module/source/symbols.cpp b/Modules/app-module/source/symbols.cpp index 324f0606f..808d7404e 100644 --- a/Modules/app-module/source/symbols.cpp +++ b/Modules/app-module/source/symbols.cpp @@ -40,6 +40,7 @@ const ModuleSymbol app_module_symbols[] = { DEFINE_MODULE_SYMBOL(app_manager_get_topmost_app_id), DEFINE_MODULE_SYMBOL(app_manager_install_path_add), DEFINE_MODULE_SYMBOL(app_manager_install_path_scan), + DEFINE_MODULE_SYMBOL(app_manager_install_path_uninstall), // app/metadata DEFINE_MODULE_SYMBOL(app_metadata_parse), // app/paths diff --git a/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp b/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp index 4fd9117e1..ea7581421 100644 --- a/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp +++ b/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp @@ -152,27 +152,19 @@ void updateApp(Context* ctx) { void updateViews(Context* ctx) { lvgl_toolbar_clear_actions(ctx->toolbar); auto app_id = ctx->entry.appId.c_str(); - AppManifest manifest; - bool is_installed = app_manager_find_manifest(app_id, &manifest) == ERROR_NONE; ctx->spinner = lvgl_toolbar_add_spinner_action(ctx->toolbar); lv_obj_add_flag(ctx->spinner, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(ctx->updateLabel, LV_OBJ_FLAG_HIDDEN); char install_path[128]; - if (app_get_install_path(app_id, install_path, sizeof(install_path)) != ERROR_NONE) { - LOG_E(TAG, "Install path not found for %s", app_id); - return; - } - - std::string metadata_path = std::string(install_path) + "/manifest.properties"; - AppMetadata metadata; - if (app_metadata_parse(metadata_path.c_str(), &metadata) != ERROR_NONE) { - LOG_E(TAG, "Failed to parse metadata at %s", metadata_path.c_str()); - return; - } + bool is_installed = app_get_install_path(app_id, install_path, sizeof(install_path)) == ERROR_NONE + && file::isFile(std::string(install_path) + "/manifest.properties"); if (is_installed) { - if (metadata.app_version_code < ctx->entry.appVersionCode) { + std::string metadata_path = std::string(install_path) + "/manifest.properties"; + AppMetadata metadata; + if (app_metadata_parse(metadata_path.c_str(), &metadata) == ERROR_NONE + && metadata.app_version_code < ctx->entry.appVersionCode) { ctx->updateButton = lvgl_toolbar_add_image_button_action(ctx->toolbar, LV_SYMBOL_DOWNLOAD, onUpdatePressed, ctx); lv_obj_remove_flag(ctx->updateLabel, LV_OBJ_FLAG_HIDDEN); } From 45c796c7cd7e2091dc60a73966cc767ea9446355 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 21:45:58 +0200 Subject: [PATCH 06/13] Fix for callstack issue when downloading app in App Hub --- Buildscripts/sdkconfig/default.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Buildscripts/sdkconfig/default.properties b/Buildscripts/sdkconfig/default.properties index d7574c1a0..268021b17 100644 --- a/Buildscripts/sdkconfig/default.properties +++ b/Buildscripts/sdkconfig/default.properties @@ -1,7 +1,7 @@ # Increase stack size for Wi-Fi (fixes crash after scan) CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072 -# Ensure large enough stack for network operations -CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096 +# Ensure large enough stack for network operations (e.g. AppHub) +CONFIG_ESP_MAIN_TASK_STACK_SIZE=6144 # Fixes static assertion: FLASH and PSRAM Mode configuration are not supported CONFIG_IDF_EXPERIMENTAL_FEATURES=y # Free up IRAM From ef4d0c6cdccdef72381ae73bda30975e591210f0 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 21:55:34 +0200 Subject: [PATCH 07/13] Fixes for paths --- Modules/app-module/source/app_install.cpp | 2 +- Modules/app-module/source/app_paths.cpp | 2 +- Modules/service-module/source/paths.cpp | 2 +- Tactility/Source/app/i2cscanner/I2cScanner.cpp | 9 +++++---- Tactility/Source/app/setup/Setup.cpp | 4 ++-- Tactility/Source/network/Ntp.cpp | 8 +++++--- Tactility/Source/settings/time.cpp | 6 ++++-- TactilityKernel/include/tactility/paths.h | 2 +- TactilityKernel/source/paths.cpp | 6 +++--- TactilityKernel/source/symbols.c | 2 +- 10 files changed, 24 insertions(+), 19 deletions(-) diff --git a/Modules/app-module/source/app_install.cpp b/Modules/app-module/source/app_install.cpp index 8d7820099..d9dfe213f 100644 --- a/Modules/app-module/source/app_install.cpp +++ b/Modules/app-module/source/app_install.cpp @@ -72,7 +72,7 @@ bool delete_recursively(const std::string& path) { bool get_app_install_directory(std::string& out_path) { char root[192]; - if (paths_get_user_data_path(root, sizeof(root)) != ERROR_NONE) { + if (paths_get_data_path(root, sizeof(root)) != ERROR_NONE) { return false; } out_path = std::string(root) + "/app"; diff --git a/Modules/app-module/source/app_paths.cpp b/Modules/app-module/source/app_paths.cpp index 8d4e1a9a1..da7f0a4ec 100644 --- a/Modules/app-module/source/app_paths.cpp +++ b/Modules/app-module/source/app_paths.cpp @@ -11,7 +11,7 @@ extern "C" { error_t app_paths_get_user_data_directory(const char* app_id, char* out_path, size_t out_path_size) { char root[192]; - error_t error = paths_get_user_data_path(root, sizeof(root)); + error_t error = paths_get_data_path(root, sizeof(root)); if (error != ERROR_NONE) { return error; } diff --git a/Modules/service-module/source/paths.cpp b/Modules/service-module/source/paths.cpp index 558af2654..8dad5562f 100644 --- a/Modules/service-module/source/paths.cpp +++ b/Modules/service-module/source/paths.cpp @@ -9,7 +9,7 @@ extern "C" { error_t service_paths_get_user_data_directory(const char* service_id, char* out_path, size_t out_path_size) { char root[192]; - error_t error = paths_get_user_data_path(root, sizeof(root)); + error_t error = paths_get_data_path(root, sizeof(root)); if (error != ERROR_NONE) { return error; } diff --git a/Tactility/Source/app/i2cscanner/I2cScanner.cpp b/Tactility/Source/app/i2cscanner/I2cScanner.cpp index 1ab9b84d9..c79a84b9f 100644 --- a/Tactility/Source/app/i2cscanner/I2cScanner.cpp +++ b/Tactility/Source/app/i2cscanner/I2cScanner.cpp @@ -1,18 +1,19 @@ -#include -#include + #include #include #include +#include +#include #include #include #include +#include #include #include #include -#include #include #include @@ -55,7 +56,7 @@ struct Context { bool getPreferencesPath(std::string& outPath) { char root[128]; - if (paths_get_user_data_path(root, sizeof(root)) != ERROR_NONE) { + if (app_paths_get_user_data_directory(manifest.id, root, sizeof(root)) != ERROR_NONE) { return false; } outPath = std::string(root) + "/i2c_scanner.properties"; diff --git a/Tactility/Source/app/setup/Setup.cpp b/Tactility/Source/app/setup/Setup.cpp index 9bbf7c858..afa501b55 100644 --- a/Tactility/Source/app/setup/Setup.cpp +++ b/Tactility/Source/app/setup/Setup.cpp @@ -9,11 +9,11 @@ #include #include #include +#include #include #include -#include #include #include @@ -40,7 +40,7 @@ namespace { bool getCompletedMarkerPath(std::string& outPath) { char root[128]; - if (paths_get_user_data_path(root, sizeof(root)) != ERROR_NONE) { + if (app_paths_get_user_data_directory(manifest.id, root, sizeof(root)) != ERROR_NONE) { return false; } outPath = std::string(root) + "/.setup_complete"; diff --git a/Tactility/Source/network/Ntp.cpp b/Tactility/Source/network/Ntp.cpp index c1d513366..e1ab0dae3 100644 --- a/Tactility/Source/network/Ntp.cpp +++ b/Tactility/Source/network/Ntp.cpp @@ -1,14 +1,15 @@ +#include + #include #include #include #include +#include #include #ifdef ESP_PLATFORM -#include -#include #include #include #endif @@ -23,7 +24,8 @@ static bool processedSyncEvent = false; static bool getPreferencesPath(std::string& outPath) { char root[128]; - if (paths_get_user_data_path(root, sizeof(root)) != ERROR_NONE) { + // Not really a service, but this is the best way of organising it for now + if (service_paths_get_user_data_directory("tactility.ntp", root, sizeof(root)) != ERROR_NONE) { return false; } outPath = std::string(root) + "/time.properties"; diff --git a/Tactility/Source/settings/time.cpp b/Tactility/Source/settings/time.cpp index 79f027923..9c6e19518 100644 --- a/Tactility/Source/settings/time.cpp +++ b/Tactility/Source/settings/time.cpp @@ -1,5 +1,6 @@ -#include +#include +#include #include #include @@ -24,7 +25,8 @@ namespace { // "syncTime" - matches the shared NVS namespace this used to be. bool getPreferencesPath(std::string& outPath) { char root[128]; - if (paths_get_user_data_path(root, sizeof(root)) != ERROR_NONE) { + // Not really a service, but this is the best way of organising it for now + if (service_paths_get_user_data_directory("tactility.time", root, sizeof(root)) != ERROR_NONE) { return false; } outPath = std::string(root) + "/" + TIME_SETTINGS_NAMESPACE + ".properties"; diff --git a/TactilityKernel/include/tactility/paths.h b/TactilityKernel/include/tactility/paths.h index e8277c283..f4820150a 100644 --- a/TactilityKernel/include/tactility/paths.h +++ b/TactilityKernel/include/tactility/paths.h @@ -17,7 +17,7 @@ extern "C" { * @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small * @retval ERROR_NONE on success */ -error_t paths_get_user_data_path(char* out_path, size_t out_path_size); +error_t paths_get_data_path(char* out_path, size_t out_path_size); #ifdef __cplusplus } diff --git a/TactilityKernel/source/paths.cpp b/TactilityKernel/source/paths.cpp index 85bc2f522..3319c6304 100644 --- a/TactilityKernel/source/paths.cpp +++ b/TactilityKernel/source/paths.cpp @@ -8,7 +8,7 @@ #include #include -static error_t paths_get_user_data_root_path(char* out_path, size_t out_path_size) { +static error_t paths_get_data_root_path(char* out_path, size_t out_path_size) { #if defined(CONFIG_TT_USER_DATA_LOCATION_INTERNAL) #ifdef ESP_PLATFORM const char* mount_point = "/data"; @@ -41,10 +41,10 @@ static error_t paths_get_user_data_root_path(char* out_path, size_t out_path_siz extern "C" { -error_t paths_get_user_data_path(char* out_path, size_t out_path_size) { +error_t paths_get_data_path(char* out_path, size_t out_path_size) { #ifdef ESP_PLATFORM char root[64]; - error_t error = paths_get_user_data_root_path(root, sizeof(root)); + error_t error = paths_get_data_root_path(root, sizeof(root)); if (error != ERROR_NONE) { return error; } diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 4f3956d4e..0f1d93030 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -249,7 +249,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(keyboard_read_key), DEFINE_MODULE_SYMBOL(KEYBOARD_TYPE), // drivers/paths - DEFINE_MODULE_SYMBOL(paths_get_user_data_path), + DEFINE_MODULE_SYMBOL(paths_get_data_path), // drivers/pointer DEFINE_MODULE_SYMBOL(pointer_enter_sleep), DEFINE_MODULE_SYMBOL(pointer_exit_sleep), From 5ff98ab38c1267c6d55fcc602a99acdd8e58a24b Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 22:13:45 +0200 Subject: [PATCH 08/13] App path updates and fixes It's not super consistent yet, but I'll fix that later --- Tactility/Include/Tactility/DeprecatedPaths.h | 2 +- Tactility/Source/DeprecatedPaths.cpp | 8 +++--- Tactility/Source/Tactility.cpp | 2 +- .../bluetooth/BluetoothPairedDevice.cpp | 2 +- .../Source/bluetooth/BluetoothSettings.cpp | 2 +- Tactility/Source/network/Ntp.cpp | 7 ++--- .../service/webserver/WebServerService.cpp | 2 +- .../service/wifi/WifiBootSplashInit.cpp | 2 +- Tactility/Source/settings/AudioSettings.cpp | 26 ++++++++++++++++--- Tactility/Source/settings/BootSettings.cpp | 2 +- .../Source/settings/KeyboardSettings.cpp | 2 +- Tactility/Source/settings/SystemSettings.cpp | 6 ++--- .../settings/TouchCalibrationSettings.cpp | 2 +- Tactility/Source/settings/time.cpp | 6 ++--- 14 files changed, 42 insertions(+), 29 deletions(-) diff --git a/Tactility/Include/Tactility/DeprecatedPaths.h b/Tactility/Include/Tactility/DeprecatedPaths.h index 06532f69d..0dd21432c 100644 --- a/Tactility/Include/Tactility/DeprecatedPaths.h +++ b/Tactility/Include/Tactility/DeprecatedPaths.h @@ -15,7 +15,7 @@ FileSystem* findSdcardFileSystem(bool mustBeMounted); std::string getUserDataRootPath(); -std::string getUserDataPath(); +std::string getDataPath(); std::string getTempPath(); diff --git a/Tactility/Source/DeprecatedPaths.cpp b/Tactility/Source/DeprecatedPaths.cpp index 68d2782d3..5aa78fac7 100644 --- a/Tactility/Source/DeprecatedPaths.cpp +++ b/Tactility/Source/DeprecatedPaths.cpp @@ -51,7 +51,7 @@ std::string getUserDataRootPath() { #endif } -std::string getUserDataPath() { +std::string getDataPath() { #ifdef ESP_PLATFORM return getUserDataRootPath() + "/tactility"; #else @@ -60,15 +60,15 @@ std::string getUserDataPath() { } std::string getTempPath() { - return getUserDataPath() + "/tmp"; + return getDataPath() + "/tmp"; } std::string getAppInstallPath() { - return getUserDataPath() + "/app"; + return getDataPath() + "/app"; } std::string getUserHomePath() { - return getUserDataPath() + "/user"; + return getDataPath() + "/user"; } std::string getAppInstallPath(const std::string& appId) { diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index d2e239edd..d28d32705 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -318,7 +318,7 @@ static void registerAndStartServices() { } void createTempDirectory() { - auto data_path = getUserDataPath(); + auto data_path = getDataPath(); auto temp_path = std::format("{}/tmp", data_path); if (!file::isDirectory(temp_path)) { FileMutex mutex; diff --git a/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp b/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp index fccc67022..ad762aed1 100644 --- a/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp +++ b/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp @@ -25,7 +25,7 @@ constexpr auto* KEY_AUTO_CONNECT = "autoConnect"; constexpr auto* KEY_PROFILE_ID = "profileId"; static std::string getSettingsFilePath() { - return getUserDataPath() + "/service/bluetooth"; + return getDataPath() + "/service/bluetooth"; } std::string addrToHex(const std::array& addr) { diff --git a/Tactility/Source/bluetooth/BluetoothSettings.cpp b/Tactility/Source/bluetooth/BluetoothSettings.cpp index d2640a16d..cd599b992 100644 --- a/Tactility/Source/bluetooth/BluetoothSettings.cpp +++ b/Tactility/Source/bluetooth/BluetoothSettings.cpp @@ -11,7 +11,7 @@ namespace tt::bluetooth::settings { constexpr auto* TAG = "BluetoothSettings"; static std::string getSettingsPath() { - return getUserDataPath() + "/settings/bluetooth.settings"; + return getDataPath() + "/settings/bluetooth.properties"; } constexpr auto* KEY_ENABLE_ON_BOOT = "enableOnBoot"; diff --git a/Tactility/Source/network/Ntp.cpp b/Tactility/Source/network/Ntp.cpp index e1ab0dae3..2e81d94c8 100644 --- a/Tactility/Source/network/Ntp.cpp +++ b/Tactility/Source/network/Ntp.cpp @@ -1,5 +1,3 @@ -#include - #include #include @@ -24,11 +22,10 @@ static bool processedSyncEvent = false; static bool getPreferencesPath(std::string& outPath) { char root[128]; - // Not really a service, but this is the best way of organising it for now - if (service_paths_get_user_data_directory("tactility.ntp", root, sizeof(root)) != ERROR_NONE) { + if (paths_get_data_path(root, sizeof(root)) != ERROR_NONE) { return false; } - outPath = std::string(root) + "/time.properties"; + outPath = std::string(root) + "/settings/ntp.properties"; return true; } diff --git a/Tactility/Source/service/webserver/WebServerService.cpp b/Tactility/Source/service/webserver/WebServerService.cpp index f8b21de17..69ab1c778 100644 --- a/Tactility/Source/service/webserver/WebServerService.cpp +++ b/Tactility/Source/service/webserver/WebServerService.cpp @@ -1450,7 +1450,7 @@ esp_err_t WebServerService::handleApiScreenshot(httpd_req_t* request) { #if TT_FEATURE_SCREENSHOT_ENABLED // Determine save location: prefer SD card root if mounted, otherwise /data - std::string save_path = getUserDataPath(); + std::string save_path = getDataPath(); // Find next available filename with incrementing number std::string screenshot_path; diff --git a/Tactility/Source/service/wifi/WifiBootSplashInit.cpp b/Tactility/Source/service/wifi/WifiBootSplashInit.cpp index 36e2f8e00..b279f1a7f 100644 --- a/Tactility/Source/service/wifi/WifiBootSplashInit.cpp +++ b/Tactility/Source/service/wifi/WifiBootSplashInit.cpp @@ -126,7 +126,7 @@ void bootSplashInit() { getMainDispatcher().dispatch([] { LOG_I(TAG, "bootSplashInit dispatch begin"); // Import any provisioning files placed on the system data partition. - const std::string provisioning_path = file::getChildPath(getUserDataPath(), "provisioning"); + const std::string provisioning_path = file::getChildPath(getDataPath(), "provisioning"); if (file::isDirectory(provisioning_path)) { importWifiApSettingsFromDir(provisioning_path); } else { diff --git a/Tactility/Source/settings/AudioSettings.cpp b/Tactility/Source/settings/AudioSettings.cpp index 89277b1b3..60b54f9dc 100644 --- a/Tactility/Source/settings/AudioSettings.cpp +++ b/Tactility/Source/settings/AudioSettings.cpp @@ -1,5 +1,7 @@ #include +#include "tactility/paths.h" + #include #include #include @@ -12,8 +14,14 @@ namespace tt::settings::audio { -static std::string getSettingsFilePath() { - return getUserDataPath() + "/settings/audio.properties"; +static bool getSettingsFilePath(std::string& outPath) { + char root[128]; + // Not really a service, but this is the best way of organising it for now + if (paths_get_data_path(root, sizeof(root)) != ERROR_NONE) { + return false; + } + outPath = std::string(root) + "/settings/audio.properties"; + return true; } constexpr auto* SETTINGS_KEY_INPUT_ENABLED = "inputEnabled"; @@ -56,7 +64,11 @@ static std::string toString(float value) { } bool load(AudioSettings& settings) { - auto settings_path = getSettingsFilePath(); + std::string settings_path; + if (getSettingsFilePath(settings_path)) { + return false; + } + if (!file::isFile(settings_path)) { return false; } @@ -103,10 +115,16 @@ bool save(const AudioSettings& settings) { map[SETTINGS_KEY_OUTPUT_MUTED] = toString(settings.outputMuted); map[SETTINGS_KEY_INPUT_VOLUME] = toString(settings.inputVolume); map[SETTINGS_KEY_OUTPUT_VOLUME] = toString(settings.outputVolume); - auto settings_path = getSettingsFilePath(); + + std::string settings_path; + if (getSettingsFilePath(settings_path)) { + return false; + } + if (!file::findOrCreateParentDirectory(settings_path, 0755)) { return false; } + return file::savePropertiesFile(settings_path, map); } diff --git a/Tactility/Source/settings/BootSettings.cpp b/Tactility/Source/settings/BootSettings.cpp index d6f5549b3..8e3704d5f 100644 --- a/Tactility/Source/settings/BootSettings.cpp +++ b/Tactility/Source/settings/BootSettings.cpp @@ -14,7 +14,7 @@ constexpr auto* PROPERTIES_KEY_LAUNCHER_APP_ID = "launcherAppId"; constexpr auto* PROPERTIES_KEY_AUTO_START_APP_ID = "autoStartAppId"; static std::string getPropertiesFilePath() { - return std::format(PROPERTIES_FILE_FORMAT, getUserDataPath()); + return std::format(PROPERTIES_FILE_FORMAT, getDataPath()); } bool loadBootSettings(BootSettings& properties) { diff --git a/Tactility/Source/settings/KeyboardSettings.cpp b/Tactility/Source/settings/KeyboardSettings.cpp index ef6f9b897..3b15979a9 100644 --- a/Tactility/Source/settings/KeyboardSettings.cpp +++ b/Tactility/Source/settings/KeyboardSettings.cpp @@ -9,7 +9,7 @@ namespace tt::settings::keyboard { static std::string getSettingsFilePath() { - return getUserDataPath() + "/settings/keyboard.properties"; + return getDataPath() + "/settings/keyboard.properties"; } constexpr auto* KEY_BACKLIGHT_ENABLED = "backlightEnabled"; diff --git a/Tactility/Source/settings/SystemSettings.cpp b/Tactility/Source/settings/SystemSettings.cpp index ca797322a..8957738ee 100644 --- a/Tactility/Source/settings/SystemSettings.cpp +++ b/Tactility/Source/settings/SystemSettings.cpp @@ -21,12 +21,12 @@ static bool cached = false; static SystemSettings cachedSettings; static bool hasSystemSettingsFile() { - auto file_path = std::format(FILE_PATH_FORMAT, getUserDataPath()); + auto file_path = std::format(FILE_PATH_FORMAT, getDataPath()); return file::isFile(file_path); } static bool loadSystemSettingsFromFile(SystemSettings& properties) { - auto file_path = std::format(FILE_PATH_FORMAT, getUserDataPath()); + auto file_path = std::format(FILE_PATH_FORMAT, getDataPath()); LOG_I(TAG, "System settings loading from %s", file_path.c_str()); std::map map; if (!file::loadPropertiesFile(file_path, map)) { @@ -75,7 +75,7 @@ bool loadSystemSettings(SystemSettings& properties) { } bool saveSystemSettings(const SystemSettings& properties) { - auto file_path = std::format(FILE_PATH_FORMAT, getUserDataPath()); + auto file_path = std::format(FILE_PATH_FORMAT, getDataPath()); std::map map; map["language"] = toString(properties.language); map["timeFormat24h"] = properties.timeFormat24h ? "true" : "false"; diff --git a/Tactility/Source/settings/TouchCalibrationSettings.cpp b/Tactility/Source/settings/TouchCalibrationSettings.cpp index bc323c008..1c3864baa 100644 --- a/Tactility/Source/settings/TouchCalibrationSettings.cpp +++ b/Tactility/Source/settings/TouchCalibrationSettings.cpp @@ -13,7 +13,7 @@ namespace tt::settings::touch { static std::string getSettingsFilePath() { - return getUserDataPath() + "/settings/touch-calibration.properties"; + return getDataPath() + "/settings/touch-calibration.properties"; } constexpr auto* SETTINGS_KEY_ENABLED = "enabled"; diff --git a/Tactility/Source/settings/time.cpp b/Tactility/Source/settings/time.cpp index 9c6e19518..bccb3d188 100644 --- a/Tactility/Source/settings/time.cpp +++ b/Tactility/Source/settings/time.cpp @@ -13,8 +13,6 @@ namespace tt::settings { -constexpr auto* TIME_SETTINGS_NAMESPACE = "time"; - constexpr auto* TIMEZONE_PREFERENCES_KEY_NAME = "tz_name"; constexpr auto* TIMEZONE_PREFERENCES_KEY_CODE = "tz_code"; constexpr auto* TIMEZONE_PREFERENCES_KEY_TIME24 = "tz_time24"; @@ -26,10 +24,10 @@ namespace { bool getPreferencesPath(std::string& outPath) { char root[128]; // Not really a service, but this is the best way of organising it for now - if (service_paths_get_user_data_directory("tactility.time", root, sizeof(root)) != ERROR_NONE) { + if (paths_get_data_path(root, sizeof(root)) != ERROR_NONE) { return false; } - outPath = std::string(root) + "/" + TIME_SETTINGS_NAMESPACE + ".properties"; + outPath = std::string(root) + "/settings/time.properties"; return true; } From 5cc815e99c389e66328be57986273c84f4a502c3 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 22:45:42 +0200 Subject: [PATCH 09/13] Fix for paths test --- .../tests/source/service_paths_test.cpp | 10 +++++----- TactilityKernel/tests/source/paths_test.cpp | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Modules/service-module/tests/source/service_paths_test.cpp b/Modules/service-module/tests/source/service_paths_test.cpp index ff197e9ee..ba19e006d 100644 --- a/Modules/service-module/tests/source/service_paths_test.cpp +++ b/Modules/service-module/tests/source/service_paths_test.cpp @@ -7,20 +7,20 @@ #include #include -TEST_CASE("paths_get_user_data_path returns a non-empty path") { +TEST_CASE("paths_get_data_path returns a non-empty path") { char buffer[192]; - CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_NONE); CHECK_GT(std::strlen(buffer), 0); } -TEST_CASE("paths_get_user_data_path reports overflow for a too-small buffer") { +TEST_CASE("paths_get_data_path reports overflow for a too-small buffer") { char buffer[1]; - CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW); + CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW); } TEST_CASE("service_paths_get_user_data_directory includes the service id") { char root[192]; - REQUIRE_EQ(paths_get_user_data_path(root, sizeof(root)), ERROR_NONE); + REQUIRE_EQ(paths_get_data_path(root, sizeof(root)), ERROR_NONE); char buffer[224]; CHECK_EQ(service_paths_get_user_data_directory("my-service", buffer, sizeof(buffer)), ERROR_NONE); diff --git a/TactilityKernel/tests/source/paths_test.cpp b/TactilityKernel/tests/source/paths_test.cpp index 7c83c70f5..e851dfe6e 100644 --- a/TactilityKernel/tests/source/paths_test.cpp +++ b/TactilityKernel/tests/source/paths_test.cpp @@ -4,28 +4,28 @@ #include -// The simulator target is never built with ESP_PLATFORM, so paths_get_user_data_path() +// The simulator target is never built with ESP_PLATFORM, so paths_get_data_path() // always takes the fixed "data" path branch here, guarded by a buffer-size check. -TEST_CASE("paths_get_user_data_path succeeds when the buffer exactly fits") { +TEST_CASE("paths_get_data_path succeeds when the buffer exactly fits") { char buffer[16] = { 0 }; - CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_NONE); CHECK_EQ(std::strcmp(buffer, "data"), 0); } -TEST_CASE("paths_get_user_data_path succeeds with a buffer sized to exactly fit the string and terminator") { +TEST_CASE("paths_get_data_path succeeds with a buffer sized to exactly fit the string and terminator") { char buffer[5] = { 0 }; // strlen("data") + 1 - CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_NONE); CHECK_EQ(std::strcmp(buffer, "data"), 0); } -TEST_CASE("paths_get_user_data_path reports a buffer overflow when the buffer is one byte too small") { +TEST_CASE("paths_get_data_path reports a buffer overflow when the buffer is one byte too small") { char buffer[4] = { 0 }; // strlen("data"), no room for the terminator - CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW); + CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW); } -TEST_CASE("paths_get_user_data_path reports a buffer overflow for a zero-size buffer") { +TEST_CASE("paths_get_data_path reports a buffer overflow for a zero-size buffer") { char buffer[1] = { 'x' }; - CHECK_EQ(paths_get_user_data_path(buffer, 0), ERROR_BUFFER_OVERFLOW); + CHECK_EQ(paths_get_data_path(buffer, 0), ERROR_BUFFER_OVERFLOW); CHECK_EQ(buffer[0], 'x'); // untouched } From 5d89d40541abb39d4ce95c203c345f86560c6d4c Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 23:08:52 +0200 Subject: [PATCH 10/13] Fix for PC build --- Modules/app-module/private/app/private/app_fs.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/app-module/private/app/private/app_fs.h b/Modules/app-module/private/app/private/app_fs.h index 1c04faa00..9438976b3 100644 --- a/Modules/app-module/private/app/private/app_fs.h +++ b/Modules/app-module/private/app/private/app_fs.h @@ -12,7 +12,11 @@ #include #include #include +#ifdef ESP_PLATFORM #include +#else +#include +#endif #include inline bool app_fs_is_directory(const std::string& path) { From 8b4124d4be192dee2654f7d3c07c0e012aaf1a94 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 23:16:35 +0200 Subject: [PATCH 11/13] Fixes --- .../app-module/private/app/private/app_fs.h | 59 +++++++++++++++---- Modules/app-module/source/manager.cpp | 36 +++++++++-- .../tests/source/service_paths_test.cpp | 2 +- 3 files changed, 79 insertions(+), 18 deletions(-) diff --git a/Modules/app-module/private/app/private/app_fs.h b/Modules/app-module/private/app/private/app_fs.h index 9438976b3..b76445baa 100644 --- a/Modules/app-module/private/app/private/app_fs.h +++ b/Modules/app-module/private/app/private/app_fs.h @@ -46,39 +46,76 @@ inline bool app_fs_delete_recursively(const std::string& path) { return true; } - if (app_fs_is_directory(path)) { - FileMutex file_mutex; - file_mutex_get(&file_mutex, path.c_str()); + // Use lstat() so symbolic links are not followed: a symlink that points at + // an external directory must be removed as a leaf entry (unlink), not + // recursed into. app_fs_is_directory() uses stat() and would follow the + // link, potentially deleting files outside the target tree. + // ESP-IDF newlib has no lstat(); ESP32 filesystems (FAT/SPIFFS) don't + // support symlinks, so stat() is equivalent there. + struct stat st {}; + FileMutex file_mutex; + file_mutex_get(&file_mutex, path.c_str()); + file_mutex_lock(&file_mutex); +#ifdef ESP_PLATFORM + int rc = stat(path.c_str(), &st); +#else + int rc = lstat(path.c_str(), &st); +#endif + file_mutex_unlock(&file_mutex); + + if (rc != 0) { + return false; + } + +#ifndef ESP_PLATFORM + if (S_ISLNK(st.st_mode)) { + // Symlink — remove as a leaf regardless of its target. file_mutex_lock(&file_mutex); + bool result = unlink(path.c_str()) == 0; + file_mutex_unlock(&file_mutex); + return result; + } +#endif + + if (S_ISDIR(st.st_mode)) { + // Collect child names while locked, then release before recursing — + // child paths can resolve to the same mount mutex (see + // app_fs_list_direct_subdirectories comment), so holding the parent + // lock across the recursive call would self-deadlock. + std::vector children; + file_mutex_lock(&file_mutex); DIR* dir = opendir(path.c_str()); if (dir == nullptr) { file_mutex_unlock(&file_mutex); return false; } - bool success = true; struct dirent* entry; - while (success && (entry = readdir(dir)) != nullptr) { + while ((entry = readdir(dir)) != nullptr) { if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) { continue; } - success = app_fs_delete_recursively(path + "/" + entry->d_name); + children.push_back(path + "/" + entry->d_name); } closedir(dir); + file_mutex_unlock(&file_mutex); - if (!success) { - file_mutex_unlock(&file_mutex); - return false; + bool success = true; + for (const auto& child : children) { + success = app_fs_delete_recursively(child); + if (!success) { + return false; + } } + file_mutex_lock(&file_mutex); bool result = rmdir(path.c_str()) == 0; file_mutex_unlock(&file_mutex); return result; } - FileMutex file_mutex; - file_mutex_get(&file_mutex, path.c_str()); + // Regular file or other — unlink. file_mutex_lock(&file_mutex); bool result = unlink(path.c_str()) == 0; file_mutex_unlock(&file_mutex); diff --git a/Modules/app-module/source/manager.cpp b/Modules/app-module/source/manager.cpp index 3786f7d21..96b976943 100644 --- a/Modules/app-module/source/manager.cpp +++ b/Modules/app-module/source/manager.cpp @@ -1,13 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 #include - #include - #include #include #include #include +#include #include #include @@ -350,19 +349,44 @@ error_t app_manager_install_path_uninstall(const char* app_id) { return ERROR_NOT_FOUND; } + const AppManifest* manifest = &iterator->second->manifest; auto path = iterator->second->path; mutex_unlock(®istry.mutex); - // app_manager_remove takes ledger.mutex internally - call outside registry.mutex - // to match the lock ordering in app_manager_install_path_scan(). + // Stop every running instance that retains this manifest pointer, mirroring + // stop_all_instances_of() in app_install.cpp. Collect under ledger.mutex, + // then call app_manager_stop() outside it (that call bound-joins the + // instance's thread, which itself takes ledger.mutex in its thread_main). + std::vector instance_ids; + auto& ledger = app_ledger(); + mutex_lock(&ledger.mutex); + for (const auto& [id, record] : ledger.instances) { + if (record.manifest == manifest) { + instance_ids.push_back(id); + } + } + mutex_unlock(&ledger.mutex); + + for (uint32_t id : instance_ids) { + app_manager_stop(id); + } + + // app_manager_remove takes ledger.mutex internally - call outside both + // registry.mutex and ledger.mutex to match the lock ordering in + // app_manager_install_path_scan(). app_manager_remove(app_id); + // Every instance has stopped and the manifest is unregistered — safe to + // delete the on-disk directory. Delete before erasing the scan record so + // that a failed deletion leaves the entry discoverable for a retry. + if (!app_fs_delete_recursively(path)) { + return ERROR_RESOURCE; + } + mutex_lock(®istry.mutex); registry.scanned.erase(app_id); mutex_unlock(®istry.mutex); - app_fs_delete_recursively(path); - return ERROR_NONE; } diff --git a/Modules/service-module/tests/source/service_paths_test.cpp b/Modules/service-module/tests/source/service_paths_test.cpp index ba19e006d..4730b7a8e 100644 --- a/Modules/service-module/tests/source/service_paths_test.cpp +++ b/Modules/service-module/tests/source/service_paths_test.cpp @@ -9,7 +9,7 @@ TEST_CASE("paths_get_data_path returns a non-empty path") { char buffer[192]; - CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_NONE); + REQUIRE_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_NONE); CHECK_GT(std::strlen(buffer), 0); } From e95e090b77d96a14e8334a590a2c2fa390d2e427 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 23:17:20 +0200 Subject: [PATCH 12/13] Invert logic --- Tactility/Source/settings/AudioSettings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tactility/Source/settings/AudioSettings.cpp b/Tactility/Source/settings/AudioSettings.cpp index 60b54f9dc..3f6ab0c24 100644 --- a/Tactility/Source/settings/AudioSettings.cpp +++ b/Tactility/Source/settings/AudioSettings.cpp @@ -65,7 +65,7 @@ static std::string toString(float value) { bool load(AudioSettings& settings) { std::string settings_path; - if (getSettingsFilePath(settings_path)) { + if (!getSettingsFilePath(settings_path)) { return false; } From 60b7d4b12592ab09139b96409da0d5b2d29a14a8 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 19 Aug 2026 23:23:23 +0200 Subject: [PATCH 13/13] Revert setup path To avoid recursive directory creation --- Tactility/Source/app/setup/Setup.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Tactility/Source/app/setup/Setup.cpp b/Tactility/Source/app/setup/Setup.cpp index afa501b55..2045ddf02 100644 --- a/Tactility/Source/app/setup/Setup.cpp +++ b/Tactility/Source/app/setup/Setup.cpp @@ -1,5 +1,7 @@ #include +#include + #include #include #include @@ -9,7 +11,6 @@ #include #include #include -#include #include @@ -39,11 +40,11 @@ constexpr auto* TAG = "setup"; namespace { bool getCompletedMarkerPath(std::string& outPath) { - char root[128]; - if (app_paths_get_user_data_directory(manifest.id, root, sizeof(root)) != ERROR_NONE) { + char path[128]; + if (paths_get_data_path(path, sizeof(path)) != ERROR_NONE) { return false; } - outPath = std::string(root) + "/.setup_complete"; + outPath = std::string(path) + "/.setup_complete"; return true; }