Send one request per keypress carrying the measured duration

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.
This commit is contained in:
mckero committed 2026-08-28 05:41:59 +01:00
1 parent e7f3f9da76
commit 7fe605eeaf
2 files changed
+40 -22

No files matched your search

+23 -10
View File
@@ -137,16 +137,6 @@ class TestFrontEnd(unittest.TestCase):
for key in webui.KEYS:
self.assertIn(f'data-key="{key}"', self.body)
def test_sends_down_and_up_not_just_tap(self):
"""Real press duration must come from the browser.
The firmware distinguishes a short press from a held key at 400 ms, so
the front end has to send the two edges separately rather than asking the
server for a fixed-length tap.
"""
self.assertIn("send(key, 'down')", self.body)
self.assertIn("send(key, 'up')", self.body)
def test_binds_pointer_and_keyboard_input(self):
self.assertIn("pointerdown", self.body)
self.assertIn("keydown", self.body)
@@ -207,5 +197,28 @@ class TestHoldMs(unittest.TestCase):
self.assertEqual(resp.get_json()["hold_ms"], 900)
class TestFrontEndHoldMs(unittest.TestCase):
"""The page must send one request per key, carrying a measured duration."""
def setUp(self):
_, http = make_app()
self.body = http.get("/").get_data(as_text=True)
def test_sends_hold_ms(self):
self.assertIn("hold_ms", self.body)
def test_measures_press_duration_in_the_browser(self):
self.assertIn("performance.now()", self.body)
def test_does_not_send_separate_down_and_up_for_taps(self):
"""Two requests per key double the latency and break at 400 ms RTT."""
self.assertNotIn("send(key, 'down')", self.body)
self.assertNotIn("send(key, 'up')", self.body)
def test_enforces_a_minimum_hold(self):
"""A very fast click still has to clear the 20 ms debounce."""
self.assertIn("MIN_HOLD_MS", self.body)
if __name__ == "__main__":
unittest.main()
+17 -12
View File
@@ -218,21 +218,23 @@ def render_index(scale: int) -> str:
<div class="pad">{grid}</div>
</div>
<div id="status">connecting...</div>
<p class="hint">Hold a key to send a long press: over 400 ms the firmware
<p class="hint">How long you hold a key is measured here and sent as a number,
so a slow link cannot turn a tap into a long press. Over 400 ms the firmware
treats it as held, which is a different event. Arrows move, Enter is MENU,
Esc is EXIT, digits map straight through. No PTT button -- the keypad model
has no PTT line.</p>
</div>
<script>
const BINDINGS = {json.dumps(KEY_BINDINGS)};
const held = new Set();
const MIN_HOLD_MS = {MIN_HOLD_MS};
const pressedAt = new Map();
async function send(key, action) {{
async function sendKey(key, holdMs) {{
try {{
await fetch('/api/key', {{
method: 'POST',
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({{key: key, action: action}})
body: JSON.stringify({{key: key, hold_ms: Math.round(holdMs)}})
}});
}} catch (err) {{
document.getElementById('status').textContent = 'send failed: ' + err;
@@ -244,18 +246,21 @@ function mark(key, on) {{
.forEach(el => el.classList.toggle('active', on));
}}
// Press duration is decided by the browser, not the server.
// One request per key, carrying the duration as a number. Sending 'down' and
// 'up' as two requests would put the network round trip inside the press: at
// 400 ms RTT every tap arrived as a ~407 ms hold, which the firmware dispatches
// as a held key and MAIN_Key_MENU ignores.
function down(key) {{
if (held.has(key)) return;
held.add(key);
if (pressedAt.has(key)) return;
pressedAt.set(key, performance.now());
mark(key, true);
send(key, 'down');
}}
function up(key) {{
if (!held.has(key)) return;
held.delete(key);
const started = pressedAt.get(key);
if (started === undefined) return;
pressedAt.delete(key);
mark(key, false);
send(key, 'up');
sendKey(key, Math.max(performance.now() - started, MIN_HOLD_MS));
}}
document.querySelectorAll('.key').forEach(btn => {{
@@ -281,7 +286,7 @@ addEventListener('keyup', ev => {{
}});
// Release on blur, so losing focus mid-press cannot leave a key stuck down.
addEventListener('blur', () => {{
[...held].forEach(up);
[...pressedAt.keys()].forEach(up);
fetch('/api/release-all', {{method: 'POST'}}).catch(() => {{}});
}});