Require SD card or >4MB flash (#545)

This commit is contained in:
Ken Van Hoeylandt
2026-07-03 23:56:03 +02:00
committed by GitHub
parent 90afba647e
commit 05720821f8
106 changed files with 403 additions and 603 deletions
+33 -18
View File
@@ -4,6 +4,9 @@
#include <Tactility/MountPoints.h>
#include <format>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/sdcard.h>
#include <tactility/filesystem/file_system.h>
namespace tt {
@@ -20,14 +23,12 @@ bool findFirstMountedSdCardPath(std::string& path) {
FileSystem* findSdcardFileSystem(bool mustBeMounted) {
FileSystem* found = nullptr;
file_system_for_each(&found, [](auto* fs, void* context) {
char path[128];
if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true;
// TODO: Find a better way to identify SD card paths
if (std::string(path).starts_with("/sdcard")) {
*static_cast<FileSystem**>(context) = fs;
return false;
auto* owner = file_system_get_owner(fs);
if (owner == nullptr || device_get_type(owner) != &SDCARD_TYPE) {
return true;
}
return true;
*static_cast<FileSystem**>(context) = fs;
return false;
});
if (found && mustBeMounted && !file_system_is_mounted(found)) {
return nullptr;
@@ -35,24 +36,38 @@ FileSystem* findSdcardFileSystem(bool mustBeMounted) {
return found;
}
std::string getSystemRootPath() {
std::string root_path;
if (!findFirstMountedSdCardPath(root_path)) {
root_path = file::MOUNT_POINT_DATA;
}
return root_path;
std::string getUserDataRootPath() {
#ifdef CONFIG_TT_USER_DATA_LOCATION_INTERNAL
return file::MOUNT_POINT_DATA;
#elif CONFIG_TT_USER_DATA_LOCATION_SD
auto* fs = findSdcardFileSystem(false);
check(fs);
char fs_path[32];
check(file_system_get_path(fs, fs_path, sizeof(fs_path)) == ERROR_NONE);
return std::string(fs_path);
#else
#error CONFIG_TT_USER_DATA_* not set or unsupported
#endif
}
std::string getUserDataPath() {
#ifdef ESP_PLATFORM
return getUserDataRootPath() + "/tactility";
#else
return "data";
#endif
}
std::string getTempPath() {
return getSystemRootPath() + "/tmp";
return getUserDataPath() + "/tmp";
}
std::string getAppInstallPath() {
return getSystemRootPath() + "/app";
return getUserDataPath() + "/app";
}
std::string getUserPath() {
return getSystemRootPath() + "/user";
std::string getUserHomePath() {
return getUserDataPath() + "/user";
}
std::string getAppInstallPath(const std::string& appId) {
@@ -62,7 +77,7 @@ std::string getAppInstallPath(const std::string& appId) {
std::string getAppUserPath(const std::string& appId) {
assert(app::isValidId(appId));
return std::format("{}/app/{}", getUserPath(), appId);
return std::format("{}/app/{}", getUserHomePath(), appId);
}
}
+13 -14
View File
@@ -35,6 +35,9 @@
#include <Tactility/InitEsp.h>
#endif
#include "Tactility/Paths.h"
#include <Tactility/bluetooth/Bluetooth.h>
namespace tt {
@@ -242,7 +245,7 @@ static void registerInstalledAppsFromFileSystems() {
if (!file_system_is_mounted(fs)) return true;
char path[128];
if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true;
const auto app_path = std::format("{}/app", path);
const auto app_path = std::format("{}/tactility/app", path);
if (!app_path.starts_with(file::MOUNT_POINT_SYSTEM) && file::isDirectory(app_path)) {
LOGGER.info("Registering apps from {}", app_path);
registerInstalledApps(app_path);
@@ -284,18 +287,21 @@ static void registerAndStartPrimaryServices() {
#endif
}
void createTempDirectory(const std::string& rootPath) {
auto temp_path = std::format("{}/tmp", rootPath);
void createTempDirectory() {
auto data_path = getUserDataPath();
auto temp_path = std::format("{}/tmp", data_path);
if (!file::isDirectory(temp_path)) {
auto lock = file::getLock(rootPath)->asScopedLock();
auto lock = file::getLock(data_path)->asScopedLock();
if (lock.lock(1000 / portTICK_PERIOD_MS)) {
if (mkdir(temp_path.c_str(), 0777) == 0) {
if (!file::findOrCreateParentDirectory(temp_path, 0777)) {
LOGGER.error("Failed to create {}", data_path);
} else if (mkdir(temp_path.c_str(), 0777) == 0) {
LOGGER.info("Created {}", temp_path);
} else {
LOGGER.error("Failed to create {}", temp_path);
}
} else {
LOGGER.error(LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, rootPath);
LOGGER.error(LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, data_path);
}
} else {
LOGGER.info("Found existing {}", temp_path);
@@ -303,14 +309,7 @@ void createTempDirectory(const std::string& rootPath) {
}
void prepareFileSystems() {
file_system_for_each(nullptr, [](auto* fs, void* context) {
if (!file_system_is_mounted(fs)) return true;
char path[128];
if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true;
if (std::string(path) == file::MOUNT_POINT_SYSTEM) return true;
createTempDirectory(path);
return true;
});
createTempDirectory();
}
void registerApps() {
+28 -1
View File
@@ -2,6 +2,7 @@
#include <Tactility/TactilityCore.h>
#include <Tactility/TactilityPrivate.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppPaths.h>
#include <Tactility/CpuAffinity.h>
@@ -10,6 +11,7 @@
#include <Tactility/kernel/SystemEvents.h>
#include <Tactility/Logger.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/Paths.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/settings/BootSettings.h>
#include <Tactility/settings/DisplaySettings.h>
@@ -44,6 +46,10 @@ class BootApp : public App {
// onShow() reads this instead of the live flag to avoid a race between the two.
static std::atomic<bool> isUsbBootSplash;
// Set by bootThreadCallback() when CONFIG_TT_USER_DATA_LOCATION_SD is defined but no SD card is mounted.
// onShow() reads this to show an error instead of the normal splash, and boot halts instead of starting the launcher.
static std::atomic<bool> sdCardMissing;
Thread thread = Thread(
"boot",
5120,
@@ -122,11 +128,20 @@ class BootApp : public App {
setupDisplay(); // Set backlight
prepareFileSystems();
#ifdef CONFIG_TT_USER_DATA_LOCATION_SD
std::string sd_path;
if (!findFirstMountedSdCardPath(sd_path)) {
LOGGER.error("SD card not found");
sdCardMissing = true;
}
#endif
if (!setupUsbBootMode()) {
LOGGER.info("initFromBootApp");
registerApps();
waitForMinimalSplashDuration(start_time);
stop(manifest.appId);
// When SD card is missing, wait for dialog result
if (!sdCardMissing) stop(manifest.appId);
startNextApp();
}
@@ -162,6 +177,11 @@ class BootApp : public App {
}
static void startNextApp() {
if (sdCardMissing) {
alertdialog::start("Error", "SD card not found.\nPlease insert one and reboot.", std::vector<const char*> { "Reboot" });
return;
}
#ifdef ESP_PLATFORM
if (esp_reset_reason() == ESP_RST_PANIC) {
crashdiagnostics::start();
@@ -195,6 +215,12 @@ public:
thread.join();
}
void onResult(AppContext& /*app*/, LaunchId /*launchId*/, Result /*result*/, std::unique_ptr<Bundle> /*bundle*/) override {
#ifdef ESP_PLATFORM
esp_restart();
#endif
}
void onShow(AppContext& app, lv_obj_t* parent) override {
lvgl::obj_set_style_bg_blacken(parent);
lv_obj_set_style_border_width(parent, 0, LV_STATE_DEFAULT);
@@ -232,6 +258,7 @@ public:
};
std::atomic<bool> BootApp::isUsbBootSplash = false;
std::atomic<bool> BootApp::sdCardMissing = false;
extern const AppManifest manifest = {
.appId = "Boot",
+8 -3
View File
@@ -10,6 +10,7 @@
#include <Tactility/crypt/Crypt.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Logger.h>
#include <Tactility/Paths.h>
#include <esp_random.h>
@@ -24,6 +25,10 @@ namespace tt::app::chat {
static const auto LOGGER = Logger("ChatSettings");
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/chat.properties";
}
constexpr auto* KEY_SENDER_ID = "senderId";
constexpr auto* KEY_NICKNAME = "nickname";
constexpr auto* KEY_ENCRYPTION_KEY = "encryptionKey";
@@ -120,7 +125,7 @@ ChatSettingsData loadSettings() {
ChatSettingsData settings = getDefaultSettings();
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(CHAT_SETTINGS_FILE, map)) {
if (!file::loadPropertiesFile(getSettingsFilePath(), map)) {
settings.senderId = generateSenderId();
return settings;
}
@@ -171,11 +176,11 @@ bool saveSettings(const ChatSettingsData& settings) {
map[KEY_ENCRYPTION_KEY] = "";
}
return file::savePropertiesFile(CHAT_SETTINGS_FILE, map);
return file::savePropertiesFile(getSettingsFilePath(), map);
}
bool settingsFileExists() {
return access(CHAT_SETTINGS_FILE, F_OK) == 0;
return access(getSettingsFilePath().c_str(), F_OK) == 0;
}
} // namespace tt::app::chat
@@ -29,7 +29,6 @@ class WebServerSettingsApp final : public App {
settings::webserver::WebServerSettings originalSettings;
bool updated = false;
bool wifiSettingsChanged = false;
bool webServerEnabledChanged = false;
lv_obj_t* dropdownWifiMode = nullptr;
lv_obj_t* textAreaApPassword = nullptr;
lv_obj_t* switchApOpenNetwork = nullptr;
@@ -61,11 +60,19 @@ class WebServerSettingsApp final : public App {
getMainDispatcher().dispatch([app, enabled] {
app->wsSettings.webServerEnabled = enabled;
app->updated = true;
app->webServerEnabledChanged = true;
if (lvgl::lock(100)) {
app->updateUrlDisplay();
lvgl::unlock();
}
// Apply immediately instead of waiting for app exit
const auto copy = app->wsSettings;
if (!settings::webserver::save(copy)) {
LOGGER.warn("Failed to persist WebServer settings; changes may be lost on reboot");
}
service::webserver::getPubsub()->publish(service::webserver::WebServerEvent::WebServerSettingsChanged);
LOGGER.info("WebServer {}", enabled ? "enabling..." : "disabling...");
service::webserver::setWebServerEnabled(enabled);
});
}
@@ -131,30 +138,6 @@ class WebServerSettingsApp final : public App {
});
}
static void onSyncAssets(lv_event_t* e) {
auto* app = static_cast<WebServerSettingsApp*>(lv_event_get_user_data(e));
auto* btn = static_cast<lv_obj_t*>(lv_event_get_target_obj(e));
lv_obj_add_state(btn, LV_STATE_DISABLED);
LOGGER.info("Manual asset sync triggered");
getMainDispatcher().dispatch([app, btn]{
bool success = service::webserver::syncAssets();
if (success) {
LOGGER.info("Asset sync completed successfully");
} else {
LOGGER.error("Asset sync failed");
}
// Only re-enable if button still exists (user hasn't navigated away)
// Must acquire LVGL lock since we're not in an LVGL event callback context
if (lvgl::lock(1000)) {
if (lv_obj_is_valid(btn)) {
lv_obj_remove_state(btn, LV_STATE_DISABLED);
}
lvgl::unlock();
}
});
}
void updateUrlDisplay() {
if (!labelUrlValue) return;
@@ -197,6 +180,8 @@ class WebServerSettingsApp final : public App {
public:
void onCreate(AppContext& app) override {
wsSettings = settings::webserver::loadOrGetDefault();
// Reflect the server's actual running state, in case it differs from the persisted setting
wsSettings.webServerEnabled = service::webserver::isWebServerEnabled();
originalSettings = wsSettings;
}
@@ -341,32 +326,6 @@ public:
updateUrlDisplay();
// Sync Assets button
auto* sync_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(sync_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(sync_wrapper, 10, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(sync_wrapper, 1, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(sync_wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_flex_cross_place(sync_wrapper, LV_FLEX_ALIGN_START, 0);
auto* sync_label = lv_label_create(sync_wrapper);
lv_label_set_text(sync_label, "Asset Synchronization");
auto* sync_info = lv_label_create(sync_wrapper);
lv_label_set_long_mode(sync_info, LV_LABEL_LONG_WRAP);
lv_obj_set_width(sync_info, LV_PCT(95));
if (lv_display_get_color_format(lv_obj_get_display(parent)) != LV_COLOR_FORMAT_L8) {
lv_obj_set_style_text_color(sync_info, lv_palette_main(LV_PALETTE_GREY), 0);
}
lv_label_set_text(sync_info, "Sync web assets between Data partition and SD card backup");
auto* sync_button = lv_btn_create(sync_wrapper);
lv_obj_set_width(sync_button, LV_SIZE_CONTENT);
auto* sync_button_label = lv_label_create(sync_button);
lv_label_set_text(sync_button_label, "Sync Assets Now");
lv_obj_center(sync_button_label);
lv_obj_add_event_cb(sync_button, onSyncAssets, LV_EVENT_CLICKED, this);
// Info text
auto* info_label = lv_label_create(main_wrapper);
lv_label_set_long_mode(info_label, LV_LABEL_LONG_WRAP);
@@ -394,11 +353,11 @@ public:
}
// Save to flash only (settings sync at boot handles SD restore)
// Note: the enable/disable toggle already saved and applied itself immediately
const auto copy = wsSettings;
const bool wifiChanged = wifiSettingsChanged;
const bool webServerChanged = webServerEnabledChanged;
getMainDispatcher().dispatch([copy, wifiChanged, webServerChanged]{
getMainDispatcher().dispatch([copy, wifiChanged]{
// Save to flash (fast, low memory pressure)
if (!settings::webserver::save(copy)) {
LOGGER.warn("Failed to persist WebServer settings; changes may be lost on reboot");
@@ -411,12 +370,6 @@ public:
if (wifiChanged) {
LOGGER.info("WiFi mode changed to {}", copy.wifiMode == settings::webserver::WiFiMode::AccessPoint ? "AP" : "Station");
}
// Control WebServer service immediately
if (webServerChanged) {
LOGGER.info("WebServer {}", copy.webServerEnabled ? "enabling..." : "disabling...");
service::webserver::setWebServerEnabled(copy.webServerEnabled);
}
});
}
}
@@ -4,13 +4,16 @@
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Logger.h>
#include <Tactility/Mutex.h>
#include <Tactility/Paths.h>
namespace tt::bluetooth::settings {
static const auto LOGGER = Logger("BluetoothSettings");
// Use the same path as the old service so existing settings survive migration.
constexpr auto* SETTINGS_PATH = "/data/service/bluetooth/settings.properties";
static std::string getSettingsPath() {
return getUserDataPath() + "/settings/bluetooth.settings";
}
constexpr auto* KEY_ENABLE_ON_BOOT = "enableOnBoot";
constexpr auto* KEY_SPP_AUTO_START = "sppAutoStart";
constexpr auto* KEY_MIDI_AUTO_START = "midiAutoStart";
@@ -27,7 +30,7 @@ static bool cached_valid = false;
static bool load(BluetoothSettings& out) {
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_PATH, map)) {
if (!file::loadPropertiesFile(getSettingsPath(), map)) {
return false;
}
auto it = map.find(KEY_ENABLE_ON_BOOT);
@@ -44,11 +47,11 @@ static bool load(BluetoothSettings& out) {
static bool save(const BluetoothSettings& s) {
std::map<std::string, std::string> map;
file::loadPropertiesFile(SETTINGS_PATH, map); // ignore failure — may not exist yet
file::loadPropertiesFile(getSettingsPath(), map); // ignore failure — may not exist yet
map[KEY_ENABLE_ON_BOOT] = s.enableOnBoot ? "true" : "false";
map[KEY_SPP_AUTO_START] = s.sppAutoStart ? "true" : "false";
map[KEY_MIDI_AUTO_START] = s.midiAutoStart ? "true" : "false";
return file::savePropertiesFile(SETTINGS_PATH, map);
return file::savePropertiesFile(getSettingsPath(), map);
}
static BluetoothSettings getCachedOrLoad() {
+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,7 @@
#ifdef ESP_PLATFORM
#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 +10,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 {
@@ -18,7 +22,7 @@ struct DevelopmentSettings {
static bool load(DevelopmentSettings& settings) {
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
if (!file::loadPropertiesFile(getSettingsFilePath(), map)) {
return false;
}
@@ -34,13 +38,13 @@ 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);
return file::savePropertiesFile(getSettingsFilePath(), 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());
}
}
@@ -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);
@@ -207,11 +207,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;
{
@@ -619,7 +614,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
@@ -990,7 +985,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");
@@ -1460,10 +1454,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 = getUserDataRootPath();
// Find next available filename with incrementing number
std::string screenshot_path;
@@ -1683,25 +1674,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
@@ -1724,7 +1699,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");
@@ -1767,11 +1742,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");
}
@@ -1854,6 +1827,11 @@ void setWebServerEnabled(bool enabled) {
}
}
bool isWebServerEnabled() {
WebServerService* instance = g_webServerInstance.load();
return instance != nullptr && instance->isEnabled();
}
} // namespace
#endif // ESP_PLATFORM
@@ -181,6 +181,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;
@@ -124,16 +124,9 @@ void bootSplashInit() {
getMainDispatcher().dispatch([] {
LOGGER.info("bootSplashInit dispatch begin");
// First import any provisioning files placed on the system data partition.
const std::string data_settings_path = file::getChildPath(file::MOUNT_POINT_DATA, "settings");
const std::string data_settings_path = file::getChildPath(getUserDataPath(), "provisioning");
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);
}
if (settings::shouldEnableOnBoot()) {
LOGGER.info("Auto-enabling due to setting");
getMainDispatcher().dispatch([] -> void { setEnabled(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");
}
}
+1 -9
View File
@@ -7,7 +7,6 @@
#include <Tactility/Paths.h>
#include <format>
#include <string>
#include <vector>
namespace tt::settings {
@@ -18,14 +17,7 @@ constexpr auto* PROPERTIES_KEY_LAUNCHER_APP_ID = "launcherAppId";
constexpr auto* PROPERTIES_KEY_AUTO_START_APP_ID = "autoStartAppId";
static std::string getPropertiesFilePath() {
std::string sdcard_path;
if (findFirstMountedSdCardPath(sdcard_path)) {
std::string path = std::format(PROPERTIES_FILE_FORMAT, sdcard_path);
if (file::isFile(path)) {
return path;
}
}
return std::format(PROPERTIES_FILE_FORMAT, file::MOUNT_POINT_DATA);
return std::format(PROPERTIES_FILE_FORMAT, getUserDataPath());
}
bool loadBootSettings(BootSettings& properties) {
@@ -1,6 +1,7 @@
#include <Tactility/settings/DisplaySettings.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Paths.h>
#include <tactility/hal/Device.h>
#include <Tactility/hal/display/DisplayDevice.h>
@@ -10,7 +11,10 @@
namespace tt::settings::display {
constexpr auto* SETTINGS_FILE = "/data/settings/display.properties";
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/display.properties";
}
constexpr auto* SETTINGS_KEY_ORIENTATION = "orientation";
constexpr auto* SETTINGS_KEY_GAMMA_CURVE = "gammaCurve";
constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty";
@@ -106,7 +110,7 @@ static bool fromString(const std::string& str, ScreensaverType& type) {
bool load(DisplaySettings& settings) {
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
if (!file::loadPropertiesFile(getSettingsFilePath(), map)) {
return false;
}
@@ -186,7 +190,7 @@ bool save(const DisplaySettings& settings) {
map[SETTINGS_KEY_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0";
map[SETTINGS_KEY_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs);
map[SETTINGS_KEY_SCREENSAVER_TYPE] = toString(settings.screensaverType);
return file::savePropertiesFile(SETTINGS_FILE, map);
return file::savePropertiesFile(getSettingsFilePath(), map);
}
lv_display_rotation_t toLvglDisplayRotation(Orientation orientation) {
@@ -1,12 +1,16 @@
#include <Tactility/settings/KeyboardSettings.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Paths.h>
#include <map>
#include <string>
namespace tt::settings::keyboard {
constexpr auto* SETTINGS_FILE = "/data/settings/keyboard.properties";
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/keyboard.properties";
}
constexpr auto* KEY_BACKLIGHT_ENABLED = "backlightEnabled";
constexpr auto* KEY_BACKLIGHT_BRIGHTNESS = "backlightBrightness";
constexpr auto* KEY_BACKLIGHT_TIMEOUT_ENABLED = "backlightTimeoutEnabled";
@@ -14,7 +18,7 @@ constexpr auto* KEY_BACKLIGHT_TIMEOUT_MS = "backlightTimeoutMs";
bool load(KeyboardSettings& settings) {
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
if (!file::loadPropertiesFile(getSettingsFilePath(), map)) {
return false;
}
@@ -54,7 +58,7 @@ bool save(const KeyboardSettings& settings) {
map[KEY_BACKLIGHT_BRIGHTNESS] = std::to_string(settings.backlightBrightness);
map[KEY_BACKLIGHT_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0";
map[KEY_BACKLIGHT_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs);
return file::savePropertiesFile(SETTINGS_FILE, map);
return file::savePropertiesFile(getSettingsFilePath(), map);
}
}
+15 -8
View File
@@ -1,11 +1,13 @@
#include <Tactility/Logger.h>
#include <Tactility/MountPoints.h>
#include <Tactility/Mutex.h>
#include <Tactility/file/File.h>
#include <Tactility/file/FileLock.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/settings/Language.h>
#include <Tactility/settings/SystemSettings.h>
#include "Tactility/Paths.h"
#include <format>
namespace tt::settings {
@@ -17,12 +19,16 @@ constexpr auto* FILE_PATH_FORMAT = "{}/settings/system.properties";
static bool cached = false;
static SystemSettings cachedSettings;
static bool hasSystemSettingsFile() {
auto file_path = std::format(FILE_PATH_FORMAT, getUserDataPath());
return file::isFile(file_path);
}
static bool loadSystemSettingsFromFile(SystemSettings& properties) {
auto file_path = std::format(FILE_PATH_FORMAT, file::MOUNT_POINT_DATA);
auto file_path = std::format(FILE_PATH_FORMAT, getUserDataPath());
LOGGER.info("System settings loading from {}", file_path);
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(file_path, map)) {
LOGGER.error("Failed to load {}", file_path);
return false;
}
@@ -55,11 +61,12 @@ static bool loadSystemSettingsFromFile(SystemSettings& properties) {
}
bool loadSystemSettings(SystemSettings& properties) {
if (!cached) {
if (!loadSystemSettingsFromFile(cachedSettings)) {
return false;
if (!cached && hasSystemSettingsFile()) {
if (loadSystemSettingsFromFile(cachedSettings)) {
cached = true;
} else {
LOGGER.error("Failed to load");
}
cached = true;
}
properties = cachedSettings;
@@ -67,7 +74,7 @@ bool loadSystemSettings(SystemSettings& properties) {
}
bool saveSystemSettings(const SystemSettings& properties) {
auto file_path = std::format(FILE_PATH_FORMAT, file::MOUNT_POINT_DATA);
auto file_path = std::format(FILE_PATH_FORMAT, getUserDataPath());
std::map<std::string, std::string> map;
map["language"] = toString(properties.language);
map["timeFormat24h"] = properties.timeFormat24h ? "true" : "false";
@@ -2,6 +2,7 @@
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Mutex.h>
#include <Tactility/Paths.h>
#include <algorithm>
#include <cstdlib>
@@ -12,7 +13,10 @@
namespace tt::settings::touch {
constexpr auto* SETTINGS_FILE = "/data/settings/touch-calibration.properties";
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/touch-calibration.properties";
}
constexpr auto* SETTINGS_KEY_ENABLED = "enabled";
constexpr auto* SETTINGS_KEY_X_MIN = "xMin";
constexpr auto* SETTINGS_KEY_X_MAX = "xMax";
@@ -61,7 +65,7 @@ bool isValid(const TouchCalibrationSettings& settings) {
bool load(TouchCalibrationSettings& settings) {
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
if (!file::loadPropertiesFile(getSettingsFilePath(), map)) {
return false;
}
@@ -112,7 +116,7 @@ bool save(const TouchCalibrationSettings& settings) {
map[SETTINGS_KEY_Y_MIN] = std::to_string(settings.yMin);
map[SETTINGS_KEY_Y_MAX] = std::to_string(settings.yMax);
if (!file::savePropertiesFile(SETTINGS_FILE, map)) {
if (!file::savePropertiesFile(getSettingsFilePath(), map)) {
return false;
}
@@ -1,5 +1,6 @@
#include <Tactility/settings/TrackballSettings.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Paths.h>
#include <map>
#include <string>
@@ -7,7 +8,10 @@
namespace tt::settings::trackball {
constexpr auto* SETTINGS_FILE = "/data/settings/trackball.properties";
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/trackball.properties";
}
constexpr auto* KEY_TRACKBALL_ENABLED = "trackballEnabled";
constexpr auto* KEY_TRACKBALL_MODE = "trackballMode";
constexpr auto* KEY_ENCODER_SENSITIVITY = "encoderSensitivity";
@@ -20,7 +24,7 @@ constexpr uint8_t MAX_POINTER_SENSITIVITY = 10;
bool load(TrackballSettings& settings) {
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
if (!file::loadPropertiesFile(getSettingsFilePath(), map)) {
return false;
}
@@ -80,7 +84,7 @@ bool save(const TrackballSettings& settings) {
map[KEY_TRACKBALL_MODE] = (settings.trackballMode == TrackballMode::Pointer) ? "1" : "0";
map[KEY_ENCODER_SENSITIVITY] = std::to_string(std::clamp(settings.encoderSensitivity, MIN_ENCODER_SENSITIVITY, MAX_ENCODER_SENSITIVITY));
map[KEY_POINTER_SENSITIVITY] = std::to_string(std::clamp(settings.pointerSensitivity, MIN_POINTER_SENSITIVITY, MAX_POINTER_SENSITIVITY));
return file::savePropertiesFile(SETTINGS_FILE, map);
return file::savePropertiesFile(getSettingsFilePath(), map);
}
}
@@ -2,6 +2,7 @@
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/file/File.h>
#include <Tactility/Logger.h>
#include <Tactility/Paths.h>
#include <charconv>
#include <map>
@@ -17,8 +18,11 @@
namespace tt::settings::webserver {
static const auto LOGGER = tt::Logger("WebServerSettings");
constexpr auto* SETTINGS_FILE = "/data/service/webserver/settings.properties";
static const auto LOGGER = Logger("WebServerSettings");
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/webserver.properties";
}
// Property keys
constexpr auto* KEY_WIFI_ENABLED = "wifiEnabled";
@@ -87,7 +91,7 @@ static bool isEmptyCredential(const std::string& value) {
bool load(WebServerSettings& settings) {
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
if (!file::loadPropertiesFile(getSettingsFilePath(), map)) {
return false;
}
@@ -146,7 +150,7 @@ bool load(WebServerSettings& settings) {
// Persist the generated password immediately
map[KEY_AP_PASSWORD] = settings.apPassword;
if (file::savePropertiesFile(SETTINGS_FILE, map)) {
if (file::savePropertiesFile(getSettingsFilePath(), map)) {
LOGGER.info("Generated and saved new secure AP password");
} else {
LOGGER.error("Failed to save generated AP password");
@@ -187,7 +191,7 @@ bool load(WebServerSettings& settings) {
// We need to save these to the file so they're consistent across reboots
map[KEY_WEBSERVER_USERNAME] = settings.webServerUsername;
map[KEY_WEBSERVER_PASSWORD] = settings.webServerPassword;
if (file::savePropertiesFile(SETTINGS_FILE, map)) {
if (file::savePropertiesFile(getSettingsFilePath(), map)) {
LOGGER.info("Generated and saved new secure credentials");
} else {
LOGGER.error("Failed to save generated credentials - auth may be inconsistent across reboots");
@@ -253,8 +257,7 @@ bool save(const WebServerSettings& settings) {
map[KEY_WEBSERVER_USERNAME] = settings.webServerUsername;
map[KEY_WEBSERVER_PASSWORD] = settings.webServerPassword;
// Save to flash storage only (no SD backup - settings sync at boot handles restore)
return file::savePropertiesFile(SETTINGS_FILE, map);
return file::savePropertiesFile(getSettingsFilePath(), map);
}
}