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:
committed by
GitHub
parent
d3656bcd3d
commit
643cbc3806
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user