mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
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:
1 parent
e7f3f9da76
commit
7fe605eeaf
2 files changed
+40
-22
No files matched your search
+23
-10
@@ -137,16 +137,6 @@ class TestFrontEnd(unittest.TestCase):
|
|||||||
for key in webui.KEYS:
|
for key in webui.KEYS:
|
||||||
self.assertIn(f'data-key="{key}"', self.body)
|
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):
|
def test_binds_pointer_and_keyboard_input(self):
|
||||||
self.assertIn("pointerdown", self.body)
|
self.assertIn("pointerdown", self.body)
|
||||||
self.assertIn("keydown", self.body)
|
self.assertIn("keydown", self.body)
|
||||||
@@ -207,5 +197,28 @@ class TestHoldMs(unittest.TestCase):
|
|||||||
self.assertEqual(resp.get_json()["hold_ms"], 900)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
+17
-12
@@ -218,21 +218,23 @@ def render_index(scale: int) -> str:
|
|||||||
<div class="pad">{grid}</div>
|
<div class="pad">{grid}</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="status">connecting...</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,
|
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
|
Esc is EXIT, digits map straight through. No PTT button -- the keypad model
|
||||||
has no PTT line.</p>
|
has no PTT line.</p>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const BINDINGS = {json.dumps(KEY_BINDINGS)};
|
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 {{
|
try {{
|
||||||
await fetch('/api/key', {{
|
await fetch('/api/key', {{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {{'Content-Type': 'application/json'}},
|
headers: {{'Content-Type': 'application/json'}},
|
||||||
body: JSON.stringify({{key: key, action: action}})
|
body: JSON.stringify({{key: key, hold_ms: Math.round(holdMs)}})
|
||||||
}});
|
}});
|
||||||
}} catch (err) {{
|
}} catch (err) {{
|
||||||
document.getElementById('status').textContent = 'send failed: ' + err;
|
document.getElementById('status').textContent = 'send failed: ' + err;
|
||||||
@@ -244,18 +246,21 @@ function mark(key, on) {{
|
|||||||
.forEach(el => el.classList.toggle('active', 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) {{
|
function down(key) {{
|
||||||
if (held.has(key)) return;
|
if (pressedAt.has(key)) return;
|
||||||
held.add(key);
|
pressedAt.set(key, performance.now());
|
||||||
mark(key, true);
|
mark(key, true);
|
||||||
send(key, 'down');
|
|
||||||
}}
|
}}
|
||||||
function up(key) {{
|
function up(key) {{
|
||||||
if (!held.has(key)) return;
|
const started = pressedAt.get(key);
|
||||||
held.delete(key);
|
if (started === undefined) return;
|
||||||
|
pressedAt.delete(key);
|
||||||
mark(key, false);
|
mark(key, false);
|
||||||
send(key, 'up');
|
sendKey(key, Math.max(performance.now() - started, MIN_HOLD_MS));
|
||||||
}}
|
}}
|
||||||
|
|
||||||
document.querySelectorAll('.key').forEach(btn => {{
|
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.
|
// Release on blur, so losing focus mid-press cannot leave a key stuck down.
|
||||||
addEventListener('blur', () => {{
|
addEventListener('blur', () => {{
|
||||||
[...held].forEach(up);
|
[...pressedAt.keys()].forEach(up);
|
||||||
fetch('/api/release-all', {{method: 'POST'}}).catch(() => {{}});
|
fetch('/api/release-all', {{method: 'POST'}}).catch(() => {{}});
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user