Merge remote-tracking branch 'origin/main' into main

This commit is contained in:
Adolfo Reyna
2026-07-04 15:39:52 -04:00
396 changed files with 114753 additions and 2051 deletions
+3 -9
View File
@@ -1,21 +1,15 @@
#include <Tactility/service/ServicePaths.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/MountPoints.h>
#include <Tactility/Paths.h>
#include <cassert>
#include <format>
#ifdef ESP_PLATFORM
constexpr auto PARTITION_PREFIX = std::string("/");
#else
constexpr auto PARTITION_PREFIX = std::string("");
#endif
namespace tt::service {
std::string ServicePaths::getUserDataDirectory() const {
return std::format("{}{}/service/{}", PARTITION_PREFIX, file::DATA_PARTITION_NAME, manifest->id);
return std::format("{}/service/{}", tt::getUserDataPath(), manifest->id);
}
std::string ServicePaths::getUserDataPath(const std::string& childPath) const {
@@ -24,7 +18,7 @@ std::string ServicePaths::getUserDataPath(const std::string& childPath) const {
}
std::string ServicePaths::getAssetsDirectory() const {
return std::format("{}{}/service/{}/assets", PARTITION_PREFIX, file::SYSTEM_PARTITION_NAME, manifest->id);
return std::format("{}/service/{}/assets", tt::getUserDataPath(), manifest->id);
}
std::string ServicePaths::getAssetsPath(const std::string& childPath) const {
@@ -1,6 +1,8 @@
#ifdef ESP_PLATFORM
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Logger.h>
#include <Tactility/Paths.h>
#include <Tactility/service/development/DevelopmentSettings.h>
#include <map>
#include <string>
@@ -9,7 +11,10 @@ namespace tt::service::development {
static const auto LOGGER = Logger("DevSettings");
constexpr auto* SETTINGS_FILE = "/data/settings/development.properties";
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/development.properties";
}
constexpr auto* SETTINGS_KEY_ENABLE_ON_BOOT = "enableOnBoot";
struct DevelopmentSettings {
@@ -17,8 +22,13 @@ struct DevelopmentSettings {
};
static bool load(DevelopmentSettings& settings) {
auto settings_path = getSettingsFilePath();
if (!file::isFile(settings_path)) {
return false;
}
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
if (!file::loadPropertiesFile(settings_path, map)) {
return false;
}
@@ -34,13 +44,18 @@ static bool load(DevelopmentSettings& settings) {
static bool save(const DevelopmentSettings& settings) {
std::map<std::string, std::string> map;
map[SETTINGS_KEY_ENABLE_ON_BOOT] = settings.enableOnBoot ? "true" : "false";
return file::savePropertiesFile(SETTINGS_FILE, map);
auto settings_path = getSettingsFilePath();
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
LOGGER.error("Failed to create parent dir for {}", settings_path);
return false;
}
return file::savePropertiesFile(settings_path, map);
}
void setEnableOnBoot(bool enable) {
DevelopmentSettings properties { .enableOnBoot = enable };
if (!save(properties)) {
LOGGER.error("Failed to save {}", SETTINGS_FILE);
LOGGER.error("Failed to save {}", getSettingsFilePath());
}
}
+52 -28
View File
@@ -27,14 +27,49 @@ void warnIfRunningOnGuiTask(const char* context) {
}
}
namespace {
enum class GuiDispatchType { Show, Hide, Exit };
struct GuiDispatchItem {
GuiService* service;
GuiDispatchType type;
std::shared_ptr<app::AppInstance> appInstance; // only used for Show
};
} // namespace
// region AppManifest
void GuiService::onGuiDispatch(void* context) {
std::unique_ptr<GuiDispatchItem> item(static_cast<GuiDispatchItem*>(context));
switch (item->type) {
case GuiDispatchType::Show:
item->service->showApp(item->appInstance);
break;
case GuiDispatchType::Hide:
item->service->hideApp();
break;
case GuiDispatchType::Exit:
item->service->exitRequested = true;
break;
}
}
void GuiService::onLoaderEvent(LoaderService::Event event) {
GuiDispatchItem* item;
if (event == LoaderService::Event::ApplicationShowing) {
auto app_instance = std::static_pointer_cast<app::AppInstance>(app::getCurrentAppContext());
showApp(app_instance);
item = new GuiDispatchItem{this, GuiDispatchType::Show, app_instance};
} else if (event == LoaderService::Event::ApplicationHiding) {
hideApp();
item = new GuiDispatchItem{this, GuiDispatchType::Hide, nullptr};
} else {
return;
}
if (dispatcher_dispatch(dispatcher, item, onGuiDispatch) != ERROR_NONE) {
LOGGER.error("Failed to dispatch gui event");
delete item;
}
}
@@ -82,26 +117,8 @@ int32_t GuiService::guiMain() {
lvgl::unlock();
while (true) {
uint32_t flags = 0;
if (service->threadFlags.wait(GUI_THREAD_FLAG_ALL, false, true, &flags, portMAX_DELAY)) {
// When service not started or starting -> exit
State service_state = getState(manifest.id);
if (service_state != State::Started && service_state != State::Starting) {
break;
}
// Process and dispatch draw call
if (flags & GUI_THREAD_FLAG_DRAW) {
service->threadFlags.clear(GUI_THREAD_FLAG_DRAW);
service->redraw();
}
if (flags & GUI_THREAD_FLAG_EXIT) {
service->threadFlags.clear(GUI_THREAD_FLAG_EXIT);
break;
}
}
while (!service->exitRequested) {
dispatcher_consume(service->dispatcher);
}
service->appRootWidget = nullptr;
@@ -177,6 +194,9 @@ void GuiService::redraw() {
}
bool GuiService::onStart(ServiceContext& service) {
exitRequested = false;
dispatcher = dispatcher_alloc();
thread = new Thread(
GUI_TASK_NAME,
4096, // Last known minimum was 2800 for launching desktop
@@ -211,8 +231,14 @@ void GuiService::onStop(ServiceContext& service) {
appToRender = nullptr;
isStarted = false;
threadFlags.set(GUI_THREAD_FLAG_EXIT);
unlock();
auto* exit_item = new GuiDispatchItem{this, GuiDispatchType::Exit, nullptr};
if (dispatcher_dispatch(dispatcher, exit_item, onGuiDispatch) != ERROR_NONE) {
LOGGER.error("Failed to dispatch gui exit event");
check(false, "Failed to dispatch exit signal to thread.");
delete exit_item;
}
thread->join();
if (lvgl::lock()) {
@@ -226,10 +252,8 @@ void GuiService::onStop(ServiceContext& service) {
}
delete thread;
}
void GuiService::requestDraw() {
threadFlags.set(GUI_THREAD_FLAG_DRAW);
dispatcher_free(dispatcher);
dispatcher = nullptr;
}
void GuiService::showApp(std::shared_ptr<app::AppInstance> app) {
@@ -253,7 +277,7 @@ void GuiService::showApp(std::shared_ptr<app::AppInstance> app) {
}
appToRender = std::move(app);
requestDraw();
redraw();
}
void GuiService::hideApp() {
@@ -166,11 +166,11 @@ class StatusbarService final : public Service {
}
void updateBluetoothIcon() {
auto radio_state = tt::bluetooth::getRadioState();
struct Device* btdev = tt::bluetooth::findFirstDevice();
auto radio_state = bluetooth::getRadioState();
Device* btdev = device_find_first_active_by_type(&BLUETOOTH_TYPE);
bool scanning = btdev ? bluetooth_is_scanning(btdev) : false;
struct Device* serial_dev = bluetooth_serial_get_device();
struct Device* midi_dev = bluetooth_midi_get_device();
Device* serial_dev = bluetooth_serial_get_device();
Device* midi_dev = bluetooth_midi_get_device();
bool connected = (serial_dev && bluetooth_serial_is_connected(serial_dev)) ||
(midi_dev && bluetooth_midi_is_connected(midi_dev));
const char* desired_icon = getBluetoothStatusIcon(radio_state, scanning, connected);
@@ -213,8 +213,8 @@ class StatusbarService final : public Service {
}
void updateUsbIcon() {
struct Device* hid_dev = device_find_first_active_by_type(&USB_HOST_HID_TYPE);
struct Device* midi_dev = device_find_first_active_by_type(&USB_HOST_MIDI_TYPE);
Device* hid_dev = device_find_first_active_by_type(&USB_HOST_HID_TYPE);
Device* midi_dev = device_find_first_active_by_type(&USB_HOST_MIDI_TYPE);
bool connected = (hid_dev && usb_host_hid_is_connected(hid_dev)) ||
(midi_dev && usb_midi_is_connected(midi_dev));
if (!connected) {
@@ -15,9 +15,9 @@
namespace tt::service::webserver {
static const auto LOGGER = tt::Logger("AssetVersion");
constexpr auto* DATA_VERSION_FILE = "/data/webserver/version.json";
constexpr auto* DATA_VERSION_FILE = "/system/app/WebServer/version.json";
constexpr auto* SD_VERSION_FILE = "/sdcard/tactility/webserver/version.json";
constexpr auto* DATA_ASSETS_DIR = "/data/webserver";
constexpr auto* DATA_ASSETS_DIR = "/system/app/WebServer";
constexpr auto* SD_ASSETS_DIR = "/sdcard/tactility/webserver";
static bool loadVersionFromFile(const char* path, AssetVersion& version) {
@@ -349,17 +349,6 @@ bool syncAssets() {
return true;
}
// POST-FLASH RECOVERY: Data empty but SD card exists
if (!dataExists) {
LOGGER.info("Data partition empty - copying from SD card (recovery mode)");
if (!copyDirectory(SD_ASSETS_DIR, DATA_ASSETS_DIR)) {
LOGGER.error("Failed to copy assets from SD card to Data");
return false;
}
LOGGER.info("Recovery complete - assets restored from SD card");
return true;
}
// NORMAL OPERATION: Both exist - compare versions
AssetVersion dataVersion, sdVersion;
bool hasDataVer = loadDataVersion(dataVersion);
@@ -210,11 +210,6 @@ bool WebServerService::onStart(ServiceContext& service) {
statusbarIconId = lvgl::statusbar_icon_add();
lvgl::statusbar_icon_set_visibility(statusbarIconId, false);
// Run asset synchronization on startup
if (!syncAssets()) {
LOGGER.warn("Asset sync failed, but continuing with available assets");
}
// Load and cache settings once at boot
bool serverEnabled;
{
@@ -629,7 +624,7 @@ static bool isAllowedBasePath(const std::string& path, bool allowRoot = false) {
return false;
}
if (allowRoot && path == "/") return true;
return path == "/data" || path.starts_with("/data/") || path == "/sdcard" || path.starts_with("/sdcard/");
return path.starts_with("/data") || path.starts_with("/system/app/WebServer") || path.starts_with("/sdcard");
}
// Normalize client-supplied path: URL-decode, trim quotes/control chars, ensure leading slash, collapse duplicate slashes
@@ -1000,7 +995,6 @@ esp_err_t WebServerService::handleAdminPost(httpd_req_t* request) {
}
const char* uri = request->uri;
if (strncmp(uri, "/admin/sync", 11) == 0) return handleSync(request);
if (strncmp(uri, "/admin/reboot", 13) == 0) return handleReboot(request);
LOGGER.info("POST {} - not found in admin dispatcher", uri);
httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "not found");
@@ -1482,10 +1476,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;
if (!findFirstMountedSdCardPath(save_path)) {
save_path = file::MOUNT_POINT_DATA;
}
std::string save_path = getUserDataPath();
// Find next available filename with incrementing number
std::string screenshot_path;
@@ -1705,25 +1696,9 @@ esp_err_t WebServerService::handleFsRename(httpd_req_t* request) {
// endregion
esp_err_t WebServerService::handleSync(httpd_req_t* request) {
LOGGER.info("POST /sync");
bool success = syncAssets();
if (success) {
httpd_resp_sendstr(request, "Assets synchronized successfully");
} else {
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Asset sync failed");
}
return success ? ESP_OK : ESP_FAIL;
}
esp_err_t WebServerService::handleReboot(httpd_req_t* request) {
LOGGER.info("POST /reboot");
httpd_resp_sendstr(request, "Rebooting...");
// Reboot after a short delay to allow response to be sent
@@ -1746,7 +1721,7 @@ esp_err_t WebServerService::handleAssets(httpd_req_t* request) {
// Special case: serve favicon from system assets
if (strcmp(uri, "/favicon.ico") == 0) {
const char* faviconPath = "/data/system/spinner.png";
const char* faviconPath = "/system/spinner.png";
if (file::isFile(faviconPath)) {
httpd_resp_set_type(request, "image/png");
httpd_resp_set_hdr(request, "Cache-Control", "public, max-age=86400");
@@ -1789,11 +1764,9 @@ esp_err_t WebServerService::handleAssets(httpd_req_t* request) {
return ESP_FAIL;
}
std::string dataPath = std::string("/data/webserver") + requestedPath;
std::string dataPath = std::string("/system/app/WebServer") + requestedPath;
if (requestedPath == "/dashboard.html" && !file::isFile(dataPath.c_str())) {
// Dashboard doesn't exist, try default.html
dataPath = "/data/webserver/default.html";
LOGGER.info("dashboard.html not found, serving default.html");
}
@@ -1876,6 +1849,11 @@ void setWebServerEnabled(bool enabled) {
}
}
bool isWebServerEnabled() {
WebServerService* instance = g_webServerInstance.load();
return instance != nullptr && instance->isEnabled();
}
} // namespace
#endif // ESP_PLATFORM
@@ -127,6 +127,10 @@ bool load(const std::string& ssid, WifiApSettings& apSettings) {
return false;
}
const auto file_path = getApPropertiesFilePath(service_context->getPaths(), ssid);
if (!file::isFile(file_path)) {
return false;
}
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(file_path, map)) {
return false;
@@ -181,6 +185,10 @@ bool save(const WifiApSettings& apSettings) {
}
const auto file_path = getApPropertiesFilePath(service_context->getPaths(), apSettings.ssid);
if (!file::findOrCreateParentDirectory(file_path, 0755)) {
LOGGER.error("Failed to create {}", file_path);
return false;
}
std::map<std::string, std::string> map;
@@ -1,4 +1,8 @@
#include <Tactility/service/wifi/WifiBootSplashInit.h>
#include "Tactility/service/wifi/Wifi.h"
#include "Tactility/service/wifi/WifiSettings.h"
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/MountPoints.h>
@@ -116,17 +120,24 @@ static void importWifiApSettingsFromDir(const std::string& path) {
}
void bootSplashInit() {
LOGGER.info("bootSplashInit dispatch");
getMainDispatcher().dispatch([] {
// First import any provisioning files placed on the system data partition.
const std::string data_settings_path = file::getChildPath(file::MOUNT_POINT_DATA, "settings");
importWifiApSettingsFromDir(data_settings_path);
// Then scan attached SD cards as before.
std::string sdcard_path;
if (findFirstMountedSdCardPath((sdcard_path))) {
const std::string sd_settings_path = file::getChildPath(sdcard_path, "settings");
importWifiApSettingsFromDir(sd_settings_path);
LOGGER.info("bootSplashInit dispatch begin");
// Import any provisioning files placed on the system data partition.
const std::string provisioning_path = file::getChildPath(getUserDataPath(), "provisioning");
if (file::isDirectory(provisioning_path)) {
importWifiApSettingsFromDir(provisioning_path);
} else {
LOGGER.info("Skip provisioning: no files at {}", provisioning_path);
}
// Dispatch WiFi on
if (settings::shouldEnableOnBoot()) {
LOGGER.info("Auto-enabling WiFi");
getMainDispatcher().dispatch([] -> void { setEnabled(true); });
}
LOGGER.info("bootSplashInit dispatch end");
});
}
@@ -926,11 +926,6 @@ public:
// We want to try and scan more often in case of startup or scan lock failure
wifi_singleton->autoConnectTimer->start();
if (settings::shouldEnableOnBoot()) {
LOGGER.info("Auto-enabling due to setting");
getMainDispatcher().dispatch([] { dispatchEnable(wifi_singleton); });
}
return true;
}
+22 -18
View File
@@ -21,14 +21,14 @@ static WifiSettings cachedSettings {
static bool cached = false;
static bool load(WifiSettings& settings) {
auto service_context = findServiceContext();
if (service_context == nullptr) {
return false;
}
static bool hasWifiSettingsFile(std::shared_ptr<ServiceContext> context) {
std::string settings_path = context->getPaths()->getUserDataPath("settings.properties");
return file::isFile(settings_path);
}
static bool load(std::shared_ptr<ServiceContext> context, WifiSettings& settings) {
std::map<std::string, std::string> map;
std::string settings_path = service_context->getPaths()->getUserDataPath("settings.properties");
std::string settings_path = context->getPaths()->getUserDataPath("settings.properties");
if (!file::loadPropertiesFile(settings_path, map)) {
return false;
}
@@ -42,23 +42,26 @@ static bool load(WifiSettings& settings) {
return true;
}
static bool save(const WifiSettings& settings) {
auto service_context = findServiceContext();
if (service_context == nullptr) {
return false;
}
static bool save(std::shared_ptr<ServiceContext> context, const WifiSettings& settings) {
std::map<std::string, std::string> map;
map[SETTINGS_KEY_ENABLE_ON_BOOT] = settings.enableOnBoot ? "true" : "false";
std::string settings_path = service_context->getPaths()->getUserDataPath("settings.properties");
std::string settings_path = context->getPaths()->getUserDataPath("settings.properties");
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
LOGGER.error("Failed to create {}", settings_path);
return false;
}
return file::savePropertiesFile(settings_path, map);
}
WifiSettings getCachedOrLoad() {
if (!cached) {
if (!load(cachedSettings)) {
LOGGER.error("Failed to load");
} else {
cached = true;
auto context = findServiceContext();
if (context && hasWifiSettingsFile(context)) {
if (load(context, cachedSettings)) {
cached = true;
} else {
LOGGER.info("Failed to load settings, using defaults");
}
}
}
@@ -67,8 +70,9 @@ WifiSettings getCachedOrLoad() {
void setEnableOnBoot(bool enable) {
cachedSettings.enableOnBoot = enable;
if (!save(cachedSettings)) {
LOGGER.error("Failed to save");
auto context = findServiceContext();
if (context && !save(context, cachedSettings)) {
LOGGER.error("Failed to save settings");
}
}