Merge remote-tracking branch 'origin/main' into main
This commit is contained in:
@@ -48,6 +48,7 @@ FileSystemApi partition_fs_api = {
|
||||
// endregion file_system stub
|
||||
|
||||
static esp_err_t initNvsFlashSafely() {
|
||||
LOGGER.info("Init NVS");
|
||||
esp_err_t result = nvs_flash_init();
|
||||
if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
@@ -76,7 +77,7 @@ size_t getSectorSize() {
|
||||
#endif
|
||||
}
|
||||
|
||||
esp_err_t initPartitionsEsp() {
|
||||
bool initPartitionsEsp() {
|
||||
LOGGER.info("Init partitions");
|
||||
ESP_ERROR_CHECK(initNvsFlashSafely());
|
||||
|
||||
@@ -91,22 +92,25 @@ esp_err_t initPartitionsEsp() {
|
||||
auto system_result = esp_vfs_fat_spiflash_mount_ro("/system", "system", &mount_config);
|
||||
if (system_result != ESP_OK) {
|
||||
LOGGER.error("Failed to mount /system ({})", esp_err_to_name(system_result));
|
||||
} else {
|
||||
LOGGER.info("Mounted /system");
|
||||
static auto system_fs_data = PartitionFsData("/system");
|
||||
file_system_add(&partition_fs_api, &system_fs_data);
|
||||
return false;
|
||||
}
|
||||
LOGGER.info("Mounted /system");
|
||||
static auto system_fs_data = PartitionFsData("/system");
|
||||
file_system_add(&partition_fs_api, &system_fs_data);
|
||||
|
||||
#ifdef CONFIG_TT_USER_DATA_LOCATION_INTERNAL
|
||||
auto data_result = esp_vfs_fat_spiflash_mount_rw_wl("/data", "data", &mount_config, &data_wl_handle);
|
||||
if (data_result != ESP_OK) {
|
||||
LOGGER.error("Failed to mount /data ({})", esp_err_to_name(data_result));
|
||||
} else {
|
||||
LOGGER.info("Mounted /data");
|
||||
static auto data_fs_data = PartitionFsData("/data");
|
||||
file_system_add(&partition_fs_api, &data_fs_data);
|
||||
return false;
|
||||
}
|
||||
|
||||
return system_result == ESP_OK && data_result == ESP_OK;
|
||||
LOGGER.info("Mounted /data");
|
||||
static auto data_fs_data = PartitionFsData("/data");
|
||||
file_system_add(&partition_fs_api, &data_fs_data);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
+33
-18
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
#endif
|
||||
|
||||
#include <format>
|
||||
#include <map>
|
||||
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/TactilityConfig.h>
|
||||
@@ -17,7 +16,6 @@
|
||||
#include <Tactility/app/AppRegistration.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/FileLock.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/hal/HalPrivate.h>
|
||||
#include <Tactility/lvgl/LvglPrivate.h>
|
||||
#include <Tactility/network/NtpPrivate.h>
|
||||
@@ -37,6 +35,9 @@
|
||||
#include <Tactility/InitEsp.h>
|
||||
#endif
|
||||
|
||||
#include "Tactility/Paths.h"
|
||||
|
||||
|
||||
#include <Tactility/bluetooth/Bluetooth.h>
|
||||
|
||||
namespace tt {
|
||||
@@ -103,6 +104,7 @@ namespace app {
|
||||
namespace power { extern const AppManifest manifest; }
|
||||
namespace selectiondialog { extern const AppManifest manifest; }
|
||||
namespace settings { extern const AppManifest manifest; }
|
||||
namespace setup { extern const AppManifest manifest; }
|
||||
namespace systeminfo { extern const AppManifest manifest; }
|
||||
namespace timedatesettings { extern const AppManifest manifest; }
|
||||
namespace touchcalibration { extern const AppManifest manifest; }
|
||||
@@ -157,6 +159,7 @@ static void registerInternalApps() {
|
||||
addAppManifest(app::notes::manifest);
|
||||
addAppManifest(app::settings::manifest);
|
||||
addAppManifest(app::selectiondialog::manifest);
|
||||
addAppManifest(app::setup::manifest);
|
||||
addAppManifest(app::systeminfo::manifest);
|
||||
addAppManifest(app::timedatesettings::manifest);
|
||||
addAppManifest(app::touchcalibration::manifest);
|
||||
@@ -216,14 +219,8 @@ static void registerInstalledApp(std::string path) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> properties;
|
||||
if (!file::loadPropertiesFile(manifest_path, properties)) {
|
||||
LOGGER.error("Failed to load manifest at {}", manifest_path);
|
||||
return;
|
||||
}
|
||||
|
||||
app::AppManifest manifest;
|
||||
if (!app::parseManifest(properties, manifest)) {
|
||||
if (!app::parseManifest(manifest_path, manifest)) {
|
||||
LOGGER.error("Failed to parse manifest at {}", manifest_path);
|
||||
return;
|
||||
}
|
||||
@@ -250,7 +247,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);
|
||||
@@ -292,18 +289,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);
|
||||
@@ -311,14 +311,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() {
|
||||
|
||||
@@ -1,38 +1,25 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/Logger.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/check.h>
|
||||
|
||||
#include <Tactility/PartitionsEsp.h>
|
||||
#include <Tactility/TactilityCore.h>
|
||||
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
constexpr auto* TAG = "Tactility";
|
||||
|
||||
namespace tt {
|
||||
|
||||
static auto LOGGER = Logger("Tactility");
|
||||
|
||||
// Initialize NVS
|
||||
static void initNvs() {
|
||||
LOGGER.info("Init NVS");
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
LOGGER.info("NVS erasing");
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
}
|
||||
|
||||
static void initNetwork() {
|
||||
LOGGER.info("Init network");
|
||||
LOG_I(TAG, "Init network");
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
}
|
||||
|
||||
void initEsp() {
|
||||
initNvs();
|
||||
initPartitionsEsp();
|
||||
check(initPartitionsEsp(), "Failed to init partitions");
|
||||
initNetwork();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <Tactility/app/AppRegistration.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/FileLock.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <tactility/hal/Device.h>
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/Paths.h>
|
||||
@@ -136,15 +135,8 @@ bool install(const std::string& path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> properties;
|
||||
if (!file::loadPropertiesFile(manifest_path, properties)) {
|
||||
LOGGER.error("Failed to load manifest at {}", manifest_path);
|
||||
cleanupInstallDirectory(app_target_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
AppManifest manifest;
|
||||
if (!parseManifest(properties, manifest)) {
|
||||
if (!parseManifest(manifest_path, manifest)) {
|
||||
LOGGER.warn("Invalid manifest");
|
||||
cleanupInstallDirectory(app_target_path);
|
||||
return false;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#include <Tactility/app/AppManifestParsing.h>
|
||||
#include <Tactility/app/AppManifestParsingInternal.h>
|
||||
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <regex>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
@@ -12,7 +16,7 @@ constexpr bool validateString(const std::string& value, const std::function<bool
|
||||
return std::ranges::all_of(value, isValidChar);
|
||||
}
|
||||
|
||||
static bool getValueFromManifest(const std::map<std::string, std::string>& map, const std::string& key, std::string& output) {
|
||||
bool getValueFromManifest(const std::map<std::string, std::string>& map, const std::string& key, std::string& output) {
|
||||
const auto iterator = map.find(key);
|
||||
if (iterator == map.end()) {
|
||||
LOGGER.error("Failed to find {} in manifest", key);
|
||||
@@ -28,98 +32,62 @@ bool isValidId(const std::string& id) {
|
||||
});
|
||||
}
|
||||
|
||||
static bool isValidManifestVersion(const std::string& version) {
|
||||
bool isValidManifestVersion(const std::string& version) {
|
||||
return !version.empty() && validateString(version, [](const char c) {
|
||||
return std::isalnum(c) != 0 || c == '.';
|
||||
});
|
||||
}
|
||||
|
||||
static bool isValidAppVersionName(const std::string& version) {
|
||||
bool isValidAppVersionName(const std::string& version) {
|
||||
return !version.empty() && validateString(version, [](const char c) {
|
||||
return std::isalnum(c) != 0 || c == '.' || c == '-' || c == '_';
|
||||
});
|
||||
}
|
||||
|
||||
static bool isValidAppVersionCode(const std::string& version) {
|
||||
bool isValidAppVersionCode(const std::string& version) {
|
||||
return !version.empty() && validateString(version, [](const char c) {
|
||||
return std::isdigit(c) != 0;
|
||||
});
|
||||
}
|
||||
|
||||
static bool isValidName(const std::string& name) {
|
||||
bool isValidName(const std::string& name) {
|
||||
return name.size() >= 2 && validateString(name, [](const char c) {
|
||||
return std::isalnum(c) != 0 || c == ' ' || c == '-';
|
||||
});
|
||||
}
|
||||
|
||||
bool parseManifest(const std::map<std::string, std::string>& map, AppManifest& manifest) {
|
||||
LOGGER.info("Parsing manifest");
|
||||
/** The V1 format's first line is always the literal "[manifest]" section header; V2 files are flat from the first line onward. */
|
||||
static bool detectIsV1Format(const std::string& filePath) {
|
||||
std::string first_line;
|
||||
bool got_first_line = false;
|
||||
file::readLines(filePath, true, [&first_line, &got_first_line](const char* line) {
|
||||
if (!got_first_line) {
|
||||
first_line = string::trim(std::string(line), " \t\r\n");
|
||||
got_first_line = true;
|
||||
}
|
||||
});
|
||||
return first_line == "[manifest]";
|
||||
}
|
||||
|
||||
// [manifest]
|
||||
bool parseManifest(const std::string& filePath, AppManifest& manifest) {
|
||||
LOGGER.info("Parsing manifest {}", filePath);
|
||||
|
||||
std::string manifest_version;
|
||||
if (!getValueFromManifest(map, "[manifest]version", manifest_version)) {
|
||||
bool is_v1_format = detectIsV1Format(filePath);
|
||||
|
||||
std::map<std::string, std::string> properties;
|
||||
if (!file::loadPropertiesFile(filePath, properties)) {
|
||||
LOGGER.error("Failed to load manifest at {}", filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidManifestVersion(manifest_version)) {
|
||||
LOGGER.error("Invalid version");
|
||||
bool success = is_v1_format
|
||||
? parseManifestV1(properties, manifest)
|
||||
: parseManifestV2(properties, manifest);
|
||||
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// [app]
|
||||
|
||||
if (!getValueFromManifest(map, "[app]id", manifest.appId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidId(manifest.appId)) {
|
||||
LOGGER.error("Invalid app id");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "[app]name", manifest.appName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidName(manifest.appName)) {
|
||||
LOGGER.error("Invalid app name");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "[app]versionName", manifest.appVersionName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidAppVersionName(manifest.appVersionName)) {
|
||||
LOGGER.error("Invalid app version name");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string version_code_string;
|
||||
if (!getValueFromManifest(map, "[app]versionCode", version_code_string)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidAppVersionCode(version_code_string)) {
|
||||
LOGGER.error("Invalid app version code");
|
||||
return false;
|
||||
}
|
||||
|
||||
manifest.appVersionCode = std::stoull(version_code_string);
|
||||
|
||||
// [target]
|
||||
|
||||
if (!getValueFromManifest(map, "[target]sdk", manifest.targetSdk)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "[target]platforms", manifest.targetPlatforms)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Defaults
|
||||
|
||||
manifest.appCategory = Category::User;
|
||||
manifest.appLocation = Location::external("");
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#include <Tactility/app/AppManifestParsing.h>
|
||||
#include <Tactility/app/AppManifestParsingInternal.h>
|
||||
|
||||
#include <Tactility/Logger.h>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
static const auto LOGGER = Logger("AppManifestV1");
|
||||
|
||||
bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest& manifest) {
|
||||
// [manifest]
|
||||
|
||||
std::string manifest_version;
|
||||
if (!getValueFromManifest(map, "[manifest]version", manifest_version)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidManifestVersion(manifest_version)) {
|
||||
LOGGER.error("Invalid version");
|
||||
return false;
|
||||
}
|
||||
|
||||
// [app]
|
||||
|
||||
if (!getValueFromManifest(map, "[app]id", manifest.appId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidId(manifest.appId)) {
|
||||
LOGGER.error("Invalid app id");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "[app]name", manifest.appName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidName(manifest.appName)) {
|
||||
LOGGER.error("Invalid app name");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "[app]versionName", manifest.appVersionName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidAppVersionName(manifest.appVersionName)) {
|
||||
LOGGER.error("Invalid app version name");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string version_code_string;
|
||||
if (!getValueFromManifest(map, "[app]versionCode", version_code_string)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidAppVersionCode(version_code_string)) {
|
||||
LOGGER.error("Invalid app version code");
|
||||
return false;
|
||||
}
|
||||
|
||||
manifest.appVersionCode = std::stoull(version_code_string);
|
||||
|
||||
// [target]
|
||||
|
||||
if (!getValueFromManifest(map, "[target]sdk", manifest.targetSdk)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "[target]platforms", manifest.targetPlatforms)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#include <Tactility/app/AppManifestParsing.h>
|
||||
#include <Tactility/app/AppManifestParsingInternal.h>
|
||||
|
||||
#include <Tactility/Logger.h>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
static const auto LOGGER = Logger("AppManifestV2");
|
||||
|
||||
bool parseManifestV2(const std::map<std::string, std::string>& map, AppManifest& manifest) {
|
||||
// manifest
|
||||
|
||||
std::string manifest_version;
|
||||
if (!getValueFromManifest(map, "manifest.version", manifest_version)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidManifestVersion(manifest_version)) {
|
||||
LOGGER.error("Invalid version");
|
||||
return false;
|
||||
}
|
||||
|
||||
// app
|
||||
|
||||
if (!getValueFromManifest(map, "app.id", manifest.appId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidId(manifest.appId)) {
|
||||
LOGGER.error("Invalid app id");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "app.name", manifest.appName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidName(manifest.appName)) {
|
||||
LOGGER.error("Invalid app name");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "app.version.name", manifest.appVersionName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidAppVersionName(manifest.appVersionName)) {
|
||||
LOGGER.error("Invalid app version name");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string version_code_string;
|
||||
if (!getValueFromManifest(map, "app.version.code", version_code_string)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidAppVersionCode(version_code_string)) {
|
||||
LOGGER.error("Invalid app version code");
|
||||
return false;
|
||||
}
|
||||
|
||||
manifest.appVersionCode = std::stoull(version_code_string);
|
||||
|
||||
// target
|
||||
|
||||
if (!getValueFromManifest(map, "target.sdk", manifest.targetSdk)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "target.platforms", manifest.targetPlatforms)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -16,14 +16,21 @@ static const auto LOGGER = Logger("BtManage");
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
static void onBtToggled(bool enabled) {
|
||||
struct Device* dev = bluetooth::findFirstDevice();
|
||||
static void onBtToggled(bool requestOn) {
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
Device* dev = device_find_first_by_type(&BLUETOOTH_TYPE);
|
||||
if (!dev) return;
|
||||
bluetooth_set_radio_enabled(dev, enabled);
|
||||
bool radio_on = bluetooth::isRadioOnOrPending(dev);
|
||||
if (requestOn && !radio_on) {
|
||||
bluetooth::start(dev);
|
||||
} else if (!requestOn && radio_on) {
|
||||
bluetooth::stop(dev);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void onScanToggled(bool enabled) {
|
||||
struct Device* dev = bluetooth::findFirstDevice();
|
||||
Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE);
|
||||
if (!dev) return;
|
||||
if (enabled) {
|
||||
bluetooth_scan_start(dev);
|
||||
@@ -108,7 +115,7 @@ void BtManage::onBtEvent(const struct BtEvent& event) {
|
||||
case BT_EVENT_RADIO_STATE_CHANGED:
|
||||
if (event.radio_state == BT_RADIO_STATE_ON) {
|
||||
getState().updatePairedPeers();
|
||||
struct Device* dev = bluetooth::findFirstDevice();
|
||||
Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE);
|
||||
if (dev && !bluetooth_is_scanning(dev)) {
|
||||
bluetooth_scan_start(dev);
|
||||
}
|
||||
@@ -121,7 +128,7 @@ void BtManage::onBtEvent(const struct BtEvent& event) {
|
||||
requestViewUpdate();
|
||||
}
|
||||
|
||||
static void onKernelBtEvent(struct Device* /*device*/, void* context, struct BtEvent event) {
|
||||
static void onKernelBtEvent(Device* /*device*/, void* context, BtEvent event) {
|
||||
// BT event callbacks can fire from the NimBLE host task (e.g. DISCONNECT during
|
||||
// nimble_port_stop shutdown). Calling onBtEvent() synchronously from the NimBLE
|
||||
// task would block it on the LVGL mutex (held by the LVGL task waiting in
|
||||
@@ -137,7 +144,7 @@ void BtManage::onShow(AppContext& app, lv_obj_t* parent) {
|
||||
// Initialise state and view before subscribing to avoid incoming events
|
||||
// racing with state initialisation.
|
||||
state.setRadioState(bluetooth::getRadioState());
|
||||
struct Device* dev = bluetooth::findFirstDevice();
|
||||
Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE);
|
||||
state.setScanning(dev ? bluetooth_is_scanning(dev) : false);
|
||||
state.updateScanResults();
|
||||
state.updatePairedPeers();
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
namespace tt::app::btmanage {
|
||||
|
||||
static const auto LOGGER = Logger("BtManageView");
|
||||
|
||||
static void onEnableSwitchChanged(lv_event_t* event) {
|
||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
|
||||
@@ -49,7 +47,7 @@ static void onEnableOnBootParentClicked(lv_event_t* event) {
|
||||
|
||||
static void onScanButtonClicked(lv_event_t* event) {
|
||||
auto bt = std::static_pointer_cast<BtManage>(getCurrentApp());
|
||||
struct Device* dev = bluetooth::findFirstDevice();
|
||||
Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE);
|
||||
bool scanning = dev ? bluetooth_is_scanning(dev) : false;
|
||||
bt->getBindings().onScanToggled(!scanning);
|
||||
}
|
||||
@@ -57,7 +55,6 @@ static void onScanButtonClicked(lv_event_t* event) {
|
||||
// region Peer list callbacks
|
||||
|
||||
struct PeerListItemData {
|
||||
View* view;
|
||||
size_t index;
|
||||
bool isPaired;
|
||||
};
|
||||
@@ -102,7 +99,7 @@ void View::createPeerListItem(const bluetooth::PeerRecord& record, bool isPaired
|
||||
|
||||
auto* button = lv_list_add_button(peers_list, nullptr, label.c_str());
|
||||
|
||||
auto* item_data = new PeerListItemData { this, index, isPaired };
|
||||
auto* item_data = new PeerListItemData { index, isPaired };
|
||||
lv_obj_set_user_data(button, item_data);
|
||||
lv_obj_add_event_cb(button, onConnect, LV_EVENT_SHORT_CLICKED, item_data);
|
||||
lv_obj_add_event_cb(button, [](lv_event_t* e) {
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
#include <Tactility/app/btpeersettings/BtPeerSettings.h>
|
||||
|
||||
#include <Tactility/Logger.h>
|
||||
#include "tactility/device.h"
|
||||
|
||||
#include <Tactility/LogMessages.h>
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/app/App.h>
|
||||
#include <Tactility/app/AppContext.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/app/alertdialog/AlertDialog.h>
|
||||
#include <Tactility/bluetooth/Bluetooth.h>
|
||||
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/bluetooth/Bluetooth.h>
|
||||
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/drivers/bluetooth.h>
|
||||
|
||||
@@ -124,7 +126,7 @@ public:
|
||||
}
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
if (struct Device* dev = bluetooth::findFirstDevice()) {
|
||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||
bluetooth_add_event_callback(dev, this, onKernelBtEvent);
|
||||
}
|
||||
|
||||
@@ -192,7 +194,7 @@ public:
|
||||
}
|
||||
|
||||
void onHide(AppContext& app) override {
|
||||
if (struct Device* dev = bluetooth::findFirstDevice()) {
|
||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||
bluetooth_remove_event_callback(dev, onKernelBtEvent);
|
||||
}
|
||||
viewEnabled = false;
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
#include <Tactility/app/chat/ChatProtocol.h>
|
||||
|
||||
#include <Tactility/crypt/Crypt.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/Paths.h>
|
||||
|
||||
#include <esp_random.h>
|
||||
|
||||
@@ -24,11 +26,17 @@ 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";
|
||||
constexpr auto* KEY_CHAT_CHANNEL = "chatChannel";
|
||||
|
||||
uint32_t defaultSenderId = 0;
|
||||
|
||||
// IV_SEED provides basic obfuscation for stored encryption keys, not strong encryption.
|
||||
// The device master key (from crypt::getIv) provides the actual security.
|
||||
static constexpr auto* IV_SEED = "chat_key";
|
||||
@@ -107,8 +115,9 @@ static uint32_t generateSenderId() {
|
||||
}
|
||||
|
||||
ChatSettingsData getDefaultSettings() {
|
||||
if (defaultSenderId == 0) defaultSenderId = generateSenderId();
|
||||
return ChatSettingsData{
|
||||
.senderId = 0,
|
||||
.senderId = defaultSenderId,
|
||||
.nickname = "Device",
|
||||
.encryptionKey = {},
|
||||
.hasEncryptionKey = false,
|
||||
@@ -119,8 +128,14 @@ ChatSettingsData getDefaultSettings() {
|
||||
ChatSettingsData loadSettings() {
|
||||
ChatSettingsData settings = getDefaultSettings();
|
||||
|
||||
auto settings_path = getSettingsFilePath();
|
||||
if (!file::isFile(settings_path)) {
|
||||
settings.senderId = generateSenderId();
|
||||
return settings;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> map;
|
||||
if (!file::loadPropertiesFile(CHAT_SETTINGS_FILE, map)) {
|
||||
if (!file::loadPropertiesFile(settings_path, map)) {
|
||||
settings.senderId = generateSenderId();
|
||||
return settings;
|
||||
}
|
||||
@@ -171,11 +186,16 @@ bool saveSettings(const ChatSettingsData& settings) {
|
||||
map[KEY_ENCRYPTION_KEY] = "";
|
||||
}
|
||||
|
||||
return file::savePropertiesFile(CHAT_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);
|
||||
}
|
||||
|
||||
bool settingsFileExists() {
|
||||
return access(CHAT_SETTINGS_FILE, F_OK) == 0;
|
||||
return access(getSettingsFilePath().c_str(), F_OK) == 0;
|
||||
}
|
||||
|
||||
} // namespace tt::app::chat
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <Tactility/app/AppContext.h>
|
||||
#include <Tactility/app/AppPaths.h>
|
||||
#include <Tactility/app/AppRegistration.h>
|
||||
#include <Tactility/app/setup/Setup.h>
|
||||
#include <Tactility/hal/power/PowerDevice.h>
|
||||
#include <Tactility/service/loader/Loader.h>
|
||||
#include <Tactility/settings/BootSettings.h>
|
||||
@@ -137,22 +138,26 @@ public:
|
||||
|
||||
void onCreate(AppContext& app) override {
|
||||
settings::BootSettings boot_properties;
|
||||
if (settings::loadBootSettings(boot_properties)) {
|
||||
if (
|
||||
!boot_properties.autoStartAppId.empty() &&
|
||||
findAppManifestById(boot_properties.autoStartAppId) != nullptr
|
||||
) {
|
||||
LOGGER.info("Starting {}", boot_properties.autoStartAppId);
|
||||
start(boot_properties.autoStartAppId);
|
||||
} else {
|
||||
LOGGER.info("No auto-start app configured. Skipping default auto-start due to boot.properties presence.");
|
||||
}
|
||||
} else if (
|
||||
if (
|
||||
// Auto-start due to built-in requirement
|
||||
strcmp(CONFIG_TT_AUTO_START_APP_ID, "") != 0 &&
|
||||
findAppManifestById(CONFIG_TT_AUTO_START_APP_ID) != nullptr
|
||||
) {
|
||||
LOGGER.info("Starting {}", CONFIG_TT_AUTO_START_APP_ID);
|
||||
start(CONFIG_TT_AUTO_START_APP_ID);
|
||||
} else if (
|
||||
// Auto-start due to user configuration
|
||||
settings::loadBootSettings(boot_properties) &&
|
||||
!boot_properties.autoStartAppId.empty() &&
|
||||
findAppManifestById(boot_properties.autoStartAppId) != nullptr
|
||||
) {
|
||||
LOGGER.info("Starting {}", boot_properties.autoStartAppId);
|
||||
start(boot_properties.autoStartAppId);
|
||||
} else {
|
||||
// No auto-start, consider running system setup
|
||||
if (!setup::isCompleted()) {
|
||||
setup::start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ extern const AppManifest manifest;
|
||||
class LocaleSettingsApp final : public App {
|
||||
tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH);
|
||||
RecursiveMutex mutex;
|
||||
lv_obj_t* regionTextArea = nullptr;
|
||||
lv_obj_t* languageDropdown = nullptr;
|
||||
bool settingsUpdated = false;
|
||||
|
||||
@@ -98,33 +97,6 @@ public:
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
|
||||
// Region
|
||||
|
||||
auto* region_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(region_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(region_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(region_wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(region_wrapper, 0, 0);
|
||||
|
||||
auto* region_label = lv_label_create(region_wrapper);
|
||||
lv_label_set_text(region_label, textResources[i18n::Text::REGION].c_str());
|
||||
lv_obj_align(region_label, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
|
||||
// Region text area for user input (e.g., US, EU, JP)
|
||||
regionTextArea = lv_textarea_create(region_wrapper);
|
||||
lv_obj_set_width(regionTextArea, 120);
|
||||
lv_textarea_set_one_line(regionTextArea, true);
|
||||
lv_textarea_set_max_length(regionTextArea, 50);
|
||||
lv_textarea_set_placeholder_text(regionTextArea, "e.g. US, EU");
|
||||
|
||||
// Load current region from settings
|
||||
settings::SystemSettings sysSettings;
|
||||
if (settings::loadSystemSettings(sysSettings)) {
|
||||
lv_textarea_set_text(regionTextArea, sysSettings.region.c_str());
|
||||
}
|
||||
lv_obj_add_event_cb(regionTextArea, onRegionChanged, LV_EVENT_VALUE_CHANGED, this);
|
||||
lv_obj_align(regionTextArea, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
|
||||
// Language
|
||||
|
||||
auto* language_wrapper = lv_obj_create(main_wrapper);
|
||||
@@ -145,16 +117,6 @@ public:
|
||||
lv_dropdown_set_selected(languageDropdown, static_cast<uint32_t>(settings::getLanguage()));
|
||||
lv_obj_add_event_cb(languageDropdown, onLanguageSet, LV_EVENT_VALUE_CHANGED, this);
|
||||
}
|
||||
|
||||
void onHide(AppContext& app) override {
|
||||
if (settingsUpdated && regionTextArea) {
|
||||
settings::SystemSettings sysSettings;
|
||||
if (settings::loadSystemSettings(sysSettings)) {
|
||||
sysSettings.region = lv_textarea_get_text(regionTextArea);
|
||||
settings::saveSystemSettings(sysSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
#include <tactility/lvgl_fonts.h>
|
||||
#include <tactility/lvgl_module.h>
|
||||
|
||||
#include <Tactility/app/App.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/app/setup/Setup.h>
|
||||
|
||||
#include <Tactility/Preferences.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
#include <Tactility/app/timezone/TimeZone.h>
|
||||
#include <Tactility/app/wifimanage/WifiManage.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/service/wifi/Wifi.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::setup {
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
constexpr auto* PREFERENCES_NAMESPACE = "setup";
|
||||
constexpr auto* PREFERENCES_KEY_COMPLETED = "completed";
|
||||
|
||||
bool isCompleted() {
|
||||
Preferences preferences(PREFERENCES_NAMESPACE);
|
||||
bool completed = false;
|
||||
preferences.optBool(PREFERENCES_KEY_COMPLETED, completed);
|
||||
return completed;
|
||||
}
|
||||
|
||||
static void markCompleted() {
|
||||
Preferences preferences(PREFERENCES_NAMESPACE);
|
||||
preferences.putBool(PREFERENCES_KEY_COMPLETED, true);
|
||||
}
|
||||
|
||||
struct StepConfiguration {
|
||||
std::string title;
|
||||
std::string description;
|
||||
std::function<void()> run;
|
||||
};
|
||||
|
||||
class SetupApp final : public App {
|
||||
|
||||
enum class Phase {
|
||||
Welcome,
|
||||
StepIntro,
|
||||
Done
|
||||
};
|
||||
|
||||
Phase phase = Phase::Welcome;
|
||||
size_t stepIndex = 0;
|
||||
std::vector<StepConfiguration> steps;
|
||||
bool isShown = false;
|
||||
|
||||
lv_obj_t* titleLabel = nullptr;
|
||||
lv_obj_t* descriptionLabel = nullptr;
|
||||
lv_obj_t* skipButton = nullptr;
|
||||
lv_obj_t* continueButton = nullptr;
|
||||
|
||||
static void onSkipClickedCallback(lv_event_t* e) {
|
||||
auto* app = (SetupApp*)lv_event_get_user_data(e);
|
||||
app->onSkipClicked();
|
||||
}
|
||||
|
||||
static void onContinueClickedCallback(lv_event_t* e) {
|
||||
auto* app = (SetupApp*)lv_event_get_user_data(e);
|
||||
app->onContinueClicked();
|
||||
}
|
||||
|
||||
void renderCurrent() {
|
||||
switch (phase) {
|
||||
case Phase::Welcome: {
|
||||
lv_label_set_text(titleLabel, "Welcome");
|
||||
auto device_names = string::split(std::string(CONFIG_TT_DEVICE_NAME_SIMPLE), ",");
|
||||
lv_label_set_text_fmt(descriptionLabel, "It's time to set up your %s!", device_names.front().c_str());
|
||||
lv_obj_add_flag(skipButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_label_set_text(lv_obj_get_child(continueButton, 0), "Continue");
|
||||
break;
|
||||
}
|
||||
case Phase::StepIntro: {
|
||||
const auto& step = steps[stepIndex];
|
||||
lv_label_set_text(titleLabel, step.title.c_str());
|
||||
lv_label_set_text(descriptionLabel, step.description.c_str());
|
||||
lv_obj_remove_flag(skipButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_label_set_text(lv_obj_get_child(skipButton, 0), "Skip");
|
||||
lv_label_set_text(lv_obj_get_child(continueButton, 0), "Continue");
|
||||
break;
|
||||
}
|
||||
case Phase::Done:
|
||||
lv_label_set_text(titleLabel, "Setup Complete");
|
||||
lv_label_set_text(descriptionLabel, "You're all set.");
|
||||
lv_obj_add_flag(skipButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_label_set_text(lv_obj_get_child(continueButton, 0), "Finish");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void advanceTo(size_t index) {
|
||||
if (index < steps.size()) {
|
||||
stepIndex = index;
|
||||
phase = Phase::StepIntro;
|
||||
} else {
|
||||
phase = Phase::Done;
|
||||
}
|
||||
|
||||
// Widgets may not exist yet: onShow() runs asynchronously on the GUI task and
|
||||
// may not have (re)created them by the time onResult() advances the state.
|
||||
// onShow() calls renderCurrent() itself once the widgets are ready.
|
||||
if (isShown) {
|
||||
renderCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
void onSkipClicked() {
|
||||
if (phase == Phase::StepIntro) {
|
||||
advanceTo(stepIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void onContinueClicked() {
|
||||
switch (phase) {
|
||||
case Phase::Welcome:
|
||||
advanceTo(0);
|
||||
break;
|
||||
case Phase::StepIntro:
|
||||
steps[stepIndex].run();
|
||||
break;
|
||||
case Phase::Done:
|
||||
markCompleted();
|
||||
stop(manifest.appId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void onCreate(AppContext& app) override {
|
||||
steps = {
|
||||
{
|
||||
.title = "Time Zone Setup",
|
||||
.description = "Let's set the time zone.",
|
||||
.run = [] { timezone::start(true); }
|
||||
},
|
||||
{
|
||||
.title = "Wi-Fi Setup",
|
||||
.description = "Let's connect to a Wi-Fi access point.",
|
||||
.run = [] {
|
||||
service::wifi::setEnabled(true);
|
||||
wifimanage::start();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
titleLabel = lv_label_create(parent);
|
||||
lv_obj_set_width(titleLabel, LV_PCT(80));
|
||||
lv_obj_set_style_text_align(titleLabel, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_label_set_long_mode(titleLabel, LV_LABEL_LONG_WRAP);
|
||||
auto* font = lvgl_get_text_font(FONT_SIZE_LARGE);
|
||||
lv_obj_set_style_text_font(titleLabel, font, 0);
|
||||
|
||||
descriptionLabel = lv_label_create(parent);
|
||||
lv_obj_set_width(descriptionLabel, LV_PCT(80));
|
||||
lv_obj_set_style_text_align(descriptionLabel, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_label_set_long_mode(descriptionLabel, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_align(descriptionLabel, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
int title_margin = lvgl_get_text_font_height(FONT_SIZE_LARGE);
|
||||
lv_obj_align_to(titleLabel, descriptionLabel, LV_ALIGN_OUT_TOP_MID, 0, -title_margin);
|
||||
|
||||
skipButton = lv_button_create(parent);
|
||||
lv_obj_t* skip_label = lv_label_create(skipButton);
|
||||
lv_label_set_text(skip_label, "Skip");
|
||||
lv_obj_center(skip_label);
|
||||
lv_obj_align(skipButton, LV_ALIGN_BOTTOM_LEFT, 12, -12);
|
||||
lv_obj_add_event_cb(skipButton, onSkipClickedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
|
||||
continueButton = lv_button_create(parent);
|
||||
lv_obj_t* continue_label = lv_label_create(continueButton);
|
||||
lv_label_set_text(continue_label, "Continue");
|
||||
lv_obj_center(continue_label);
|
||||
lv_obj_align(continueButton, LV_ALIGN_BOTTOM_RIGHT, -12, -12);
|
||||
lv_obj_add_event_cb(continueButton, onContinueClickedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
|
||||
isShown = true;
|
||||
renderCurrent();
|
||||
}
|
||||
|
||||
void onHide(AppContext& app) override {
|
||||
isShown = false;
|
||||
}
|
||||
|
||||
void onResult(AppContext& app, LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||
lvgl_lock();
|
||||
advanceTo(stepIndex + 1);
|
||||
lvgl_unlock();
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.appId = "Setup",
|
||||
.appName = "Setup",
|
||||
.appCategory = Category::System,
|
||||
.appFlags = AppManifest::Flags::Hidden | AppManifest::Flags::HideStatusBar,
|
||||
.createApp = create<SetupApp>
|
||||
};
|
||||
|
||||
LaunchId start() {
|
||||
return app::start(manifest.appId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
#include "tactility/lvgl_module.h"
|
||||
|
||||
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/RecursiveMutex.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/app/timezone/TimeZone.h>
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/RecursiveMutex.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/service/loader/Loader.h>
|
||||
#include <Tactility/settings/Time.h>
|
||||
#include <Tactility/settings/SystemSettings.h>
|
||||
#include <Tactility/settings/Time.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
@@ -23,6 +26,7 @@ class TimeDateSettingsApp final : public App {
|
||||
RecursiveMutex mutex;
|
||||
lv_obj_t* timeZoneLabel = nullptr;
|
||||
lv_obj_t* dateFormatDropdown = nullptr;
|
||||
bool isShown = false;
|
||||
|
||||
static void onTimeFormatChanged(lv_event_t* event) {
|
||||
auto* widget = lv_event_get_target_obj(event);
|
||||
@@ -31,7 +35,7 @@ class TimeDateSettingsApp final : public App {
|
||||
}
|
||||
|
||||
static void onTimeZonePressed(lv_event_t* event) {
|
||||
timezone::start();
|
||||
timezone::start(true);
|
||||
}
|
||||
|
||||
static void onDateFormatChanged(lv_event_t* event) {
|
||||
@@ -133,6 +137,12 @@ public:
|
||||
}
|
||||
lv_obj_center(timeZoneLabel);
|
||||
lv_label_set_text(timeZoneLabel, timeZoneName.c_str());
|
||||
|
||||
isShown = true;
|
||||
}
|
||||
|
||||
void onHide(AppContext& app) override {
|
||||
isShown = false;
|
||||
}
|
||||
|
||||
void onResult(AppContext& app, LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||
@@ -140,13 +150,14 @@ public:
|
||||
const auto name = timezone::getResultName(*bundle);
|
||||
const auto code = timezone::getResultCode(*bundle);
|
||||
LOGGER.info("Result name={} code={}", name, code);
|
||||
settings::setTimeZone(name, code);
|
||||
|
||||
if (!name.empty()) {
|
||||
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
|
||||
// onShow() may not have (re)created the widgets yet: onResult() runs synchronously
|
||||
// on the loader thread and can race ahead of the async gui-task redraw.
|
||||
if (!name.empty() && lvgl_try_lock(100 / portTICK_PERIOD_MS)) {
|
||||
if (isShown) {
|
||||
lv_label_set_text(timeZoneLabel, name.c_str());
|
||||
lvgl::unlock();
|
||||
}
|
||||
lvgl_unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/service/loader/Loader.h>
|
||||
#include <Tactility/settings/Time.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <memory>
|
||||
@@ -23,6 +24,7 @@ static const auto LOGGER = Logger("TimeZone");
|
||||
|
||||
constexpr auto* RESULT_BUNDLE_CODE_INDEX = "code";
|
||||
constexpr auto* RESULT_BUNDLE_NAME_INDEX = "name";
|
||||
constexpr auto* PARAM_SAVE_TIME_ZONE = "saveTimeZone";
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
@@ -74,6 +76,7 @@ class TimeZoneApp final : public App {
|
||||
std::unique_ptr<Timer> updateTimer;
|
||||
lv_obj_t* listWidget = nullptr;
|
||||
lv_obj_t* filterTextareaWidget = nullptr;
|
||||
bool saveTimeZone = false;
|
||||
|
||||
static void onTextareaValueChangedCallback(lv_event_t* e) {
|
||||
auto* app = (TimeZoneApp*)lv_event_get_user_data(e);
|
||||
@@ -104,6 +107,10 @@ class TimeZoneApp final : public App {
|
||||
|
||||
auto& entry = entries[index];
|
||||
|
||||
if (saveTimeZone) {
|
||||
settings::setTimeZone(entry.name, entry.code);
|
||||
}
|
||||
|
||||
auto bundle = std::make_unique<Bundle>();
|
||||
setResultName(*bundle, entry.name);
|
||||
setResultCode(*bundle, entry.code);
|
||||
@@ -222,6 +229,11 @@ public:
|
||||
}
|
||||
|
||||
void onCreate(AppContext& app) override {
|
||||
auto parameters = app.getParameters();
|
||||
if (parameters != nullptr) {
|
||||
parameters->optBool(PARAM_SAVE_TIME_ZONE, saveTimeZone);
|
||||
}
|
||||
|
||||
updateTimer = std::make_unique<Timer>(Timer::Type::Once, 500 / portTICK_PERIOD_MS, [this] {
|
||||
updateList();
|
||||
});
|
||||
@@ -230,14 +242,16 @@ public:
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.appId = "TimeZone",
|
||||
.appName = "Select timezone",
|
||||
.appName = "Select Time zone",
|
||||
.appCategory = Category::System,
|
||||
.appFlags = AppManifest::Flags::Hidden,
|
||||
.createApp = create<TimeZoneApp>
|
||||
};
|
||||
|
||||
LaunchId start() {
|
||||
return app::start(manifest.appId);
|
||||
LaunchId start(bool saveTimeZone) {
|
||||
auto bundle = std::make_shared<Bundle>();
|
||||
bundle->putBool(PARAM_SAVE_TIME_ZONE, saveTimeZone);
|
||||
return app::start(manifest.appId, bundle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,18 +40,16 @@ static std::vector<CachedAddr> scan_addr_cache; // parallel to scan_results_cach
|
||||
|
||||
// ---- Device accessor ----
|
||||
|
||||
struct Device* findFirstDevice() {
|
||||
struct Device* found = nullptr;
|
||||
device_for_each_of_type(&BLUETOOTH_TYPE, &found, [](struct Device* dev, void* ctx) -> bool {
|
||||
if (device_is_ready(dev)) {
|
||||
*static_cast<struct Device**>(ctx) = dev;
|
||||
return false;
|
||||
}
|
||||
Device* findFirstRegisteredDevice() {
|
||||
Device* found = nullptr;
|
||||
device_for_each_of_type(&BLUETOOTH_TYPE, &found, [](Device* dev, void* ctx) -> bool {
|
||||
*static_cast<Device**>(ctx) = dev;
|
||||
return true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
|
||||
// ---- Scan cache helpers ----
|
||||
|
||||
void cacheScanAddr(const uint8_t addr[6], uint8_t addr_type) {
|
||||
@@ -110,7 +108,7 @@ static void cachePeerRecord(const BtPeerRecord& krecord) {
|
||||
// and settings management. Consumers should register their own callbacks via
|
||||
// bluetooth_add_event_callback() to receive events directly.
|
||||
|
||||
static void bt_event_bridge(struct Device* /*device*/, void* /*context*/, struct BtEvent event) {
|
||||
static void bt_event_bridge(Device*, void* /*context*/, BtEvent event) {
|
||||
switch (event.type) {
|
||||
case BT_EVENT_RADIO_STATE_CHANGED:
|
||||
switch (event.radio_state) {
|
||||
@@ -126,24 +124,24 @@ static void bt_event_bridge(struct Device* /*device*/, void* /*context*/, struct
|
||||
}
|
||||
if (has_hid_host_auto) {
|
||||
LOGGER.info("HID host auto-connect peer found — starting scan");
|
||||
if (struct Device* dev = findFirstDevice()) {
|
||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||
bluetooth_scan_start(dev);
|
||||
}
|
||||
} else if (has_hid_device_auto) {
|
||||
LOGGER.info("HID device auto-start (bonded peer found)");
|
||||
if (struct Device* dev = bluetooth_hid_device_get_device()) {
|
||||
if (Device* dev = bluetooth_hid_device_get_device()) {
|
||||
bluetooth_hid_device_start(dev, BT_HID_DEVICE_MODE_KEYBOARD);
|
||||
}
|
||||
} else {
|
||||
if (settings::shouldSppAutoStart()) {
|
||||
LOGGER.info("Auto-starting SPP server");
|
||||
if (struct Device* dev = bluetooth_serial_get_device()) {
|
||||
if (Device* dev = bluetooth_serial_get_device()) {
|
||||
bluetooth_serial_start(dev);
|
||||
}
|
||||
}
|
||||
if (settings::shouldMidiAutoStart()) {
|
||||
LOGGER.info("Auto-starting MIDI server");
|
||||
if (struct Device* dev = bluetooth_midi_get_device()) {
|
||||
if (Device* dev = bluetooth_midi_get_device()) {
|
||||
bluetooth_midi_start(dev);
|
||||
}
|
||||
}
|
||||
@@ -233,7 +231,7 @@ static void bt_event_bridge(struct Device* /*device*/, void* /*context*/, struct
|
||||
}
|
||||
}
|
||||
if (has_auto) {
|
||||
if (struct Device* dev = findFirstDevice()) {
|
||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||
if (!bluetooth_is_scanning(dev)) {
|
||||
bluetooth_scan_start(dev);
|
||||
}
|
||||
@@ -251,19 +249,80 @@ static void bt_event_bridge(struct Device* /*device*/, void* /*context*/, struct
|
||||
// ---- systemStart ----
|
||||
|
||||
void systemStart() {
|
||||
struct Device* dev = findFirstDevice();
|
||||
Device* dev = findFirstRegisteredDevice();
|
||||
if (dev == nullptr) {
|
||||
LOGGER.warn("systemStart: no BLE device found");
|
||||
return;
|
||||
}
|
||||
bluetooth_add_event_callback(dev, nullptr, bt_event_bridge);
|
||||
|
||||
if (settings::shouldEnableOnBoot()) {
|
||||
LOGGER.info("Auto-enabling Bluetooth on boot");
|
||||
bluetooth_set_radio_enabled(dev, true);
|
||||
start(dev);
|
||||
}
|
||||
}
|
||||
|
||||
bool isRadioOnOrPending(Device* dev) {
|
||||
if (!device_is_ready(dev)) return false;
|
||||
BtRadioState state;
|
||||
if (bluetooth_get_radio_state(dev, &state) != ERROR_NONE) return false;
|
||||
return state == BT_RADIO_STATE_ON || state == BT_RADIO_STATE_ON_PENDING;
|
||||
}
|
||||
|
||||
bool start(Device* dev) {
|
||||
LOGGER.info("Auto-enabling BLE on boot");
|
||||
if (!device_is_ready(dev)) {
|
||||
LOGGER.info("Starting BLE device");
|
||||
if (device_start(dev) != ERROR_NONE) {
|
||||
LOGGER.error("Failed to start BLE device");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Fix bug where repeatedly calling start would add this callback multiple times
|
||||
if (bluetooth_add_event_callback(dev, nullptr, bt_event_bridge) != ERROR_NONE) {
|
||||
LOGGER.error("Failed to set BLE callback");
|
||||
}
|
||||
|
||||
LOGGER.info("Enabling BT radio");
|
||||
if (bluetooth_set_radio_enabled(dev, true) != ERROR_NONE) {
|
||||
LOGGER.error("Failed to enable BLE radio");
|
||||
// Add bridge again
|
||||
bluetooth_remove_event_callback(dev, bt_event_bridge);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.info("BT enabled");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool stop(Device* dev) {
|
||||
BtRadioState state;
|
||||
if (bluetooth_get_radio_state(dev, &state) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state == BT_RADIO_STATE_OFF || state == BT_RADIO_STATE_OFF_PENDING) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (bluetooth_remove_event_callback(dev, bt_event_bridge) != ERROR_NONE) {
|
||||
LOGGER.error("Failed to remove BLE callback");
|
||||
}
|
||||
|
||||
if (bluetooth_set_radio_enabled(dev, false) != ERROR_NONE) {
|
||||
LOGGER.error("Failed to disable BT radio");
|
||||
// Re-register bridge
|
||||
bluetooth_add_event_callback(dev, nullptr, bt_event_bridge);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (device_stop(dev) != ERROR_NONE) {
|
||||
LOGGER.error("Failed to stop BT device");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Public API ----
|
||||
|
||||
const char* radioStateToString(RadioState state) {
|
||||
@@ -278,7 +337,7 @@ const char* radioStateToString(RadioState state) {
|
||||
}
|
||||
|
||||
RadioState getRadioState() {
|
||||
struct Device* dev = findFirstDevice();
|
||||
Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE);
|
||||
if (dev == nullptr) return RadioState::Off;
|
||||
BtRadioState state = BT_RADIO_STATE_OFF;
|
||||
bluetooth_get_radio_state(dev, &state);
|
||||
@@ -346,7 +405,7 @@ void pair(const std::array<uint8_t, 6>& /*addr*/) {
|
||||
}
|
||||
|
||||
void unpair(const std::array<uint8_t, 6>& addr) {
|
||||
struct Device* dev = findFirstDevice();
|
||||
Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE);
|
||||
if (dev != nullptr) {
|
||||
bluetooth_unpair(dev, addr.data());
|
||||
}
|
||||
@@ -358,16 +417,16 @@ void connect(const std::array<uint8_t, 6>& addr, int profileId) {
|
||||
if (profileId == BT_PROFILE_HID_HOST) {
|
||||
hidHostConnect(addr);
|
||||
} else if (profileId == BT_PROFILE_HID_DEVICE) {
|
||||
if (struct Device* dev = bluetooth_hid_device_get_device()) {
|
||||
if (Device* dev = bluetooth_hid_device_get_device()) {
|
||||
bluetooth_hid_device_start(dev, BT_HID_DEVICE_MODE_KEYBOARD);
|
||||
}
|
||||
} else if (profileId == BT_PROFILE_SPP) {
|
||||
if (struct Device* dev = bluetooth_serial_get_device()) {
|
||||
if (Device* dev = bluetooth_serial_get_device()) {
|
||||
bluetooth_serial_start(dev);
|
||||
settings::setSppAutoStart(true);
|
||||
}
|
||||
} else if (profileId == BT_PROFILE_MIDI) {
|
||||
if (struct Device* dev = bluetooth_midi_get_device()) {
|
||||
if (Device* dev = bluetooth_midi_get_device()) {
|
||||
bluetooth_midi_start(dev);
|
||||
settings::setMidiAutoStart(true);
|
||||
}
|
||||
@@ -379,11 +438,11 @@ void disconnect(const std::array<uint8_t, 6>& addr, int profileId) {
|
||||
if (profileId == BT_PROFILE_HID_HOST) {
|
||||
hidHostDisconnect();
|
||||
} else if (profileId == BT_PROFILE_HID_DEVICE) {
|
||||
if (struct Device* dev = bluetooth_hid_device_get_device()) {
|
||||
if (Device* dev = bluetooth_hid_device_get_device()) {
|
||||
bluetooth_hid_device_stop(dev);
|
||||
}
|
||||
} else {
|
||||
struct Device* dev = findFirstDevice();
|
||||
Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE);
|
||||
if (dev == nullptr) return;
|
||||
bluetooth_disconnect(dev, addr.data(), (BtProfileId)profileId);
|
||||
}
|
||||
|
||||
@@ -576,8 +576,8 @@ static void hidHostSubscribeNext(HidHostCtx& ctx) {
|
||||
}
|
||||
device.name = name;
|
||||
settings::save(device);
|
||||
if (struct Device* dev = findFirstDevice()) {
|
||||
struct BtEvent e = {};
|
||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||
BtEvent e = {};
|
||||
e.type = BT_EVENT_PROFILE_STATE_CHANGED;
|
||||
e.profile_state.state = BT_PROFILE_STATE_CONNECTED;
|
||||
e.profile_state.profile = BT_PROFILE_HID_HOST;
|
||||
@@ -738,7 +738,7 @@ static int hidHostGapCb(struct ble_gap_event* event, void* /*arg*/) {
|
||||
} else {
|
||||
LOGGER.warn("Connect failed status={}", event->connect.status);
|
||||
hid_host_ctx.reset();
|
||||
if (struct Device* dev = findFirstDevice()) {
|
||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||
bluetooth_set_hid_host_active(dev, false);
|
||||
struct BtEvent e = {};
|
||||
e.type = BT_EVENT_PROFILE_STATE_CHANGED;
|
||||
@@ -763,7 +763,7 @@ static int hidHostGapCb(struct ble_gap_event* event, void* /*arg*/) {
|
||||
hid_host_mouse_btn.store(false);
|
||||
hid_host_mouse_active.store(false);
|
||||
|
||||
if (struct Device* dev = findFirstDevice()) {
|
||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||
bluetooth_set_hid_host_active(dev, false);
|
||||
struct BtEvent e = {};
|
||||
e.type = BT_EVENT_PROFILE_STATE_CHANGED;
|
||||
@@ -865,7 +865,7 @@ void hidHostConnect(const std::array<uint8_t, 6>& addr) {
|
||||
}
|
||||
|
||||
// Notify driver that a HID host central connection is starting.
|
||||
if (struct Device* dev = findFirstDevice()) bluetooth_set_hid_host_active(dev, true);
|
||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) bluetooth_set_hid_host_active(dev, true);
|
||||
|
||||
// Look up the addr_type from the cached scan results.
|
||||
ble_addr_t ble_addr = {};
|
||||
@@ -885,10 +885,10 @@ void hidHostConnect(const std::array<uint8_t, 6>& addr) {
|
||||
if (rc != 0) {
|
||||
LOGGER.warn("ble_gap_connect failed rc={}", rc);
|
||||
hid_host_ctx.reset();
|
||||
if (struct Device* dev = findFirstDevice()) {
|
||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||
bluetooth_set_hid_host_active(dev, false);
|
||||
// Fire IDLE so bt_event_bridge can start a new scan and retry.
|
||||
struct BtEvent e = {};
|
||||
BtEvent e = {};
|
||||
e.type = BT_EVENT_PROFILE_STATE_CHANGED;
|
||||
e.profile_state.state = BT_PROFILE_STATE_IDLE;
|
||||
e.profile_state.profile = BT_PROFILE_HID_HOST;
|
||||
@@ -939,7 +939,7 @@ void autoConnectHidHost() {
|
||||
auto peers = settings::loadAll();
|
||||
for (const auto& peer : peers) {
|
||||
if (peer.autoConnect && peer.profileId == BT_PROFILE_HID_HOST) {
|
||||
if (struct Device* dev = findFirstDevice()) {
|
||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||
if (!bluetooth_is_scanning(dev)) {
|
||||
LOGGER.info("Auto-connect HID host: device not in scan, retrying scan");
|
||||
bluetooth_scan_start(dev);
|
||||
|
||||
@@ -60,8 +60,11 @@ bool hasFileForDevice(const std::string& addr_hex) {
|
||||
}
|
||||
|
||||
bool load(const std::string& addr_hex, PairedDevice& device) {
|
||||
auto file_path = getFilePath(addr_hex);
|
||||
if (!file::isFile(file_path)) return false;
|
||||
|
||||
std::map<std::string, std::string> map;
|
||||
if (!file::loadPropertiesFile(getFilePath(addr_hex), map)) return false;
|
||||
if (!file::loadPropertiesFile(file_path, map)) return false;
|
||||
if (!map.contains(KEY_ADDR)) return false;
|
||||
if (!hexToAddr(map[KEY_ADDR], device.addr)) return false;
|
||||
|
||||
@@ -83,7 +86,12 @@ bool save(const PairedDevice& device) {
|
||||
map[KEY_ADDR] = addr_hex;
|
||||
map[KEY_AUTO_CONNECT] = device.autoConnect ? "true" : "false";
|
||||
map[KEY_PROFILE_ID] = std::to_string(device.profileId);
|
||||
return file::savePropertiesFile(getFilePath(addr_hex), map);
|
||||
auto file_path = getFilePath(addr_hex);
|
||||
if (!file::findOrCreateParentDirectory(file_path, 0755)) {
|
||||
LOGGER.error("Failed to create parent dir for {}", file_path);
|
||||
return false;
|
||||
}
|
||||
return file::savePropertiesFile(file_path, map);
|
||||
}
|
||||
|
||||
bool remove(const std::string& addr_hex) {
|
||||
|
||||
@@ -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";
|
||||
@@ -26,8 +29,13 @@ static BluetoothSettings cached;
|
||||
static bool cached_valid = false;
|
||||
|
||||
static bool load(BluetoothSettings& out) {
|
||||
auto settings_path = getSettingsPath();
|
||||
if (!file::isFile(settings_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> map;
|
||||
if (!file::loadPropertiesFile(SETTINGS_PATH, map)) {
|
||||
if (!file::loadPropertiesFile(settings_path, map)) {
|
||||
return false;
|
||||
}
|
||||
auto it = map.find(KEY_ENABLE_ON_BOOT);
|
||||
@@ -44,11 +52,18 @@ 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
|
||||
if (file::isFile(getSettingsPath())) {
|
||||
file::loadPropertiesFile(getSettingsPath(), map);
|
||||
}
|
||||
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);
|
||||
auto settings_path = getSettingsPath();
|
||||
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
|
||||
LOGGER.error("Failed to create parent dir for {}", settings_path);
|
||||
return false;
|
||||
}
|
||||
return file::savePropertiesFile(settings_path, map);
|
||||
}
|
||||
|
||||
static BluetoothSettings getCachedOrLoad() {
|
||||
|
||||
@@ -41,7 +41,7 @@ void download(
|
||||
config->auth_type = HTTP_AUTH_TYPE_NONE;
|
||||
config->cert_pem = reinterpret_cast<const char*>(certificate.get());
|
||||
config->cert_len = certificate_length;
|
||||
config->tls_version = ESP_HTTP_CLIENT_TLS_VER_TLS_1_3;
|
||||
config->tls_version = ESP_HTTP_CLIENT_TLS_VER_TLS_1_2;
|
||||
config->method = HTTP_METHOD_GET;
|
||||
config->timeout_ms = 5000;
|
||||
config->transport_type = HTTP_TRANSPORT_OVER_SSL;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,35 +1,28 @@
|
||||
#include <Tactility/MountPoints.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/Logger.h>
|
||||
#include <Tactility/settings/BootSettings.h>
|
||||
|
||||
#include <Tactility/Paths.h>
|
||||
#include <format>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::settings {
|
||||
|
||||
constexpr auto* TAG = "BootSettings";
|
||||
|
||||
constexpr auto* PROPERTIES_FILE_FORMAT = "{}/settings/boot.properties";
|
||||
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) {
|
||||
const std::string path = getPropertiesFilePath();
|
||||
if (!file::isFile(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file::loadPropertiesFile(path, [&properties](auto& key, auto& value) {
|
||||
if (key == PROPERTIES_KEY_AUTO_START_APP_ID) {
|
||||
properties.autoStartAppId = value;
|
||||
@@ -37,7 +30,6 @@ bool loadBootSettings(BootSettings& properties) {
|
||||
properties.launcherAppId = value;
|
||||
}
|
||||
})) {
|
||||
LOG_I(TAG, "No settings at %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/Paths.h>
|
||||
#include <tactility/hal/Device.h>
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
|
||||
@@ -10,7 +12,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";
|
||||
@@ -110,8 +115,13 @@ static bool fromString(const std::string& str, ScreensaverType& type) {
|
||||
}
|
||||
|
||||
bool load(DisplaySettings& 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;
|
||||
}
|
||||
|
||||
@@ -191,7 +201,11 @@ 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);
|
||||
auto settings_path = getSettingsFilePath();
|
||||
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
|
||||
return false;
|
||||
}
|
||||
return file::savePropertiesFile(settings_path, map);
|
||||
}
|
||||
|
||||
lv_display_rotation_t toLvglDisplayRotation(Orientation orientation) {
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
#include <Tactility/settings/KeyboardSettings.h>
|
||||
#include <Tactility/file/File.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";
|
||||
constexpr auto* KEY_BACKLIGHT_TIMEOUT_MS = "backlightTimeoutMs";
|
||||
|
||||
bool load(KeyboardSettings& 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;
|
||||
}
|
||||
|
||||
@@ -54,7 +64,11 @@ 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);
|
||||
auto settings_path = getSettingsFilePath();
|
||||
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
|
||||
return false;
|
||||
}
|
||||
return file::savePropertiesFile(settings_path, map);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -14,16 +16,19 @@ static const auto LOGGER = Logger("SystemSettings");
|
||||
|
||||
constexpr auto* FILE_PATH_FORMAT = "{}/settings/system.properties";
|
||||
|
||||
static Mutex mutex;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -51,25 +56,17 @@ static bool loadSystemSettingsFromFile(SystemSettings& properties) {
|
||||
properties.dateFormat = "MM/DD/YYYY";
|
||||
}
|
||||
|
||||
// Load region
|
||||
auto region_entry = map.find("region");
|
||||
if (region_entry != map.end() && !region_entry->second.empty()) {
|
||||
properties.region = region_entry->second;
|
||||
} else {
|
||||
LOGGER.info("Region missing or empty, using default EU");
|
||||
properties.region = "EU";
|
||||
}
|
||||
|
||||
LOGGER.info("System settings loaded");
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -77,12 +74,16 @@ 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";
|
||||
map["dateFormat"] = properties.dateFormat;
|
||||
map["region"] = properties.region;
|
||||
|
||||
if (!file::findOrCreateParentDirectory(file_path, 0755)) {
|
||||
LOGGER.error("Failed to create parent dir for {}", file_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file::savePropertiesFile(file_path, map)) {
|
||||
LOGGER.error("Failed to save {}", file_path);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include <Tactility/settings/TouchCalibrationSettings.h>
|
||||
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/Mutex.h>
|
||||
#include <Tactility/Paths.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
@@ -12,7 +14,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";
|
||||
@@ -60,8 +65,13 @@ bool isValid(const TouchCalibrationSettings& settings) {
|
||||
}
|
||||
|
||||
bool load(TouchCalibrationSettings& 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;
|
||||
}
|
||||
|
||||
@@ -112,7 +122,12 @@ 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)) {
|
||||
auto settings_path = getSettingsFilePath();
|
||||
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file::savePropertiesFile(settings_path, map)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <Tactility/settings/TrackballSettings.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/Paths.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -7,7 +9,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";
|
||||
@@ -19,8 +24,13 @@ constexpr uint8_t MIN_POINTER_SENSITIVITY = 1;
|
||||
constexpr uint8_t MAX_POINTER_SENSITIVITY = 10;
|
||||
|
||||
bool load(TrackballSettings& 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;
|
||||
}
|
||||
|
||||
@@ -80,7 +90,11 @@ 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);
|
||||
auto settings_path = getSettingsFilePath();
|
||||
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
|
||||
return false;
|
||||
}
|
||||
return file::savePropertiesFile(settings_path, 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";
|
||||
@@ -86,8 +90,13 @@ static bool isEmptyCredential(const std::string& value) {
|
||||
}
|
||||
|
||||
bool load(WebServerSettings& 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;
|
||||
}
|
||||
|
||||
@@ -146,7 +155,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 +196,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 +262,12 @@ 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);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,6 +49,15 @@ std::string getTimeZoneName() {
|
||||
}
|
||||
}
|
||||
|
||||
bool hasTimeZone() {
|
||||
Preferences preferences(TIME_SETTINGS_NAMESPACE);
|
||||
std::string timezone;
|
||||
if (!preferences.optString(TIMEZONE_PREFERENCES_KEY_NAME, timezone)) {
|
||||
return false;
|
||||
}
|
||||
return !timezone.empty();
|
||||
}
|
||||
|
||||
std::string getTimeZoneCode() {
|
||||
Preferences preferences(TIME_SETTINGS_NAMESPACE);
|
||||
std::string result;
|
||||
|
||||
Reference in New Issue
Block a user