feat(sim): board feature parity (reset/GPS/sensors/admin/keyboard) + input fixes

Rounds out the browser sim harness with the rest of the physical board's
interactions: a reset button (JS-driven, since board.reboot() is inert
under -sEXIT_RUNTIME=0), GPS input wired into a real LocationProvider via
new SimSensorManager, JS-settable battery/environment telemetry, an
admin/repeater-login test hook (sendRoomLogin against the default
"password"), and full physical-keyboard text entry (printable ASCII
passthrough into the existing KeyboardWidget, Tab->KEY_KB_ENTER submit).

Also fixes three real bugs found while exercising all of this in a real
browser:
- UITask.cpp's native-only stdin poll branch had no __EMSCRIPTEN__
  exclusion, so it also compiled into the wasm build and called a real,
  blocking window.prompt() on nearly every frame -- the actual cause of
  the reported time/controls jumping. Now gated to native only.
- 'n'/'p' were mapped as Next/Prev keyboard shortcuts, colliding with
  typing those literal letters. Removed the shortcuts; added explicit
  Next/Prev buttons to mesh.html (previously relied solely on them).
- Buttons grabbed native browser keyboard focus on click, so a later
  stray Enter/Space could silently re-trigger a previously-clicked button
  (e.g. Reset). mousedown now calls preventDefault() on all buttons.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-03 11:31:42 +02:00
co-authored by Claude Sonnet 5
parent d7242ddc21
commit 97a86216c6
9 changed files with 644 additions and 55 deletions
+50
View File
@@ -422,4 +422,54 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_test_open_dm_with_first_contact() {
return 1;
}
#endif
// Same idea as findFirstChatContact() above, but for the first known
// admin-loginable contact (a repeater or room server) instead of another
// chat instance.
static bool findFirstAdminContact(ContactInfo& out) {
int n = the_mesh.getNumContacts();
for (int i = 0; i < n; i++) {
ContactInfo ci;
if (the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, ci) &&
(ci.type == ADV_TYPE_REPEATER || ci.type == ADV_TYPE_ROOM)) {
out = ci;
return true;
}
}
return false;
}
#ifdef DISPLAY_CLASS
// Jumps the on-device UI straight to AdminScreen for the first known
// repeater/room contact -- UITask::openAdminFor(ci, false), the exact same
// function NearbyScreen's "Nodes" Hold-Enter admin action calls (see
// NearbyScreen.h:709), so a test harness can screenshot the real Admin
// screen instead of scripting contact-list navigation key-by-key. Returns
// 1 if a repeater/room contact was found and the screen switched, 0 if not.
extern "C" EMSCRIPTEN_KEEPALIVE int sim_test_open_admin_with_first_repeater() {
if (!g_sim_ready) return 0;
ContactInfo ci;
if (!findFirstAdminContact(ci)) return 0;
ui_task.openAdminFor(ci, false);
return 1;
}
#endif
// Submits a login against the first known repeater/room contact via
// MyMesh::sendRoomLogin() -- the exact same function AdminScreen's own
// submit button calls (examples/companion_radio/ui-new/AdminScreen.h) --
// so a test harness can verify the admin/password flow without scripting
// the on-device virtual keyboard. Only reports whether the login *request*
// was sent (matching sim_test_send_msg_to_first_contact()'s same
// synchronous-only contract) -- the actual accept/reject arrives async via
// AbstractUITask::onRoomLoginResult() and is visible on AdminScreen once
// sim_test_open_admin_with_first_repeater() has switched to it. Returns 1
// if sent, 0 if send failed, -1 if no repeater/room contact known yet.
extern "C" EMSCRIPTEN_KEEPALIVE int sim_test_login_first_repeater(const char* password) {
if (!g_sim_ready) return -1;
ContactInfo ci;
if (!findFirstAdminContact(ci)) return -1;
uint32_t est_timeout;
return the_mesh.sendRoomLogin(ci, password, est_timeout) ? 1 : 0;
}
#endif
+15 -1
View File
@@ -2424,7 +2424,21 @@ void UITask::loop() {
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
if (!_locked) enqueueKey(handleTripleClick(KEY_SELECT));
}
#elif defined(SIM_PLATFORM)
#elif defined(SIM_PLATFORM) && !defined(__EMSCRIPTEN__)
// Native terminal input ONLY -- this branch previously had no
// __EMSCRIPTEN__ exclusion, so it also compiled into the wasm build
// (SIM_PLATFORM is defined there too, and UI_HAS_JOYSTICK/PIN_USER_BTN
// are both unset for variants/sim). Every tick it called real select()/
// read() on fd 0; under Emscripten, with no stdin ever wired up, that
// hits the runtime's default TTY device, which falls back to a real,
// blocking window.prompt("Input: ") -- so every single browser tab
// running the wasm build was popping a native dialog on nearly every
// frame, discovered by seeing Playwright's page 'dialog' event fire
// continuously from the moment the module boots. The wasm build's own
// input already comes through sim_enqueue_key()/injectSimKey() (see
// above, in the #if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
// block) -- this stdin-poll branch was only ever meant for Phase 1's
// native terminal target.
// Native terminal input: stdin is put into raw/non-canonical mode by
// variants/sim/sim_main.cpp's main(), so keys arrive here one at a time
// with no Enter-to-submit line buffering. Non-blocking select() on fd 0