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
+5 -25
View File
@@ -24,6 +24,7 @@
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <format>
#include <unistd.h>
namespace tt::app::files {
@@ -174,43 +175,22 @@ static bool copyRecursive(const std::string& src, const std::string& dst) {
void View::viewFile(const std::string& path, const std::string& filename) {
std::string file_path = path + "/" + filename;
// For PC we need to make the path relative to the current work directory,
// because that's how LVGL maps its 'drive letter' to the file system.
std::string processed_filepath;
if (kernel::getPlatform() == kernel::PlatformSimulator) {
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
LOG_E(TAG, "Failed to get current working directory");
return;
}
if (!file_path.starts_with(cwd)) {
LOG_E(TAG, "Can only work with files in working directory %s", cwd);
return;
}
processed_filepath = file_path.substr(strlen(cwd));
} else {
processed_filepath = file_path;
}
LOG_I(TAG, "Clicked %s", file_path.c_str());
if (isSupportedAppFile(filename)) {
#ifdef ESP_PLATFORM
// install(filename);
auto message = std::format("Do you want to install {}?", filename);
installAppPath = processed_filepath;
installAppPath = file_path;
auto choices = std::vector<std::string> {"Yes", "No"};
installDialogId = alertdialog::start(appInstanceId, "Install?", message, choices);
#endif
} else if (isSupportedImageFile(filename)) {
imageviewer::start(processed_filepath);
imageviewer::start(file_path);
} else if (isSupportedTextFile(filename)) {
if (kernel::getPlatform() == kernel::PlatformEsp) {
notes::start(processed_filepath);
notes::start(file_path);
} else {
// Remove forward slash, because we need a relative path
notes::start(processed_filepath.substr(1));
notes::start(file_path.substr(1));
}
} else {
LOG_W(TAG, "Opening files of this type is not supported");
@@ -1,18 +1,23 @@
#include "Tactility/app/fileselection/FileSelection.h"
#include "Tactility/app/fileselection/FileSelectionPrivate.h"
#include "Tactility/app/fileselection/View.h"
#include "Tactility/app/fileselection/State.h"
#include <app/event.h>
#include <app/io.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/stream.h>
#include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h>
#include <cstdio>
#include <memory>
#include <string>
#include <unistd.h>
namespace tt::app::fileselection {
@@ -27,20 +32,11 @@ struct Context {
Mode mode;
std::shared_ptr<State> state;
std::unique_ptr<View> view;
// The eventual appMain() return value - see AlertDialog.cpp's Context::result for why this
// is a plain (non-atomic) field safely shared between the LVGL thread (writer, before
// emitting APP_EVENT_CLOSE) and this app's own thread (reader, after waking from it).
int32_t result = 1; // Cancelled - safety-net default if closed without picking a file
std::string resultPath;
int32_t resultCode = 1; // 1 means Cancelled
};
// The last picked path. Static rather than per-instance: simple, and in practice only one
// FileSelection dialog is ever open at a time. Written on the LVGL thread (View's select-button
// callback, before emitting APP_EVENT_CLOSE); read by the parent via getLastPath() after
// receiving that event - safe without a lock for the same reason Context::result is (see
// AlertDialog.cpp).
std::string lastPath;
void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData);
ctx->view->init(parent, ctx->mode);
@@ -52,16 +48,11 @@ int32_t appMain(int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
ctx.mode = (argc > 0 && std::string(argv[0]) == "existing_or_new") ? Mode::ExistingOrNew : Mode::Existing;
ctx.mode = (argc > 0 && std::string(argv[0]) == "--existing-or-new") ? Mode::ExistingOrNew : Mode::Existing;
ctx.state = std::make_shared<State>();
ctx.view = std::make_unique<View>(appInstanceId, ctx.state, [&ctx, appInstanceId](const std::string& path) {
// Runs on the LVGL task (View::onSelectButtonPressed) - must NOT call app_manager_stop()
// here: that bound-waits (thread_join) for this app's own thread to finish, which needs
// the LVGL lock (window_manager_remove()) - but this callback runs ON the LVGL task,
// which would deadlock against itself. The caller reaps this instance via
// app_manager_stop() after it receives the APP_EVENT_RESULT instead.
lastPath = path;
ctx.result = 0;
ctx.resultPath = path;
ctx.resultCode = 0;
app_event_emit_close(appInstanceId);
});
@@ -91,27 +82,42 @@ int32_t appMain(int argc, char* argv[]) {
check(app_event_unsubscribe(&sub) == ERROR_NONE);
task_event_group_destruct(&event_group);
return ctx.result;
if (ctx.resultCode == 0) {
// The parent captures this via an AppStream bound to our stdout (see startWithMode()) -
// see AppStdioWrap.cpp for how printf() itself gets routed there on POSIX.
LOG_I(TAG, "Result: %s", ctx.resultPath.c_str());
printf("%s", ctx.resultPath.c_str());
}
return ctx.resultCode;
}
} // namespace
std::string getLastPath() {
return lastPath;
}
namespace {
uint32_t startForExistingFile(uint32_t callerAppInstanceId) {
const char* argv[] = { "existing" };
uint32_t startWithMode(const char* modeArg, uint32_t callerAppInstanceId, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup) {
const char* argv[] = { modeArg };
AppStreamBinding binding = {
.producer_fd = STDOUT_FILENO,
.stream = &stream,
.buffer = buffer,
.buffer_capacity = bufferCapacity,
.event_group = eventGroup,
};
uint32_t instanceId = 0;
app_manager_start_for_result(manifest.id, callerAppInstanceId, 1, argv, &instanceId);
app_manager_start_for_result_with_streams(manifest.id, callerAppInstanceId, 1, argv, &binding, 1, &instanceId);
return instanceId;
}
uint32_t startForExistingOrNewFile(uint32_t callerAppInstanceId) {
const char* argv[] = { "existing_or_new" };
uint32_t instanceId = 0;
app_manager_start_for_result(manifest.id, callerAppInstanceId, 1, argv, &instanceId);
return instanceId;
} // namespace
uint32_t startForExistingFile(uint32_t callerAppInstanceId, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup) {
return startWithMode("--existing", callerAppInstanceId, stream, buffer, bufferCapacity, eventGroup);
}
uint32_t startForExistingOrNewFile(uint32_t callerAppInstanceId, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup) {
return startWithMode("--existing-or-new", callerAppInstanceId, stream, buffer, bufferCapacity, eventGroup);
}
extern const ::AppManifest manifest = {
+2 -4
View File
@@ -33,7 +33,7 @@ std::string State::getSelectedChildPath() const {
}
bool State::setEntriesForPath(const std::string& path) {
LOG_I(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str());
LOG_D(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str());
auto lock = mutex.asScopedLock();
if (!lock.lock(100)) {
@@ -47,7 +47,6 @@ bool State::setEntriesForPath(const std::string& path) {
*/
bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/");
if (show_custom_root) {
LOG_I(TAG, "Setting custom root");
dir_entries = file::getFileSystemDirents();
current_path = path;
selected_child_entry = "";
@@ -56,7 +55,6 @@ bool State::setEntriesForPath(const std::string& path) {
dir_entries.clear();
int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType);
if (count >= 0) {
LOG_I(TAG, "%s has %d entries", path.c_str(), count);
current_path = path;
selected_child_entry = "";
return true;
@@ -69,7 +67,7 @@ bool State::setEntriesForPath(const std::string& path) {
bool State::setEntriesForChildPath(const std::string& childPath) {
auto path = file::getChildPath(current_path, childPath);
LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
LOG_D(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
return setEntriesForPath(path);
}
+7 -4
View File
@@ -60,12 +60,15 @@ void View::onTapFile(const std::string& path, const std::string& filename) {
LOG_E(TAG, "Can only work with files in working directory %s", cwd);
return;
}
processed_filepath = file_path.substr(strlen(cwd));
// MountPoints.h's MOUNT_POINT_DATA/MOUNT_POINT_SYSTEM have no leading slash on POSIX
// (fopen() resolves relative to cwd there), unlike ESP32's real VFS mount points - so
// strip the separator too, not just cwd itself.
processed_filepath = file_path.substr(strlen(cwd) + 1);
} else {
processed_filepath = file_path;
}
LOG_I(TAG, "Clicked %s", processed_filepath.c_str());
LOG_D(TAG, "Clicked %s", processed_filepath.c_str());
lv_textarea_set_text(path_textarea, processed_filepath.c_str());
}
@@ -73,7 +76,7 @@ void View::onTapFile(const std::string& path, const std::string& filename) {
void View::onDirEntryPressed(uint32_t index) {
dirent dir_entry;
if (state->getDirent(index, dir_entry)) {
LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, (int)dir_entry.d_type);
LOG_D(TAG, "Pressed %s %d", dir_entry.d_name, (int)dir_entry.d_type);
state->setSelectedChildEntry(dir_entry.d_name);
using namespace tt::file;
switch (dir_entry.d_type) {
@@ -146,7 +149,7 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
void View::onNavigateUpPressed() {
if (state->getCurrentPath() != "/") {
LOG_I(TAG, "Navigating upwards");
LOG_D(TAG, "Navigating upwards");
std::string new_absolute_path;
if (string::getPathParent(state->getCurrentPath(), new_absolute_path)) {
state->setEntriesForPath(new_absolute_path);
+28 -6
View File
@@ -1,4 +1,7 @@
#include <Tactility/app/notes/Notes.h>
#include "Tactility/app/alertdialog/AlertDialog.h"
#include <Tactility/app/fileselection/FileSelection.h>
#include <Tactility/file/File.h>
@@ -6,6 +9,7 @@
#include <app/manager.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/stream.h>
#include <lvgl_window_manager/window_manager.h>
@@ -25,6 +29,7 @@ namespace {
struct Context {
uint32_t appInstanceId;
TaskEventGroup* eventGroup = nullptr;
lv_obj_t* uiCurrentFileName = nullptr;
lv_obj_t* uiDropDownMenu = nullptr;
@@ -35,6 +40,10 @@ struct Context {
uint32_t loadFileLaunchId = 0;
uint32_t saveFileLaunchId = 0;
AppStream loadResultStream {};
uint8_t loadResultBuffer[256] {};
AppStream saveResultStream {};
uint8_t saveResultBuffer[256] {};
};
@@ -90,11 +99,11 @@ void appNotesEventCb(lv_event_t* e) {
lvgl_lock();
ctx->saveBuffer = lv_textarea_get_text(ctx->uiNoteText);
lvgl_unlock();
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId);
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId, ctx->saveResultStream, ctx->saveResultBuffer, sizeof(ctx->saveResultBuffer), ctx->eventGroup);
LOG_I(TAG, "launched with id %u", ctx->saveFileLaunchId);
break;
case 3: // Load
ctx->loadFileLaunchId = fileselection::startForExistingFile(ctx->appInstanceId);
ctx->loadFileLaunchId = fileselection::startForExistingFile(ctx->appInstanceId, ctx->loadResultStream, ctx->loadResultBuffer, sizeof(ctx->loadResultBuffer), ctx->eventGroup);
LOG_I(TAG, "launched with id %u", ctx->loadFileLaunchId);
break;
}
@@ -102,7 +111,7 @@ void appNotesEventCb(lv_event_t* e) {
auto* cont = lv_event_get_current_target_obj(e);
if (obj == cont) return;
if (lv_obj_get_child(cont, 1)) {
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId);
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId, ctx->saveResultStream, ctx->saveResultBuffer, sizeof(ctx->saveResultBuffer), ctx->eventGroup);
LOG_I(TAG, "launched with id %u", ctx->saveFileLaunchId);
} else { //Reset
resetFileContent(ctx);
@@ -189,6 +198,7 @@ int32_t appMain(int argc, char* argv[]) {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
ctx.eventGroup = &event_group;
AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
@@ -206,23 +216,35 @@ int32_t appMain(int argc, char* argv[]) {
shouldClose = true;
break;
case APP_EVENT_RESULT:
LOG_I(TAG, "Result for launch id %u", event.result.launch_id);
LOG_I(TAG, "Result for launch id %u = %u", event.result.launch_id, event.result.result);
if (event.result.launch_id == ctx.loadFileLaunchId) {
ctx.loadFileLaunchId = 0;
if (event.result.result == 0 /* Ok */) {
auto path = fileselection::getLastPath();
char destination[sizeof(ctx.loadResultBuffer)];
size_t length = app_stream_read(&ctx.loadResultStream, destination, sizeof(destination));
app_stream_unsubscribe(&ctx.loadResultStream);
auto path = std::string(destination, length);
LOG_I(TAG, "Path: '%s'", path.c_str());
if (!path.empty()) {
openFile(&ctx, path);
}
} else {
app_stream_unsubscribe(&ctx.loadResultStream);
}
} else if (event.result.launch_id == ctx.saveFileLaunchId) {
ctx.saveFileLaunchId = 0;
if (event.result.result == 0 /* Ok */) {
auto path = fileselection::getLastPath();
char destination[sizeof(ctx.saveResultBuffer)];
size_t length = app_stream_read(&ctx.saveResultStream, destination, sizeof(destination));
app_stream_unsubscribe(&ctx.saveResultStream);
auto path = std::string(destination, length);
// Must re-open file, because the UI was cleared after opening the dialog.
LOG_I(TAG, "Path: '%s'", path.c_str());
if (!path.empty() && saveFile(&ctx, path)) {
openFile(&ctx, path);
}
} else {
app_stream_unsubscribe(&ctx.saveResultStream);
}
}
app_manager_stop(event.result.launch_id);