Correct the docs that said PTT did not exist

Three places claimed there was no PTT line, and one of them named the wrong pin
(GPIOC rather than PB10). All now say the same thing: PTT works, but not through
the key table, because the firmware reads its own pin instead of scanning it as a
matrix key.

Documents the transmit bar's two gates -- FUNCTION_TRANSMIT and gSetting_mic_bar,
the latter already on because blank flash reads 0xFF -- and why the release path
gets more attention than the press: a stuck PTT leaves every later test running
against a transmitting radio.

Also records the '.key' vs '.key[data-key]' trap for whoever adds the next
non-key button.
This commit is contained in:
mckero committed 2026-08-29 05:18:57 +01:00
1 parent 88994b1bd0
commit a310429eff
5 files changed
+46 -9

No files matched your search

+23
View File
@@ -459,6 +459,29 @@ Two constraints are not negotiable, both from untimed spin loops in the firmware
measuring. Not hypothetical: the first test run decoded 48 registers correctly and
still reported RSSI as 0 for precisely this reason.
### PTT, and the transmit level bar
PTT is not a matrix key. `GPIO_IsPttPressed` reads PB10 directly
(`driver/gpio.h:31`, active low), so the model gives it its own GPIO line rather than a
column/row intersection, exposed as a boolean `ptt` property on the keypad device.
That is what makes the transmit level bar reachable. `app/app.c:1700` draws it only
while `gCurrentFunction == FUNCTION_TRANSMIT` and `gSetting_mic_bar` is set — the
latter is `Data[7]` bit 4 at flash `0xA0A8` (`settings.c:423`), and blank flash reads
`0xFF`, so it is already on. The level itself comes from `REG_64` via
`BK4819_GetVoiceAmplitudeOut`.
**Treat the release as the important half.** A stuck PTT leaves the emulated radio
keyed, and every later test then runs against a transmitting radio. The web UI releases
on `pointerleave`, `pointercancel` and `pagehide`; `/api/release-all` clears PTT
explicitly, because an empty `press` does not touch it; and the endpoint rejects
non-boolean bodies so `{"held": "false"}` cannot key the transmitter by truthiness.
`tools/test_ptt.py` asserts the release, not just the press.
One trap worth knowing if you add another non-key button: the browser wired handlers
over `.key`, which matched the PTT button as well, and it has no `data-key` — so it
would have sent the key `"undefined"`. Use `.key[data-key]`.
### Reads were shifted one bit, and it hid everything else
Fixed in `ad88ee1`, but worth reading because of how long it stayed invisible.
+14 -2
View File
@@ -43,6 +43,7 @@ has no public datasheet, so its driver is the only specification available.
| Serial input, CPS programming protocol | works, `-serial` any chardev |
| BK4819 register interface | works, RSSI and status readable |
| S-meter | works via monitor (SIDE1); reads -53 dBm, S9+40 |
| PTT and transmit | works; TX annunciator, timer, and mic level bar |
| Timing accuracy | deliberately wrong, see [Timing](#timing) |
| Analogue RF behaviour | **not modelled and never will be**, see [AGENTS.md](AGENTS.md#the-bk4819-and-where-modelling-it-stops) |
@@ -83,6 +84,7 @@ keypresses silently stop working. Run the test after touching that code;
test_bk4819.py BK4819 register interface, RSSI not stuck at zero
test_bk4819_readback.sh register reads come back bit-aligned
test_smeter.py the S-meter reads a signal when monitoring
test_ptt.py PTT keys the radio and releases cleanly
lib_kill_emulator.sh cleanup that only ever kills emulators
webui.py web remote control: live LCD plus clickable keypad
dn42_firewall.sh restrict the web UI port to DN42 sources
@@ -129,6 +131,7 @@ Then check the build actually works, which takes about a minute:
python3 tools/test_bk4819.py
bash tools/test_bk4819_readback.sh
python3 tools/test_smeter.py
python3 tools/test_ptt.py
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
@@ -250,8 +253,17 @@ watching the counters rather than by assuming:
The rules do not survive a reboot. Re-run `apply`, or persist them with
`iptables-persistent`.
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.
PTT is separate from the keypad grid, because the firmware reads its own pin (PB10)
rather than scanning it as a matrix key. It has its own button in the UI and its own
endpoint, and it is held rather than tapped:
curl -X POST -H 'Content-Type: application/json' \
-d '{"held": true}' http://127.0.0.1:8080/api/ptt
Anything that ends a session releases it — dragging off the button, closing the tab,
or `POST /api/release-all` — so a client going away cannot leave the radio keyed.
The `press` property still rejects "PTT" as a key name; unknown keys get a 400 rather
than being forwarded.
## How the machine is put together
+3 -2
View File
@@ -31,8 +31,9 @@ class TestKeys(unittest.TestCase):
names = set(re.findall(r'"([^"]+)"', block.group(1)))
self.assertEqual(names, set(KEYS))
def test_ptt_is_not_offered(self):
# The keypad model has no PTT line; qom-set would error on it.
def test_ptt_is_not_offered_as_a_key(self):
# PTT works, but not through the key table: the firmware reads its own pin, so
# it is a separate boolean property. Setting "press" to PTT would error.
self.assertFalse(is_valid("PTT"))
def test_normalise_is_case_insensitive_and_strips(self):
+4 -3
View File
@@ -4,9 +4,10 @@
Mirrors keypad_key_names in qemu/py32f071.c. test_uvk5_keys.py parses that array
out of the model source and fails if the two drift apart.
PTT is absent on purpose: the keypad model has no PTT line -- it is wired
separately on GPIOC -- so the "press" property rejects the name. Offering a PTT
button would produce a QMP error rather than a transmission.
PTT is absent on purpose, and still is now that the model supports it: the firmware
reads PB10 directly rather than scanning PTT as a matrix key, so it is exposed as its
own boolean property and its own endpoint (POST /api/ptt) instead of a name here. The
"press" property rejects "PTT".
"""
KEYS = (
+2 -2
View File
@@ -471,8 +471,8 @@ def render_index(scale: int) -> str:
<p class="hint">How long you hold a key is measured here and sent as one
number, so the firmware sees exactly the press you made. Hold past 400 ms for a
long press, which the firmware treats as a separate event and which repeats.
Arrows move, Enter is MENU, Esc is EXIT, digits map straight through. No PTT
button -- the keypad model has no PTT line.</p>
Arrows move, Enter is MENU, Esc is EXIT, digits map straight through. PTT is held
rather than measured, and releases if you drag off it or close the tab.</p>
</div>
<script>
const BINDINGS = {json.dumps(KEY_BINDINGS)};