From 20600950281d8963e5409631bc98c156bbf55ef8 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Mon, 14 Sep 2026 21:02:54 -0400 Subject: [PATCH] feat(sim): web viewer at /sim + touch injection at /api/sim/touch - GET /sim: live viewer page, 2s screenshot refresh, click-to-touch - POST /api/sim/touch?x,y,down: injects into sdl-pointer backend, auto-releases after 1.5s; simulator-only (404 on ESP32) - sdl_input: file-scope touch override state with extern C setters --- .../simulator/Source/drivers/sdl_input.cpp | 41 ++++++++- Devices/simulator/Source/drivers/sdl_input.h | 10 ++ .../service/webserver/WebServerService.h | 2 + .../service/webserver/WebServerService.cpp | 92 +++++++++++++++++++ 4 files changed, 144 insertions(+), 1 deletion(-) diff --git a/Devices/simulator/Source/drivers/sdl_input.cpp b/Devices/simulator/Source/drivers/sdl_input.cpp index 747f5544..abe45809 100644 --- a/Devices/simulator/Source/drivers/sdl_input.cpp +++ b/Devices/simulator/Source/drivers/sdl_input.cpp @@ -14,6 +14,34 @@ constexpr size_t KEY_QUEUE_CAPACITY = 32; SdlPointerState pointer_state = { 0, 0, false }; +} // namespace + +// Web touch-injection override (headless sim viewer). Guarded by tick count so +// a press auto-releases even if the viewer never sends the release event. +// File-scope (not anonymous namespace): the extern "C" setters below must be +// visible to the linker for WebServerService's touch endpoint. +bool touch_override_active = false; +SdlPointerState touch_override = { 0, 0, false }; +uint32_t touch_override_until_tick = 0; +#define SIM_TOUCH_HOLD_MS 1500 + +extern "C" void sdl_input_set_touch_override(int32_t x, int32_t y, bool pressed) { + touch_override.x = x; + touch_override.y = y; + touch_override.pressed = pressed; + touch_override_active = pressed; + if (pressed) { + touch_override_until_tick = SDL_GetTicks() + SIM_TOUCH_HOLD_MS; + } +} + +extern "C" void sdl_input_clear_touch_override(void) { + touch_override_active = false; + touch_override.pressed = false; +} + +namespace { + uint32_t key_queue[KEY_QUEUE_CAPACITY]; size_t key_queue_head = 0; size_t key_queue_count = 0; @@ -70,7 +98,7 @@ uint32_t keycode_to_key(SDL_Keycode sdl_key, bool shift) { } } -} +} // namespace void sdl_input_pump() { if (!text_input_started) { @@ -125,6 +153,17 @@ void sdl_input_pump() { } void sdl_input_get_pointer_state(SdlPointerState* out_state) { + if (touch_override_active) { + // Auto-release: viewer sends press only; LVGL needs press then release + // to register a click. Hold long enough for several indev polls. + if ((int32_t)(SDL_GetTicks() - touch_override_until_tick) >= 0) { + touch_override_active = false; + touch_override.pressed = false; + } else { + *out_state = touch_override; + return; + } + } *out_state = pointer_state; } diff --git a/Devices/simulator/Source/drivers/sdl_input.h b/Devices/simulator/Source/drivers/sdl_input.h index 8e7158ef..d7bee752 100644 --- a/Devices/simulator/Source/drivers/sdl_input.h +++ b/Devices/simulator/Source/drivers/sdl_input.h @@ -42,6 +42,16 @@ bool sdl_input_pop_key(uint32_t* out_key); */ bool sdl_input_has_queued_key(void); +/** + * @brief Web-injected touch override (for headless sim viewer without SDL window). + * When active, sdl_pointer_get_touched_points() reports this state instead of + * the SDL mouse state. Coordinates are in LVGL logical pixels (e.g. 640x480). + * Pass pressed=false to release. Auto-releases after SIM_TOUCH_HOLD_MS unless + * refreshed (tap = press, wait, release handled by the web viewer). + */ +void sdl_input_set_touch_override(int32_t x, int32_t y, bool pressed); +void sdl_input_clear_touch_override(void); + #ifdef __cplusplus } #endif diff --git a/Tactility/Private/Tactility/service/webserver/WebServerService.h b/Tactility/Private/Tactility/service/webserver/WebServerService.h index 32124437..6ca08467 100644 --- a/Tactility/Private/Tactility/service/webserver/WebServerService.h +++ b/Tactility/Private/Tactility/service/webserver/WebServerService.h @@ -81,6 +81,8 @@ private: static error_t handleApiAppsInstall(struct HttpServerRequest* request, void* user_ctx); static error_t handleApiWifi(struct HttpServerRequest* request, void* user_ctx); static error_t handleApiScreenshot(struct HttpServerRequest* request, void* user_ctx); + static error_t handleApiSimTouch(struct HttpServerRequest* request, void* user_ctx); + static error_t handleSimViewer(struct HttpServerRequest* request, void* user_ctx); #ifdef ESP_PLATFORM static error_t handleApiMcp(struct HttpServerRequest* request, void* user_ctx); static error_t handleApiScreenRaw(struct HttpServerRequest* request, void* user_ctx); diff --git a/Tactility/Source/service/webserver/WebServerService.cpp b/Tactility/Source/service/webserver/WebServerService.cpp index 698172cd..b4052a3a 100644 --- a/Tactility/Source/service/webserver/WebServerService.cpp +++ b/Tactility/Source/service/webserver/WebServerService.cpp @@ -31,6 +31,13 @@ #include "app/install.h" #include "app/manager.h" +#ifndef ESP_PLATFORM +// Simulator touch injection backend (Devices/simulator sdl-pointer driver). +// Declared extern "C" here instead of including the device header: Tactility +// must not depend upward on a device implementation. +extern "C" void sdl_input_set_touch_override(int32_t x, int32_t y, bool pressed); +#endif + #ifdef ESP_PLATFORM #include #include @@ -215,7 +222,11 @@ bool WebServerService::onStart(ServiceContext& service) { lock.lock(); g_cachedSettings = settings::webserver::loadOrGetDefault(); g_settingsCached = true; +#ifdef ESP_PLATFORM serverEnabled = g_cachedSettings.webServerEnabled || settings::mcp::loadOrGetDefault().mcpEnabled; +#else + serverEnabled = g_cachedSettings.webServerEnabled; +#endif } // Subscribe to settings change events to refresh cache settingsEventSubscription = pubsub->subscribe([](WebServerEvent event) { @@ -224,7 +235,11 @@ bool WebServerService::onStart(ServiceContext& service) { lock.lock(); g_cachedSettings = settings::webserver::loadOrGetDefault(); g_settingsCached = true; +#ifdef ESP_PLATFORM const bool enabled = g_cachedSettings.webServerEnabled || settings::mcp::loadOrGetDefault().mcpEnabled; +#else + const bool enabled = g_cachedSettings.webServerEnabled; +#endif if (g_webServerInstance.load() != nullptr) { g_webServerInstance.load()->setEnabled(enabled); } @@ -1058,6 +1073,9 @@ error_t WebServerService::handleApiGet(HttpServerRequest* request, void* user_ct if (strncmp(uri, "/api/apps", 9) == 0) { return handleApiApps(request, user_ctx); } + if (strncmp(uri, "/api/sim/touch", 14) == 0) { + return handleApiSimTouch(request, user_ctx); + } if (strncmp(uri, "/api/wifi", 9) == 0) { return handleApiWifi(request, user_ctx); } @@ -1083,6 +1101,9 @@ error_t WebServerService::handleApiPost(HttpServerRequest* request, void* user_c if (strncmp(uri, "/api/apps/run", 13) == 0) { return handleApiAppsRun(request, user_ctx); } + if (strncmp(uri, "/api/sim/touch", 14) == 0) { + return handleApiSimTouch(request, user_ctx); + } if (strncmp(uri, "/api/apps/uninstall", 19) == 0) { return handleApiAppsUninstall(request, user_ctx); } @@ -1764,6 +1785,72 @@ error_t WebServerService::handleReboot(HttpServerRequest* request, void*) { return ERROR_NONE; // Unreachable on ESP_PLATFORM, but satisfies function signature } +// POST /api/sim/touch?x=123&y=456[&down=0|1] - inject touch into simulator. +// Simulator-only: on ESP32 there is no sdl-pointer backend, so this 404s. +// x/y are LVGL logical pixels (sim display is 640x480). down=1 (default) +// presses and auto-releases after ~1.5s (long enough for LVGL indev polls to +// register a click); down=0 releases immediately. +error_t WebServerService::handleApiSimTouch(HttpServerRequest* request, void*) { +#ifdef ESP_PLATFORM + http_server_request_send_error(request, 404, "simulator only"); + return ERROR_UNDEFINED; +#else + std::string sx, sy, sdown; + int x = -1, y = -1; + if (getQueryParam(request, "x", sx)) x = atoi(sx.c_str()); + if (getQueryParam(request, "y", sy)) y = atoi(sy.c_str()); + bool down = true; + if (getQueryParam(request, "down", sdown)) down = (sdown != "0"); + if (x < 0 || y < 0 || x > 4096 || y > 4096) { + http_server_request_send_error(request, 400, "x and y params required (0..4096)"); + return ERROR_UNDEFINED; + } + sdl_input_set_touch_override((int32_t)x, (int32_t)y, down); + LOG_I(TAG, "[200] /api/sim/touch x=%d y=%d down=%d", x, y, (int)down); + http_server_request_send_string(request, "ok"); + return ERROR_NONE; +#endif +} + +// GET /sim - live simulator viewer: MJPEG-style screenshot refresh + click-to-touch. +error_t WebServerService::handleSimViewer(HttpServerRequest* request, void*) { + bool authPassed = false; + error_t authResult = validateRequestAuth(request, authPassed); + if (!authPassed) { + return authResult; + } + http_server_request_set_content_type(request, "text/html"); + static const char* page = + "" + "Tactility Sim" + "
Tactility Simlive" + "
" + "sim screen" + "
click/tap the screen to touch · 640x480 logical
" + ""; + http_server_request_send_string(request, page); + return ERROR_NONE; +} + error_t WebServerService::handleAssets(HttpServerRequest* request, void*) { // Auth check for UI access control bool authPassed = false; @@ -1776,6 +1863,11 @@ error_t WebServerService::handleAssets(HttpServerRequest* request, void*) { http_server_request_get_uri(request, uri, sizeof(uri)); LOG_I(TAG, "GET %s", uri); + // Simulator live viewer (no auth bypass: checked inside handler) + if (strncmp(uri, "/sim", 4) == 0 && (uri[4] == '\0' || uri[4] == '?' || uri[4] == '/')) { + return handleSimViewer(request, nullptr); + } + // Special case: serve favicon from system assets if (strcmp(uri, "/favicon.ico") == 0) { std::string faviconPathStr = std::string(file::MOUNT_POINT_SYSTEM) + "/spinner.png";