Merge develop into main (#304)

## New

- Read property files with `PropertiesFile`
- Support `boot.properties` so the user can specify the launcher app and an optional app to start after the launcher finishes. (see `BootProperties.cpp`)
- Create registry for CPU affinity and update code to make use of it
- `AppRegistration` and `ServiceRegistration` now also ensure that the `/data` directories always exist for all apps
- `Notes` is now the default app for opening text files. `TextViewer` is removed entirely. Created `tt::app::notes::start(path)` function.
- WiFi settings moved from NVS to properties file.
- Specify `*.ap.properties` file on the SD card for automatic WiFi settings import on start-up.
- Added `file::getLock(path)` and `file::withLock(path, function)` to do safe file operations on SD cards

## Improvements

- Update TinyUSB to `1.7.6~1`
- Improved `Boot.cpp` code. General code quality fixes and some restructuring to improve readability.
- `tt::string` functionality improvements
- Rename `AppRegistry` to `AppRegistration`
- Rename `ServiceRegistry` to `ServiceRegistration`
- Cleanup in `Notes.cpp`
- `FileTest.cpp` fix for PC
- Created `TestFile` helper class for tests, which automatically deletes files after the test.
- Renamed `Partitions.h` to `MountPoints.h`
- Created `std::string getMountPoints()` function for easy re-use
- Other code quality improvements
- `SdCardDevice`'s `getState()` and `isMounted()` now have a timeout argument

## Fixes

- ELF loading now has a lock so to avoid a bug when 2 ELF apps are loaded in parallel
This commit is contained in:
Ken Van Hoeylandt
2025-08-23 17:10:18 +02:00
committed by GitHub
parent fbaff8cbac
commit ee5a5a7181
109 changed files with 1396 additions and 744 deletions
+85
View File
@@ -0,0 +1,85 @@
#include "Tactility/CpuAffinity.h"
#include <Tactility/Check.h>
namespace tt {
#ifdef ESP_PLATFORM
static CpuAffinity getEspWifiAffinity() {
#ifdef CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0
return 0;
#elif defined(CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1)
return 1;
#endif
}
// Warning: Must watch ESP WiFi, as this task is used by WiFi
static CpuAffinity getEspMainSchedulerAffinity() {
#ifdef CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0
return 0;
#elif defined(CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1)
return 1;
#endif
}
static CpuAffinity getFreeRtosTimerAffinity() {
#if defined(CONFIG_FREERTOS_TIMER_TASK_NO_AFFINITY)
return None;
#elif defined(CONFIG_FREERTOS_TIMER_TASK_AFFINITY_CPU0)
return 0;
#elif defined(CONFIG_FREERTOS_TIMER_TASK_AFFINITY_CPU1)
return 1;
#else
static_assert(false);
#endif
}
#if CONFIG_FREERTOS_NUMBER_OF_CORES == 1
static const CpuAffinityConfiguration esp = {
.system = 0,
.graphics = 0,
.wifi = 0,
.mainDispatcher = 0,
.apps = 0,
.timer = getFreeRtosTimerAffinity()
};
#elif CONFIG_FREERTOS_NUMBER_OF_CORES == 2
static const CpuAffinityConfiguration esp = {
.system = 0,
.graphics = 1,
.wifi = getEspWifiAffinity(),
.mainDispatcher = getEspMainSchedulerAffinity(),
.apps = 1,
.timer = getFreeRtosTimerAffinity()
};
#endif
#else
static const CpuAffinityConfiguration simulator = {
.system = None,
.graphics = None,
.wifi = None,
.mainDispatcher = 0,
.apps = None,
.timer = None
};
#endif
const CpuAffinityConfiguration& getCpuAffinityConfiguration() {
#ifdef ESP_PLATFORM
#if CONFIG_FREERTOS_NUMBER_OF_CORES == 2
// WiFi uses the main dispatcher to defer operations in the background
assert(esp.wifi == esp.mainDispatcher);
#endif // CORES
return esp;
#else
return simulator;
#endif
}
}
+10 -6
View File
@@ -29,24 +29,28 @@ std::string getLastPathSegment(const std::string& path) {
}
}
std::vector<std::string> split(const std::string&input, const std::string&delimiter) {
void split(const std::string& input, const std::string& delimiter, std::function<void(const std::string&)> callback) {
size_t token_index = 0;
size_t delimiter_index;
const size_t delimiter_length = delimiter.length();
std::string token;
std::vector<std::string> result;
while ((delimiter_index = input.find(delimiter, token_index)) != std::string::npos) {
token = input.substr(token_index, delimiter_index - token_index);
std::string token = input.substr(token_index, delimiter_index - token_index);
token_index = delimiter_index + delimiter_length;
result.push_back(token);
callback(token);
}
auto end_token = input.substr(token_index);
if (!end_token.empty()) {
result.push_back(end_token);
callback(end_token);
}
}
std::vector<std::string> split(const std::string&input, const std::string&delimiter) {
std::vector<std::string> result;
split(input, delimiter, [&result](const std::string& token) {
result.push_back(token);
});
return result;
}
+1 -1
View File
@@ -13,7 +13,7 @@ void Timer::onCallback(TimerHandle_t hTimer) {
}
}
static inline TimerHandle_t createTimer(Timer::Type type, void* timerId, TimerCallbackFunction_t callback) {
static TimerHandle_t createTimer(Timer::Type type, void* timerId, TimerCallbackFunction_t callback) {
assert(timerId != nullptr);
assert(callback != nullptr);
+3 -3
View File
@@ -127,7 +127,7 @@ static void getKey(uint8_t key[32]) {
}
void getIv(const void* data, size_t dataLength, uint8_t iv[16]) {
memset((void*)iv, 0, 16);
memset(iv, 0, 16);
auto* data_bytes = (uint8_t*)data;
for (int i = 0; i < dataLength; ++i) {
size_t safe_index = i % 16;
@@ -161,7 +161,7 @@ static int aes256CryptCbc(
return result;
}
int encrypt(const uint8_t iv[16], uint8_t* inData, uint8_t* outData, size_t dataLength) {
int encrypt(const uint8_t iv[16], const uint8_t* inData, uint8_t* outData, size_t dataLength) {
tt_check(dataLength % 16 == 0, "Length is not a multiple of 16 bytes (for AES 256");
uint8_t key[32];
getKey(key);
@@ -173,7 +173,7 @@ int encrypt(const uint8_t iv[16], uint8_t* inData, uint8_t* outData, size_t data
return aes256CryptCbc(key, MBEDTLS_AES_ENCRYPT, dataLength, iv_copy, inData, outData);
}
int decrypt(const uint8_t iv[16], uint8_t* inData, uint8_t* outData, size_t dataLength) {
int decrypt(const uint8_t iv[16], const uint8_t* inData, uint8_t* outData, size_t dataLength) {
tt_check(dataLength % 16 == 0, "Length is not a multiple of 16 bytes (for AES 256");
uint8_t key[32];
getKey(key);
+14
View File
@@ -2,6 +2,11 @@
#include <cstring>
#include <fstream>
#include <unistd.h>
namespace tt::hal::sdcard {
class SdCardDevice;
}
namespace tt::file {
@@ -208,4 +213,13 @@ bool findOrCreateDirectory(std::string path, mode_t mode) {
return true;
}
bool isFile(const std::string& path) {
return access(path.c_str(), F_OK) == 0;
}
bool isDirectory(const std::string& path) {
struct stat stat_result;
return stat(path.c_str(), &stat_result) == 0 && S_ISDIR(stat_result.st_mode);
}
}
@@ -1,78 +0,0 @@
#include "Tactility/file/ObjectFile.h"
#include "Tactility/file/ObjectFilePrivate.h"
#include <cstring>
#include <Tactility/Log.h>
namespace tt::file {
constexpr const char* TAG = "ObjectFileReader";
bool ObjectFileReader::open() {
auto opening_file = std::unique_ptr<FILE, FileCloser>(fopen(filePath.c_str(), "r"));
if (opening_file == nullptr) {
TT_LOG_E(TAG, "Failed to open file %s", filePath.c_str());
return false;
}
FileHeader file_header;
if (fread(&file_header, sizeof(FileHeader), 1, opening_file.get()) != 1) {
TT_LOG_E(TAG, "Failed to read file header from %s", filePath.c_str());
return false;
}
if (file_header.identifier != OBJECT_FILE_IDENTIFIER) {
TT_LOG_E(TAG, "Invalid file type for %s", filePath.c_str());
return false;
}
if (file_header.version != OBJECT_FILE_VERSION) {
TT_LOG_E(TAG, "Unknown version for %s: %lu", filePath.c_str(), file_header.identifier);
return false;
}
ContentHeader content_header;
if (fread(&content_header, sizeof(ContentHeader), 1, opening_file.get()) != 1) {
TT_LOG_E(TAG, "Failed to read content header from %s", filePath.c_str());
return false;
}
if (recordSize != content_header.recordSize) {
TT_LOG_E(TAG, "Record size mismatch for %s: expected %lu, got %lu", filePath.c_str(), recordSize, content_header.recordSize);
return false;
}
recordCount = content_header.recordCount;
recordVersion = content_header.recordVersion;
file = std::move(opening_file);
TT_LOG_D(TAG, "File version: %lu", file_header.version);
TT_LOG_D(TAG, "Content: version = %lu, size = %lu bytes, count = %lu", content_header.recordVersion, content_header.recordSize, content_header.recordCount);
return true;
}
void ObjectFileReader::close() {
recordCount = 0;
recordVersion = 0;
recordsRead = 0;
file = nullptr;
}
bool ObjectFileReader::readNext(void* output) {
if (file == nullptr) {
TT_LOG_E(TAG, "File not open");
return false;
}
bool result = fread(output, recordSize, 1, file.get()) == 1;
if (result) {
recordsRead++;
}
return result;
}
}
@@ -1,125 +0,0 @@
#include "Tactility/file/ObjectFile.h"
#include "Tactility/file/ObjectFilePrivate.h"
#include <cstring>
#include <Tactility/Log.h>
#include <unistd.h>
namespace tt::file {
constexpr const char* TAG = "ObjectFileWriter";
bool ObjectFileWriter::open() {
bool edit_existing = append && access(filePath.c_str(), F_OK) == 0;
if (append && !edit_existing) {
TT_LOG_W(TAG, "access() to %s failed: %s", filePath.c_str(), strerror(errno));
}
// Edit existing or create a new file
auto* mode = edit_existing ? "r+" : "w";
auto opening_file = std::unique_ptr<FILE, FileCloser>(std::fopen(filePath.c_str(), mode));
if (opening_file == nullptr) {
TT_LOG_E(TAG, "Failed to open file %s in %s mode", filePath.c_str(), mode);
return false;
}
auto file_size = getSize(opening_file.get());
if (file_size > 0 && edit_existing) {
// Read and parse file header
FileHeader file_header;
if (fread(&file_header, sizeof(FileHeader), 1, opening_file.get()) != 1) {
TT_LOG_E(TAG, "Failed to read file header from %s", filePath.c_str());
return false;
}
if (file_header.identifier != OBJECT_FILE_IDENTIFIER) {
TT_LOG_E(TAG, "Invalid file type for %s", filePath.c_str());
return false;
}
if (file_header.version != OBJECT_FILE_VERSION) {
TT_LOG_E(TAG, "Unknown version for %s: %lu", filePath.c_str(), file_header.identifier);
return false;
}
// Read and parse content header
ContentHeader content_header;
if (fread(&content_header, sizeof(ContentHeader), 1, opening_file.get()) != 1) {
TT_LOG_E(TAG, "Failed to read content header from %s", filePath.c_str());
return false;
}
if (recordSize != content_header.recordSize) {
TT_LOG_E(TAG, "Record size mismatch for %s: expected %lu, got %lu", filePath.c_str(), recordSize, content_header.recordSize);
return false;
}
if (recordVersion != content_header.recordVersion) {
TT_LOG_E(TAG, "Version mismatch for %s: expected %lu, got %lu", filePath.c_str(), recordVersion, content_header.recordVersion);
return false;
}
recordsWritten = content_header.recordCount;
fseek(opening_file.get(), 0, SEEK_END);
} else {
FileHeader file_header;
if (fwrite(&file_header, sizeof(FileHeader), 1, opening_file.get()) != 1) {
TT_LOG_E(TAG, "Failed to write file header for %s", filePath.c_str());
return false;
}
// Seek forward (skip ContentHeader that will be written later)
fseek(opening_file.get(), sizeof(ContentHeader), SEEK_CUR);
}
file = std::move(opening_file);
return true;
}
void ObjectFileWriter::close() {
if (file == nullptr) {
TT_LOG_E(TAG, "File not opened: %s", filePath.c_str());
return;
}
if (fseek(file.get(), sizeof(FileHeader), SEEK_SET) != 0) {
TT_LOG_E(TAG, "File seek failed: %s", filePath.c_str());
return;
}
ContentHeader content_header = {
.recordVersion = this->recordVersion,
.recordSize = this->recordSize,
.recordCount = this->recordsWritten
};
if (fwrite(&content_header, sizeof(ContentHeader), 1, file.get()) != 1) {
TT_LOG_E(TAG, "Failed to write content header to %s", filePath.c_str());
}
file = nullptr;
}
bool ObjectFileWriter::write(void* data) {
if (file == nullptr) {
TT_LOG_E(TAG, "File not opened: %s", filePath.c_str());
return false;
}
if (fwrite(data, recordSize, 1, file.get()) != 1) {
TT_LOG_E(TAG, "Failed to write record to %s", filePath.c_str());
return false;
}
recordsWritten++;
return true;
}
// endregion Writer
}