Files
uv-k5-v3-emulator/tools/key.py
T
mckero 1c9a2afe55 Fix key.py hold times; the keypad was never broken
key.py held every key for 2500 ms, on the assumption that guest time runs
fast during delays so a press needs a long wall-clock hold. That is wrong
for this path, and it is why the keypad looked dead.

The two SysTick mechanisms are separate. poll-boost accelerates counter
*reads* so SYSTICK_DelayUs converges; it does not speed up interrupt
delivery. Interrupts drive SysTick_Handler -> gNextTimeslice ->
APP_TimeSlice10ms -> CheckKeys at close to real time, so the firmware's
thresholds hold in wall clock as written: 20 ms to register a press,
400 ms to count as held.

2500 ms is ~250 ticks, six times past the long-press threshold, so every
press was dispatched as a hold. MAIN_Key_MENU acts only on a short release
and returns early when bKeyHeld is set, so nothing happened. Confirmed by
reading gDebounceCounter mid-hold: 317 after a 3 s hold, which also proves
the timeslice was running all along.

Now HOLD_MS=200 and LONG_HOLD_MS=900. Verified with screenshots: the menu
opens and UP/DOWN move through it.

Also here:
- Drop the three TRACE fprintfs. They fired on every keypad poll and
  buried the console; the matrix is confirmed working.
- Drop a redundant forward declaration of py32_spi_xfer_byte, silencing
  the only build warning.
- Document the real remaining gap: power save (~6 s after boot) stops the
  keypad scan and the model does not wake from it. Includes the two dead
  ends already ruled out by experiment, so nobody repeats them.
- Add README screenshots captured from guest memory.
2026-08-27 16:45:54 +01:00

126 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""Press keys on the emulated radio through its QMP socket.
The keypad model exposes a "press" property: writing a key name holds that key,
writing an empty string releases it. The firmware debounces over several 10 ms
polls, so a press has to be held for a while to register -- see HOLD_MS.
Usage:
key.py MENU # one short press
key.py MENU UP UP EXIT # a sequence
key.py --long F # long press
key.py --list # show key names
"""
import argparse
import json
import socket
import sys
import time
QMP_SOCKET = "/tmp/uvk5-qmp.sock"
KEYPAD_PATH = "/machine/keypad"
# App/app/app.c debounces in 10 ms timeslices driven by the SysTick interrupt:
# key_debounce_10ms = 2 -> 20 ms to register a press
# key_repeat_delay_10ms = 40 -> 400 ms counts as a key *held*
#
# SysTick *interrupts* fire at close to real time here, so these thresholds apply
# in wall clock as written. The `poll-boost` property accelerates SysTick counter
# *reads* (so SYSTICK_DelayUs converges); it does not speed up interrupt delivery.
# Do not conflate the two -- an earlier HOLD_MS of 2500 assumed it did, which put
# every press ~250 ticks past the long-press threshold. Handlers like
# MAIN_Key_MENU act only on a short release and return early when bKeyHeld is
# set, so the UI appeared to ignore every key.
HOLD_MS = 200 # ~20 ticks: past debounce, well short of the 40-tick hold
LONG_HOLD_MS = 900 # ~90 ticks: comfortably past the hold threshold
GAP_MS = 400 # let the release be debounced before the next press
KEYS = [
"MENU", "UP", "DOWN", "EXIT", "F", "STAR",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"SIDE1", "SIDE2",
]
class Qmp:
"""Minimal QMP client: connect, negotiate, send commands."""
def __init__(self, path: str):
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
self.sock.connect(path)
except (FileNotFoundError, ConnectionRefusedError) as exc:
raise SystemExit(
f"cannot reach the emulator at {path}: {exc}\n"
"Start it with sim/tools/run.sh first."
) from exc
self.buf = b""
self._read_json() # greeting
self.command("qmp_capabilities")
def _read_json(self) -> dict:
while b"\n" not in self.buf:
chunk = self.sock.recv(4096)
if not chunk:
raise SystemExit("emulator closed the QMP connection")
self.buf += chunk
line, self.buf = self.buf.split(b"\n", 1)
return json.loads(line)
def command(self, name: str, **args) -> dict:
payload = {"execute": name}
if args:
payload["arguments"] = args
self.sock.sendall(json.dumps(payload).encode() + b"\n")
while True:
msg = self._read_json()
if "error" in msg:
raise SystemExit(f"QMP error: {msg['error'].get('desc', msg['error'])}")
if "return" in msg:
return msg["return"]
# Events (RESET, STOP, ...) arrive interleaved; keep reading.
def set_key(self, value: str) -> None:
self.command("qom-set", path=KEYPAD_PATH, property="press", value=value)
def press(qmp: Qmp, key: str, hold_ms: int) -> None:
qmp.set_key(key)
time.sleep(hold_ms / 1000)
qmp.set_key("")
time.sleep(GAP_MS / 1000)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("keys", nargs="*", help="key names to press in order")
ap.add_argument("--long", action="store_true", help="hold each key longer")
ap.add_argument("--hold", type=int, help="hold time in ms, overrides --long")
ap.add_argument("--list", action="store_true", help="list key names and exit")
ap.add_argument("--socket", default=QMP_SOCKET)
args = ap.parse_args()
if args.list:
print(" ".join(KEYS))
return 0
if not args.keys:
ap.error("no keys given (try --list)")
unknown = [k for k in args.keys if k.upper() not in KEYS]
if unknown:
raise SystemExit(f"unknown key(s): {', '.join(unknown)}\nKnown: {' '.join(KEYS)}")
hold = args.hold if args.hold else (LONG_HOLD_MS if args.long else HOLD_MS)
qmp = Qmp(args.socket)
for key in args.keys:
press(qmp, key.upper(), hold)
print(f"pressed {key.upper()} ({hold} ms)")
return 0
if __name__ == "__main__":
sys.exit(main())