Implement posix app loading (#643)

- Added POSIX desktop support for SDK builds, application packaging, and integration testing.
- Added POSIX filesystem partitions and improved application path handling.
- Added simulator support for loading and running applications dynamically.
- Improved simulator task stack handling and display startup reliability.
- Improved simulator display scaling, resizing, high-DPI support, and pointer accuracy.
- Fixed LVGL timers and input polling on POSIX. (fixes simulator with Linux on some Intel graphics platforms)
- Standardized data paths across platforms.
- Logging now works via separate task: this allows apps to write to log without it affecting their stdout (for apps that output text as relevant date for other apps, like the File Selection app)
- Fixes for app stdio
This commit is contained in:
Ken Van Hoeylandt
2026-08-31 22:09:04 +02:00
committed by GitHub
parent d3656bcd3d
commit 643cbc3806
66 changed files with 2139 additions and 336 deletions
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
// Fixed capacity of one queued log line (256 bytes * 24-deep queue = 6KB, negligible against the
// hundreds of KB of free heap typically available - generous enough to hold a full log line with
// color/timestamp/tag prefix plus a realistic message, e.g. a full SD-card path in an error log).
#define LOG_QUEUE_MESSAGE_MAX_LENGTH 256U
/**
* Starts the shared log queue and its dedicated drain (writer) task. Idempotent - safe to call
* more than once (only the first call has any effect). Must be called as early as possible in
* boot - see kernel_init.cpp, which calls this as its very first statement.
*/
void log_queue_init(void);
/**
* Enqueues an already-fully-formatted, ready-to-write log line (NOT a printf-style format
* string) so the dedicated drain task can write it via a path that never goes through
* app-module's fd-table redirection (see Modules/app-module/source/io.cpp's app_io_write()) -
* this is the ONLY function log-producing code should call to reach the console; it must never
* itself call write()/printf()/etc, since those would be intercepted whenever called from an app
* instance's own task, which is exactly the bug this exists to structurally prevent.
*
* Non-blocking: if the queue is full, the message is dropped and a counter is incremented; the
* drain task prepends a "N messages dropped" notice before its next write. If called before
* log_queue_init() has run, performs a direct synchronous write instead of enqueueing - safe,
* since no app instance (and so no fd-table redirection) can exist yet that early in boot.
* Truncates messages longer than LOG_QUEUE_MESSAGE_MAX_LENGTH - 1.
*/
void log_queue_write(const char* data, size_t length);
#ifdef __cplusplus
}
#endif
+3
View File
@@ -2,6 +2,7 @@
#include <tactility/device.h>
#include <tactility/log.h>
#include <tactility/log_queue.h>
#ifdef __cplusplus
extern "C" {
@@ -46,6 +47,8 @@ Module kernel_module = {
};
error_t kernel_init(Module* const dts_modules[], const DtsDevice dts_devices[]) {
log_queue_init();
LOG_I(TAG, "init");
if (module_construct_add_start(&kernel_module) != ERROR_NONE) {
+41 -9
View File
@@ -3,6 +3,7 @@
#ifndef ESP_PLATFORM
#include <tactility/log.h>
#include <tactility/log_queue.h>
#include <mutex>
#include <inttypes.h>
@@ -11,7 +12,11 @@
#include <stdarg.h>
#include <sys/time.h>
static const char* get_log_color(LogLevel level) {
namespace {
constexpr auto MINIMUM_LOG_LEVEL = LOG_LEVEL_DEBUG;
const char* get_log_color(LogLevel level) {
using enum LogLevel;
switch (level) {
case LOG_LEVEL_ERROR:
@@ -29,7 +34,7 @@ static const char* get_log_color(LogLevel level) {
}
}
static inline char get_log_prefix(LogLevel level) {
inline char get_log_prefix(LogLevel level) {
using enum LogLevel;
switch (level) {
case LOG_LEVEL_ERROR:
@@ -47,7 +52,7 @@ static inline char get_log_prefix(LogLevel level) {
}
}
static uint64_t get_log_timestamp() {
uint64_t get_log_timestamp() {
static uint64_t base = 0U;
static std::once_flag init_flag;
std::call_once(init_flag, []() {
@@ -61,15 +66,42 @@ static uint64_t get_log_timestamp() {
return now - base;
}
}
extern "C" {
void log_generic(enum LogLevel level, const char* tag, const char* format, ...) {
va_list args;
va_start(args, format);
printf("%s %c (%" PRIu64 ") %s ", get_log_color(level), get_log_prefix(level), get_log_timestamp(), tag);
vprintf(format, args);
printf("\033[0m\n");
va_end(args);
if (MINIMUM_LOG_LEVEL >= level) {
char buffer[LOG_QUEUE_MESSAGE_MAX_LENGTH];
size_t offset = 0;
int prefix_len = snprintf(buffer, sizeof(buffer), "%s %c (%" PRIu64 ") %s ",
get_log_color(level), get_log_prefix(level), get_log_timestamp(), tag);
if (prefix_len > 0) {
offset = static_cast<size_t>(prefix_len) < sizeof(buffer) ? static_cast<size_t>(prefix_len) : sizeof(buffer) - 1;
}
if (offset < sizeof(buffer)) {
va_list args;
va_start(args, format);
int written = vsnprintf(buffer + offset, sizeof(buffer) - offset, format, args);
va_end(args);
if (written > 0) {
size_t remaining = sizeof(buffer) - offset;
offset += static_cast<size_t>(written) < remaining ? static_cast<size_t>(written) : remaining - 1;
}
}
if (offset < sizeof(buffer)) {
int tail_len = snprintf(buffer + offset, sizeof(buffer) - offset, "\033[0m\n");
if (tail_len > 0) {
size_t remaining = sizeof(buffer) - offset;
offset += static_cast<size_t>(tail_len) < remaining ? static_cast<size_t>(tail_len) : remaining - 1;
}
}
log_queue_write(buffer, offset);
}
}
}
+184
View File
@@ -0,0 +1,184 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/log_queue.h>
#include <tactility/freertos/queue.h>
#include <tactility/freertos/task.h>
#include <tactility/memory.h>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#if defined(ESP_PLATFORM)
#include <esp_log.h>
#endif
namespace {
constexpr size_t LOG_QUEUE_DEPTH = 8;
constexpr size_t DRAIN_TASK_STACK_DEPTH = 4096 / sizeof(StackType_t);
struct LogQueueMessage {
uint16_t length;
char text[LOG_QUEUE_MESSAGE_MAX_LENGTH];
};
std::atomic<QueueHandle_t> g_queue { nullptr };
// The one and only place a real console write happens for log output - deliberately never
// through app_io_write()'s fd-table redirection (Modules/app-module/source/io.cpp). That
// redirection only ever triggers for a task app_task_main() (Modules/app-module/source/
// scheduler.cpp) set up as a running app instance; this drain task is never that, so
// app_scheduler_current_app_id() always reads 0 on it and app_io_write()'s lookup is skipped
// unconditionally - a plain write() already reaches the real syscall, on every platform, with no
// wrap-bypassing needed here.
void write_real(const char* data, size_t length) {
if (length == 0) {
return;
}
#if defined(ESP_PLATFORM)
constexpr int fd = 1; // ESP-IDF's default console fd
#else
constexpr int fd = 2; // matches log_generic()'s stderr choice
#endif
::write(fd, data, length);
}
void drain_task_main(void* context) {
// The queue handle is already valid by the time this task starts (it's created before the
// task is), but reading it back from g_queue here would race: g_queue is only published
// (log_queue_init()'s g_queue.store()) AFTER task creation returns, so this task can start
// running before that store happens. Take it directly as the task's own argument instead.
auto queue = static_cast<QueueHandle_t>(context);
LogQueueMessage message;
while (true) {
if (xQueueReceive(queue, &message, portMAX_DELAY) == pdTRUE) {
write_real(message.text, message.length);
}
}
}
#if defined(ESP_PLATFORM)
// ESP-IDF's own log macros pre-format the entire line (color, level letter, timestamp, tag,
// message, color reset, newline - see esp_log_write()/LOG_FORMAT() in esp_log.h) before handing
// it to the installed vprintf_like_t as fmt+args, so this hook only needs one vsnprintf - no
// prefix building of its own (unlike log_generic() in log.cpp, which builds its own prefix).
// Replaces (does not chain through) the previously-installed vprintf: the whole point is
// removing the direct-to-stdout path, not adding a second consumer of it.
int log_queue_vprintf_hook(const char* format, va_list args) {
char buffer[LOG_QUEUE_MESSAGE_MAX_LENGTH];
int written = vsnprintf(buffer, sizeof(buffer), format, args);
if (written > 0) {
size_t length = static_cast<size_t>(written) < sizeof(buffer) ? static_cast<size_t>(written) : sizeof(buffer) - 1;
log_queue_write(buffer, length);
}
return written;
}
#endif
} // namespace
extern "C" {
void log_queue_init(void) {
if (g_queue.load(std::memory_order_relaxed) != nullptr) {
return; // already initialized
}
#if defined(ESP_PLATFORM)
// Message storage: prefer PSRAM/external memory, fall back to internal automatically if
// unavailable (MemoryPolicy's documented semantics) - this is the bulk of the queue's memory
// (24 * 256 bytes), so it's the part worth offloading to PSRAM when there is any.
MemoryPolicy storage_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
auto* queue_storage = static_cast<uint8_t*>(memory_alloc_with_policy(LOG_QUEUE_DEPTH * sizeof(LogQueueMessage), &storage_policy));
if (queue_storage == nullptr) {
return;
}
// The queue's own control block and the drain task's TCB are both small, fixed-size kernel
// objects (not the bulk data) - keep them in internal RAM like scheduler.cpp's
// APP_TASK_TCB_POLICY already does for app instance tasks.
MemoryPolicy internal_policy = { .required = MEMORY_CAPABILITY_INTERNAL, .desired = 0, .alignment = 0 };
auto* queue_struct = static_cast<StaticQueue_t*>(memory_alloc_with_policy(sizeof(StaticQueue_t), &internal_policy));
if (queue_struct == nullptr) {
memory_free(queue_storage);
return;
}
QueueHandle_t queue = xQueueCreateStatic(LOG_QUEUE_DEPTH, sizeof(LogQueueMessage), queue_storage, queue_struct);
if (queue == nullptr) {
memory_free(queue_struct);
memory_free(queue_storage);
return;
}
// Stack: same PSRAM-preferred/internal-fallback policy as the message storage above.
MemoryPolicy stack_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
auto* stack_buffer = static_cast<StackType_t*>(memory_alloc_with_policy(DRAIN_TASK_STACK_DEPTH * sizeof(StackType_t), &stack_policy));
if (stack_buffer == nullptr) {
vQueueDelete(queue);
memory_free(queue_struct);
memory_free(queue_storage);
return;
}
auto* task_tcb = static_cast<StaticTask_t*>(memory_alloc_with_policy(sizeof(StaticTask_t), &internal_policy));
if (task_tcb == nullptr) {
memory_free(stack_buffer);
vQueueDelete(queue);
memory_free(queue_struct);
memory_free(queue_storage);
return;
}
TaskHandle_t task_handle = xTaskCreateStatic(drain_task_main, "log_drain", DRAIN_TASK_STACK_DEPTH, queue, tskIDLE_PRIORITY + 1, stack_buffer, task_tcb);
if (task_handle == nullptr) {
memory_free(task_tcb);
memory_free(stack_buffer);
vQueueDelete(queue);
memory_free(queue_struct);
memory_free(queue_storage);
return;
}
#else
QueueHandle_t queue = xQueueCreate(LOG_QUEUE_DEPTH, sizeof(LogQueueMessage));
if (queue == nullptr) {
return;
}
TaskHandle_t task_handle = nullptr;
if (xTaskCreate(drain_task_main, "log_drain", DRAIN_TASK_STACK_DEPTH, queue, tskIDLE_PRIORITY + 1, &task_handle) != pdPASS) {
vQueueDelete(queue);
return;
}
#endif
g_queue.store(queue, std::memory_order_release); // published last
#if defined(ESP_PLATFORM)
esp_log_set_vprintf(log_queue_vprintf_hook);
#endif
}
void log_queue_write(const char* data, size_t length) {
if (data == nullptr || length == 0) {
return;
}
QueueHandle_t queue = g_queue.load(std::memory_order_acquire);
if (queue == nullptr) {
write_real(data, length); // pre-init fallback
return;
}
LogQueueMessage message;
size_t copy_length = length < sizeof(message.text) ? length : sizeof(message.text) - 1;
memcpy(message.text, data, copy_length);
message.length = static_cast<uint16_t>(copy_length);
xQueueSend(queue, &message, portMAX_DELAY);
}
} // extern "C"
-9
View File
@@ -42,7 +42,6 @@ static error_t paths_get_data_root_path(char* out_path, size_t out_path_size) {
extern "C" {
error_t paths_get_data_path(char* out_path, size_t out_path_size) {
#ifdef ESP_PLATFORM
char root[64];
error_t error = paths_get_data_root_path(root, sizeof(root));
if (error != ERROR_NONE) {
@@ -53,14 +52,6 @@ error_t paths_get_data_path(char* out_path, size_t out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
#else
const char* fixed_path = "data";
if (std::strlen(fixed_path) + 1 > out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
std::strcpy(out_path, fixed_path);
return ERROR_NONE;
#endif
}
} // extern "C"
+6 -6
View File
@@ -5,22 +5,22 @@
#include <tactility/paths.h>
// The simulator target is never built with ESP_PLATFORM, so paths_get_data_path()
// always takes the fixed "data" path branch here, guarded by a buffer-size check.
// always takes the fixed "data" root, with "/tactility" appended, guarded by a buffer-size check.
TEST_CASE("paths_get_data_path succeeds when the buffer exactly fits") {
char buffer[16] = { 0 };
char buffer[32] = { 0 };
CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_NONE);
CHECK_EQ(std::strcmp(buffer, "data"), 0);
CHECK_EQ(std::strcmp(buffer, "data/tactility"), 0);
}
TEST_CASE("paths_get_data_path succeeds with a buffer sized to exactly fit the string and terminator") {
char buffer[5] = { 0 }; // strlen("data") + 1
char buffer[15] = { 0 }; // strlen("data/tactility") + 1
CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_NONE);
CHECK_EQ(std::strcmp(buffer, "data"), 0);
CHECK_EQ(std::strcmp(buffer, "data/tactility"), 0);
}
TEST_CASE("paths_get_data_path reports a buffer overflow when the buffer is one byte too small") {
char buffer[4] = { 0 }; // strlen("data"), no room for the terminator
char buffer[14] = { 0 }; // strlen("data/tactility"), no room for the terminator
CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
}