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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <Tactility/mcp/McpSystem.h>
|
||||
#include <Tactility/settings/McpSettings.h>
|
||||
@@ -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 =
|
||||
"<!doctype html><html><head><meta charset=utf-8><meta name=viewport "
|
||||
"content='width=device-width,initial-scale=1'>"
|
||||
"<title>Tactility Sim</title><style>"
|
||||
"body{margin:0;background:#111;color:#eee;font-family:sans-serif;"
|
||||
"display:flex;flex-direction:column;align-items:center;gap:8px;padding:12px}"
|
||||
"img{width:min(96vw,720px);image-rendering:pixelated;border:1px solid #333;"
|
||||
"border-radius:8px;touch-action:none;cursor:crosshair;background:#000}"
|
||||
".bar{display:flex;gap:8px;align-items:center;font-size:13px;color:#aaa}"
|
||||
"button{background:#313244;color:#fff;border:0;border-radius:8px;padding:6px 12px}"
|
||||
"</style></head><body>"
|
||||
"<div class=bar><b>Tactility Sim</b><span id=st>live</span>"
|
||||
"<button onclick='snap()'>refresh</button></div>"
|
||||
"<img id=scr alt='sim screen'>"
|
||||
"<div class=bar>click/tap the screen to touch · 640x480 logical</div>"
|
||||
"<script>"
|
||||
"const img=document.getElementById('scr'),st=document.getElementById('st');"
|
||||
"function snap(){img.src='/api/screenshot?ts='+Date.now();}"
|
||||
"setInterval(snap,2000);snap();"
|
||||
"img.addEventListener('error',()=>{st.textContent='reconnecting…';});"
|
||||
"img.addEventListener('load',()=>{st.textContent='live';});"
|
||||
"img.addEventListener('pointerdown',e=>{"
|
||||
"const r=img.getBoundingClientRect();"
|
||||
"const x=Math.round((e.clientX-r.left)/r.width*640);"
|
||||
"const y=Math.round((e.clientY-r.top)/r.height*480);"
|
||||
"fetch('/api/sim/touch?x='+x+'&y='+y,{method:'POST'});});"
|
||||
"</script></body></html>";
|
||||
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";
|
||||
|
||||
Reference in New Issue
Block a user