mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
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.
126 lines
4.4 KiB
Python
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())
|