mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
Document the web remote control
README gets a section with the endpoint table, the two constraints that will otherwise surprise someone (single QMP client, no authentication), and the reason frames go through memsave rather than pmemsave or gdb. AGENTS.md gets the run instructions plus a new entry under 'Things that already went wrong' for the pmemsave trap: it takes a physical address, returns zeros for gFrameBuffer, and reports success. The web UI was built on it initially because a benchmark showed it was fast -- the benchmark never checked the contents. Worth recording as the general lesson, not just the specific fix.
This commit is contained in:
1 parent
db1117d61f
commit
2d0fc24b62
3 files changed
+89
No files matched your search
@@ -13,3 +13,7 @@ build/
|
||||
|
||||
# Plan documents from the plan skill: local working notes.
|
||||
.hermes/
|
||||
|
||||
# Python bytecode from the tools tests.
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -63,6 +63,24 @@ session:
|
||||
|
||||
python3 tools/keypad_test.py
|
||||
|
||||
There is also a browser UI, which is usually the quickest way to poke at the
|
||||
firmware by hand:
|
||||
|
||||
python3 tools/webui.py --frame-addr 0x200013DC \
|
||||
--status-addr 0x2000175C # then open http://127.0.0.1:8080/
|
||||
|
||||
Two things about it that matter when working on this repo:
|
||||
|
||||
- **It holds the QMP socket for its lifetime**, so `key.py` cannot run at the same
|
||||
time. The socket accepts a single client.
|
||||
- **It reads frames with QMP `memsave`, deliberately.** Not `pmemsave`, which
|
||||
takes a *physical* address and silently returns zeros for `gFrameBuffer` --
|
||||
a blank screen with no error. And not gdb, which halts the guest on every
|
||||
attach: that stutters the stream and perturbs key debounce timing.
|
||||
|
||||
Its tests: `tools/test_uvk5_*.py` and `tools/test_webui.py` need no emulator,
|
||||
`tools/test_webui_e2e.py` boots its own.
|
||||
|
||||
## Things that already went wrong
|
||||
|
||||
**GDB breakpoints halt the guest.** A key held across a breakpoint session is
|
||||
@@ -98,6 +116,14 @@ reported `IDR=0x0000` for several rounds because its regex did not match gdb's
|
||||
output format at all. The register was fine; the reader was broken. Cross-check
|
||||
with `tools/gpiob_dump.sh`, which uses a different path.
|
||||
|
||||
**QMP `pmemsave` is physical, `memsave` is virtual.** The framebuffer symbols are
|
||||
CPU virtual addresses, so `pmemsave` on `gFrameBuffer` returns a block of zeros
|
||||
and reports success -- a blank screen with nothing logged anywhere. The web UI was
|
||||
built on `pmemsave` first because a timing benchmark said it was fast; the
|
||||
benchmark never checked the *contents*. Measure the thing you actually care
|
||||
about: the bug surfaced only when a rendered frame came back with 0 lit pixels
|
||||
where the gdb path reported 1693.
|
||||
|
||||
## The keypad: two real bugs, both fixed
|
||||
|
||||
The old note here said "keys reach the firmware but the UI does not react" and
|
||||
|
||||
@@ -70,6 +70,10 @@ keypresses silently stop working. Run the test after touching that code;
|
||||
docs/screenshots/ LCD captures used in this README
|
||||
tools/ run, screenshot, inject keys, probe state
|
||||
keypad_test.py keypad regression test, boots its own instance
|
||||
webui.py web remote control: live LCD plus clickable keypad
|
||||
uvk5_qmp.py QMP client
|
||||
uvk5_lcd.py framebuffer decode, PNG encode, frame grabber
|
||||
uvk5_keys.py key names the keypad model accepts
|
||||
harness/, stubs/, shim/, tests/ host build of the CW timing chain (stage A)
|
||||
|
||||
## Building
|
||||
@@ -108,6 +112,12 @@ This matters more than it looks. The keypad can break silently under -O2 without
|
||||
any compiler warning -- see the `volatile` note in [Status](#status) -- so a clean
|
||||
build is not evidence that keypresses work.
|
||||
|
||||
The rest of the tests:
|
||||
|
||||
cd tools && python3 -m unittest discover -p 'test_uvk5*.py' -v # fast, no emulator
|
||||
cd tools && python3 -m unittest test_webui -v # fast, no emulator
|
||||
python3 tools/test_webui_e2e.py # boots its own emulator
|
||||
|
||||
## Running
|
||||
|
||||
python3 tools/make_flash.py # once, builds assets/flash.img
|
||||
@@ -128,6 +138,55 @@ between builds. Find them with:
|
||||
|
||||
arm-none-eabi-nm firmware.elf | grep -E 'gFrameBuffer|gStatusLine'
|
||||
|
||||
## Web remote control
|
||||
|
||||
`tools/webui.py` serves the LCD and a clickable keypad, so the radio can be
|
||||
driven from a browser instead of `key.py` plus `screenshot.py`.
|
||||
|
||||
tools/run.sh # emulator first
|
||||
python3 tools/webui.py --frame-addr 0x200013DC \
|
||||
--status-addr 0x2000175C # then the server
|
||||
|
||||
Open <http://127.0.0.1:8080/>. The keypad is laid out like the radio, with the
|
||||
side keys alongside. Arrow keys, Enter (MENU), Esc (EXIT) and the digits are
|
||||
bound to the physical keys.
|
||||
|
||||
Press duration comes from how long you actually hold the button, because the
|
||||
firmware treats anything past 400 ms as a *held* key and dispatches it as a
|
||||
different event. The browser sends the two edges separately rather than asking
|
||||
the server for a fixed-length press.
|
||||
|
||||
Endpoints, if you want to script it:
|
||||
|
||||
| Route | Purpose |
|
||||
| --- | --- |
|
||||
| `GET /` | the page |
|
||||
| `GET /stream` | multipart PNG stream, up to 15 fps |
|
||||
| `GET /frame.png` | one frame |
|
||||
| `POST /api/key` | `{"key": "MENU", "action": "down"}` — also `up` or `tap` |
|
||||
| `POST /api/release-all` | release every key, if one ever sticks |
|
||||
| `GET /api/status` | QMP `query-status` |
|
||||
|
||||
Frames are read with QMP `memsave`, about 1.35 ms each, and the guest keeps
|
||||
running throughout. Two details there are easy to get wrong:
|
||||
|
||||
- **`memsave`, not `pmemsave`.** The framebuffer symbols are CPU virtual
|
||||
addresses. `pmemsave` treats its argument as physical and returns a block of
|
||||
zeros, so the screen renders blank with no error anywhere.
|
||||
- **Not gdb.** `screenshot.py` reads frames through gdb, which halts the guest on
|
||||
every attach. That is unusable for a live stream and it also perturbs key
|
||||
debounce timing.
|
||||
|
||||
Two constraints worth knowing before you use it:
|
||||
|
||||
- **The QMP socket takes one client.** While the server is up, `tools/key.py`
|
||||
cannot talk to the same emulator.
|
||||
- **There is no authentication.** It binds loopback, and anyone who reaches the
|
||||
port has full control of the emulated radio. Do not expose it.
|
||||
|
||||
There is no PTT button: the keypad model has no PTT line, so the `press` property
|
||||
rejects the name. Unknown keys are rejected with 400 rather than forwarded.
|
||||
|
||||
## How the machine is put together
|
||||
|
||||
Register layouts come from the vendor CMSIS header shipped with the firmware
|
||||
|
||||
Reference in new issue
Block a user