The browser now times the press with performance.now() and posts hold_ms once,
instead of sending down and up as two requests. Halves the round trips per key and
makes press duration independent of the link.
Deletes test_sends_down_and_up_not_just_tap: it asserted the behaviour being
replaced, so keeping it would have meant asserting the bug.
MIN_HOLD_MS is injected into the page from webui.py so the two agree on the floor,
which exists because anything under the firmware's 20 ms debounce does not register
at all -- a very fast click still has to ask for 60 ms.
Verified over a simulated 400 ms link: three keys in 1.58 s where the old design
needed ~2.45 s, and a 120 ms press opens the menu (gScreenToDisplay 0 -> 1) with
DOWN then moving the cursor. The generated page also passes node --check.
POST /api/key now accepts {"key": "MENU", "hold_ms": 120} and holds the key for
exactly that long, locally.
The old design sent down and up as two requests so the browser would own press
duration. That is correct on loopback and broken over a real link: the round trip
between the two requests *is* the press duration. Measured against this server at
400 ms RTT, an intended tap arrived as a 407 ms hold, and since the firmware reads
400 ms as held (key_repeat_delay_10ms = 40), every short press was dispatched to
the hold path where MAIN_Key_MENU does nothing. Jitter either side of that
threshold is why it felt intermittent rather than simply broken.
Verified at 400 ms simulated RTT:
one request 531 ms total, firmware saw 120 ms -> short press
two requests 816 ms total, firmware saw 408 ms -> held (the bug)
hold_ms is clamped to MAX_HOLD_MS, rejects negatives and non-numbers, and defaults
to TAP_MS. A deliberate 900 ms hold is preserved, so long-press events still work.
down and up stay for scripting on a fast link.
Flask app on the existing QMP socket. GET / serves the page, GET /stream is a
multipart PNG sequence at up to 15 fps, POST /api/key drives the keypad model.
The keypad takes discrete down/up rather than a fixed-duration tap, because the
firmware reads anything past 400 ms as a held key and dispatches it differently.
Letting the browser own the timing is what makes both short and long presses
reachable; tap stays available for scripting and is pinned between the 20 ms
debounce and the 400 ms hold, with a test asserting that range.
The stream only re-encodes when the framebuffer bytes change. The LCD is static
most of the time, so idle CPU stays near zero.
Unknown key names are rejected with 400 before reaching QMP, so a PTT button
cannot be added by accident. The front end releases on pointerleave,
pointercancel and blur, and /api/release-all is the safety valve for a key that
somehow stays down.
Verified against a live emulator over HTTP: /frame.png renders the dual-VFO main
screen, a MENU tap opens the menu at 01/79, and two DOWN presses reach 03/79.
19 unit tests, 41 across the whole suite.