Fixes and improvements (#617)

This commit is contained in:
Ken Van Hoeylandt 2026-08-20 17:22:01 +02:00 committed by GitHub
parent b03759a111
commit 0ff1627385
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 309 additions and 342 deletions

View File

@ -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

View File

@ -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.
@ -46,14 +47,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.

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "instance.h"
#include <stddef.h>
#include <stdint.h>
@ -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.

View File

@ -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

View File

@ -12,6 +12,11 @@
#include <dirent.h>
#include <string>
#include <sys/stat.h>
#ifdef ESP_PLATFORM
#include <sys/unistd.h>
#else
#include <unistd.h>
#endif
#include <vector>
inline bool app_fs_is_directory(const std::string& path) {
@ -36,6 +41,87 @@ 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;
}
// 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<std::string> children;
file_mutex_lock(&file_mutex);
DIR* dir = opendir(path.c_str());
if (dir == nullptr) {
file_mutex_unlock(&file_mutex);
return false;
}
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) {
continue;
}
children.push_back(path + "/" + entry->d_name);
}
closedir(dir);
file_mutex_unlock(&file_mutex);
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;
}
// Regular file or other — unlink.
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<std::string>& 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,

View File

@ -66,62 +66,13 @@ 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) {
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";
@ -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(&registry.mutex);
error_t result = uninstall_locked(app_id);
error_t error = uninstall_locked(app_id);
mutex_unlock(&registry.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"

View File

@ -1,5 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/location.h>
#include <app/manager.h>
#include <app/paths.h>
#include <tactility/paths.h>
@ -9,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;
}
@ -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<const char*>(manifest.location.location));
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}

View File

@ -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();

View File

@ -1,13 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manager.h>
#include <app/metadata.h>
#include <app/private/app_fs.h>
#include <app/private/app_ledger.h>
#include <app/private/app_scheduler.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <algorithm>
@ -340,4 +339,55 @@ void app_manager_install_path_scan(void) {
mutex_unlock(&registry.mutex);
}
error_t app_manager_install_path_uninstall(const char* app_id) {
auto& registry = install_path_registry();
mutex_lock(&registry.mutex);
auto iterator = registry.scanned.find(app_id);
if (iterator == registry.scanned.end()) {
mutex_unlock(&registry.mutex);
return ERROR_NOT_FOUND;
}
const AppManifest* manifest = &iterator->second->manifest;
auto path = iterator->second->path;
mutex_unlock(&registry.mutex);
// 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<uint32_t> 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(&registry.mutex);
registry.scanned.erase(app_id);
mutex_unlock(&registry.mutex);
return ERROR_NONE;
}
} // extern "C"

View File

@ -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

View File

@ -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;
}

View File

@ -7,20 +7,20 @@
#include <cstring>
#include <string>
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);
REQUIRE_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);

View File

@ -2,9 +2,6 @@
#include <sdkconfig.h>
#endif
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/module.h>
#include <soc/soc_caps.h>
@ -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.
#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);
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT
check(driver_construct_add(&esp32_usb_hid_device_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED
check(driver_construct_add(&esp32_usb_msc_device_driver) == ERROR_NONE);
#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);
#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);
&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_remove_destruct(&esp32_usb_device_controller_driver) == ERROR_NONE);
&esp32_usb_device_controller_driver,
#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);
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT
&esp32_usb_hid_device_driver,
#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);
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED
&esp32_usb_msc_device_driver,
#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);
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT
&esp32_usb_midi_device_driver,
#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);
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED
&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
};

View File

@ -1,26 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/check.h>
#include <tactility/driver.h>
#include <tactility/module.h>
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
};

View File

@ -15,7 +15,7 @@ FileSystem* findSdcardFileSystem(bool mustBeMounted);
std::string getUserDataRootPath();
std::string getUserDataPath();
std::string getDataPath();
std::string getTempPath();

View File

@ -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) {

View File

@ -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;

View File

@ -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);
}

View File

@ -1,18 +1,19 @@
#include <Tactility/app/i2cscanner/I2cHelpers.h>
#include <Tactility/app/i2cscanner/I2cScannerPrivate.h>
#include <Tactility/LogMessages.h>
#include <Tactility/RecursiveMutex.h>
#include <Tactility/Timer.h>
#include <Tactility/app/i2cscanner/I2cHelpers.h>
#include <Tactility/app/i2cscanner/I2cScannerPrivate.h>
#include <app/event.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/paths.h>
#include <lvgl_window_manager/window_manager.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#include <tactility/paths.h>
#include <tactility/preferences.h>
#include <cassert>
@ -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";

View File

@ -1,5 +1,7 @@
#include <Tactility/app/setup/Setup.h>
#include <tactility/paths.h>
#include <Tactility/StringUtils.h>
#include <Tactility/app/timezone/TimeZone.h>
#include <Tactility/app/wifimanage/WifiManage.h>
@ -13,7 +15,6 @@
#include <lvgl_window_manager/window_manager.h>
#include <tactility/log.h>
#include <tactility/paths.h>
#include <lvgl/fonts.h>
#include <lvgl/lvgl.h>
@ -39,11 +40,11 @@ constexpr auto* TAG = "setup";
namespace {
bool getCompletedMarkerPath(std::string& outPath) {
char root[128];
if (paths_get_user_data_path(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;
}

View File

@ -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<uint8_t, 6>& addr) {

View File

@ -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";

View File

@ -3,12 +3,11 @@
#include <tactility/log.h>
#include <tactility/paths.h>
#include <tactility/preferences.h>
#include <tactility/system_event.h>
#include <string>
#ifdef ESP_PLATFORM
#include <Tactility/TactilityCore.h>
#include <tactility/system_event.h>
#include <esp_netif_sntp.h>
#include <esp_sntp.h>
#endif
@ -23,10 +22,10 @@ static bool processedSyncEvent = false;
static bool getPreferencesPath(std::string& outPath) {
char root[128];
if (paths_get_user_data_path(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;
}

View File

@ -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;

View File

@ -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 {

View File

@ -1,5 +1,7 @@
#include <Tactility/settings/AudioSettings.h>
#include "tactility/paths.h"
#include <Tactility/DeprecatedPaths.h>
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
@ -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);
}

View File

@ -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) {

View File

@ -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";

View File

@ -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<std::string, std::string> 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<std::string, std::string> map;
map["language"] = toString(properties.language);
map["timeFormat24h"] = properties.timeFormat24h ? "true" : "false";

View File

@ -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";

View File

@ -1,5 +1,6 @@
#include <Tactility/settings/Time.h>
#include <service/paths.h>
#include <Tactility/settings/Time.h>
#include <Tactility/settings/SystemSettings.h>
#include <tactility/paths.h>
@ -12,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";
@ -24,10 +23,11 @@ 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 (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;
}

View File

@ -1,42 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stddef.h>
#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

View File

@ -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),

View File

@ -1,42 +0,0 @@
#include "tt_time.h"
#include <Tactility/settings/Time.h>
#include <cstring>
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);
}
}

View File

@ -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
}

View File

@ -8,7 +8,7 @@
#include <cstdio>
#include <cstring>
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;
}

View File

@ -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),

View File

@ -4,28 +4,28 @@
#include <tactility/paths.h>
// 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
}