Kenwood TM-D710: Phase 1 capture and the memory line (#113) - #114
Open
ww8l wants to merge 15 commits into
Open
Conversation
ww8l
force-pushed
the
issue-113-kenwood-tmd710
branch
from
September 1, 2026 22:16
7227826 to
abf1835
Compare
The TM-D710 is a **live-mode** radio and the fourth programming modality in this app: no clone image and no card file, one ASCII command per memory, `\r` terminated, the radio answering in kind. Every other driver here clones an image, writes binary records at flash addresses, or patches a file the radio wrote to a microSD card. Measured on Tim's radio, not inherited: ID -> ID TM-D710 non-G, K-type baud -> 57600 CHIRP's driver assumes 9600; this is silent there ME 999 -> N an empty slot, 962 of 1000 round trip-> 17 ms all 1000 slots in 17.2 s MU -> 42 fields order anchored at p1 and p26 by single-change diffs Two published claims died on contact with the radio, both from the table this was about to generate code from: - LA3QMA documents menu brightness as `0=off, 1=max`. It is Menu 501, `OFF/LEVEL 1~LEVEL 8`. Measured by changing it to LEVEL 3 and watching p26 go 8 -> 3 alone. Generating from that enum would have shipped an 8-level control as a checkbox. - `MU` is **not exhaustive**: p28 is Menu 503 and p29 is Menu 507, so 504 CONTRAST, 505 DISPLAY REVERSE and 506 have no parameter at all. Also measured: the first command after opening the port can answer `?` because a previous write left the parser mid-line, so one `?` is not a refusal. `ask_settling` retries once; a second `?` is real. `memory.rs` models the `ME` line field by field and its gate is the live-mode form of the byte-identical re-encode every card radio here is held to: **all 38 captured slots re-emit character-identically.** Fields whose meaning is not established are carried through as the radio's own text, so a slot can be read and written back long before every field is understood. Shift is decoded against Tim's actual repeaters (447.275 and 145.310 minus, 147.360 plus) rather than against the documentation. The driver identifies and nothing else — the scaffolding stance the FT5D was registered under. It could claim `SettingsReader` today on the strength of `MU`, and deliberately does not: the same capability flag would put a settings write in front of an operator, and **nothing has ever been written to this radio**. A D710G is named and refused; its menu set is a different radio's and has not been measured. `kenwood_tmd710_probe.rs` is the measuring instrument that produced all of the above, `#[ignore]`d like the FT5D's `hw_probe` so `cargo test` stays hardware-free. npm run ci green: 446 tests, clippy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013cZgbB4FwqZ3Z4cdHt6vMH
Tim's counter-proposal, and a better shape than mine: build the write path first, then changes are made from here and verified on the radio's screen — instead of him setting things by hand for me to read back. Every write proves itself by reading the slot back and comparing the whole line. That is not belt-and-braces, it is the only evidence available: this radio has no checksum and no commit step, so a malformed line draws `?` but a well-formed line the radio interprets differently draws nothing at all. The D890UV had a settings field the firmware owned that silently reverted after a write; read-back is what makes that visible rather than a lie in the report. - `write_memory` / `write_name` — write, re-read, refuse to claim success unless the line comes back identical - `write_menu` — `MU` sets all 42 parameters at once, so it returns which ones did NOT take rather than throwing. A field the firmware owns is a finding, not an exception - `Menu` — the 42-field line, kept as text. `with_field` pads to the width the radio used, because `8` where the radio said `08` shifts every field after it: one badly formatted field is not one wrong setting, it is potentially forty-two. A too-wide value is refused - `d710_restore` — puts the radio back from the captured transcript. This is what makes writing to a real radio a reasonable thing to do at all HARDWARE, on Tim's radio: Ladder step 1 (identity write) PASSED. Slot 000 read, written back identically, re-read: byte-for-byte the same line. Ladder step 2 in flight: slots 500-503 were empty (`N`) and now hold known tone and DCS values for Tim to read off the radio's own screen. Nothing populated was touched. Also measured, free of radio time: joining the radio's 38 memories to Tim's channel library on frequency AND callsign, field 9 (Tone) predicts the library's TX tone 33 right / 0 wrong under "standard 42-tone CTCSS list, 0-indexed", while field 10 (CTCSS) gets 27/6 — so field 9 is the transmit tone and field 10 is a separate squelch tone. The ENDS of the table are what slots 500/501 are for; DCS is unmeasured entirely, which is what 502/503 are for. npm run ci green: 453 tests, clippy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013cZgbB4FwqZ3Z4cdHt6vMH
Phase 1 ended owing four readings off the radio's screen, because `023` in field 11 is both a plausible index and a real DCS code. The radio answers it itself: it validates a write and refuses it *whole*, leaving the slot exactly as it was, so acceptance is a measurement. 754 valid code, invalid index -> refused, slot stayed N 103 invalid code, valid index -> accepted 104 invalid either way -> refused, slot kept 103 Takes 103, refuses 754: an index. The same pair on fields 9 and 10 puts both at 0..=41. So 42 tones and 104 DCS codes, 0-based, all measured rather than inherited. The tables are Kenwood's own (TM-D710GA/GE manual, SIGNALING-1/2 — ⚠ that is the G, and the caveat is written down), and the transcription is checked, not trusted: joined against Tim's channel library on frequency AND callsign it reproduces session 120's number exactly, 33 right / 0 wrong. The manual's printed 01~42 keypad numbers are display order and are not where the 0-based offset comes from; the join is. Left unconfirmed on purpose: the DCS list's *order* has no cross-check, since not one of the radio's 38 memories uses DCS. Memory 503 holds dcs_idx 023 and should read D134 on the screen. Anything else means this table is wrong. Also here: - `ME nnn,C` clears a slot — documented since session 120, never run, now proven. `d710_restore` could overwrite a memory but not un-write one, so every slot the campaign created was permanent. `d710_clear_slots` closes that and refuses any slot present in memories.txt, so it cannot eat one of Tim's own channels. - `d710_read_slots`, read-only. Not `d710_dump_memories`: that one rewrites memories.txt, which is the only copy of the radio's as-found state. - The harness said the cable goes in the main unit's PC port. It does not — that is the D710G. Session 120 measured the operation panel's rear COM port and the doc never caught up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CrC78t5gKpZi3eJPhYN4tx
The encoder, and the nine field ranges it needed. All of them measured the
same way the tone tables were: this radio validates a write and refuses it
whole, so `d710_field_bounds` sweeps a field on an empty slot, reads back, and
the first refused value is the edge of the enum. Nine fields in eight seconds.
★★ A step that does not divide the frequency is refused. Field 3 came back
NON-CONTIGUOUS — [0,3,5,6,8] at 146.520 — which is the answer rather than a
problem: those are exactly the steps that divide it evenly. Against the table
[5, 6.25, 8.33, 10, 12.5, 15, 20, 25, 30, 50] kHz, four frequencies gave 40
accept/refuse predictions with no misses. 145.050 is what pins index 9 as
50 kHz and not 100 — it divides by one and not the other. A driver emitting a
fixed step would write memories the radio quietly declines to store.
★★ There is no split shift. Shift::Split = 3 came from CHIRP's table; the
radio refuses it with a zero offset, with a 600 kHz offset, and with a TX
frequency present. An odd split is field 14 alone, and a shift or offset
together with field 14 is refused in every combination tried. Fourth published
claim about this radio to die on contact with it. The variant stays parseable
so a line carrying one can be read, and a test asserts nothing the encoder can
be handed ever emits it.
Mode 2 = AM is pinned by the radio's own 118.400 memory — air band — and 0 = FM
by the 37 repeaters beside it; the field takes three values and Menu 102 offers
three modes, so 1 = NFM by elimination rather than by the menu's printed order.
Names: 94 of 95 printable characters survive verbatim, lowercase included. The
comma — the field separator — comes back as '+', and a ninth character is
silently truncated. Both are done in encode_name, which is what keeps
write_name's read-back a real check instead of a guaranteed failure.
The gate runs both ways. Each real memory is decoded into app terms and the
encoder asked to rebuild the line; all 38 do, with three fields excluded and
named. One of those exclusions is something the gate found: ME 040 (144.390,
APRS) is simplex and still carries a 600 kHz offset, so the offset field
persists whether or not the shift uses it.
Then on the radio itself — the check no unit test can make, because a value
this radio dislikes is not an error but a slot that quietly stays as it was:
d710_encoder_acceptance: 38 encoded memories accepted by the radio
each written to a spare slot, verified by read-back, and cleared.
Still owed and written down, not smoothed over: memory 503 should read D134 on
the screen (the DCS list's order is the last thing a cable cannot settle);
Cross Tone falls back to the transmit tone rather than guessing a flag pair,
since the radio accepts all eight combinations and refusal can measure
nothing; and the split is stored but unconfirmed on the air.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrC78t5gKpZi3eJPhYN4tx
…Phase 3) ★ rx_bands is measured, not copied out of a manual. The radio refuses an `ME` line it cannot hold, so coverage is the same accept/refuse question every other field answered this session: `d710_rx_band_sweep` wrote 1350 frequencies at 1 MHz into a spare slot and bisected both edges to 5 kHz. The answer is ONE contiguous span, 118.000-523.995 MHz, with no interior gap. That matters twice. The manual on hand is the TM-D710**G**'s and lists an 800-1300 MHz group this radio refuses outright, so a band table copied from it would have promised memories that cannot exist — and rx_bands has the worst failure mode in this project, an out-of-coverage frequency reported as written while the memory sits empty. Here it cannot: the sweep is the coverage. ⚠ tx_bands is NOT measured and must not be — the only way to ask this radio what it transmits on is to key it. 144-148 / 430-450 is the K-type allocation and `TY` answered K,0,3,1,0 on Tim's radio. A TX-modified radio is under-served by this row, which is the safe direction. covers_220 is TRUE where the TH-D72 beside it is false. This radio genuinely hears 220 — Tim's own memory 005 is a 224.840 repeater — so that channel must land RECEIVE-ONLY rather than be dropped. The Phase 3 gate is that distinction: a test seeds a real database and runs the four channel shapes through `channel_fit`, the same function the export preview and every program dialog call, and asserts the verdicts rather than the band arithmetic. ReceiveOnly and Excluded are different answers and only one of them keeps the repeater. Deliberately conservative, each with its reason in the seed comment: - banks_supported false. The radio has ten memory groups (Menu 203) and they are probably the D72's positional hundreds, but that is an inference from a sibling radio and this project has a rule about those. A flat 1000-slot pool is what the encoder writes today. - the settings schema is EMPTY. `MU` carries all 42 menu parameters and the driver parses them, but none is attributed to a named setting yet. Phase 4. - max_name_length 8 measured on the wire, not read off Menu 200 — a ninth character comes back silently truncated rather than refused. No migration: seeding INSERTs new (manufacturer, model) rows, so the model reaches existing databases on the next startup. Verified in dev — it is in the dev database with the measured bands, returned by the same query the Radios screen uses, and the app starts clean. The generic program dialog is capability-driven and this driver still claims no capability trait, so nothing here puts a write in front of an operator. That stays true until the hardware ladder is climbed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CrC78t5gKpZi3eJPhYN4tx
Seeding the TM-D710 exposed a hole. `showCable` is
isProgrammable(model) && (media == null || cableCapable)
so for a non-media radio it is true whatever the driver can do — `media ==
null` was standing in for "has a cable path", which had been true of every
non-media radio until now. The TM-D710 is the first driver registered with a
cable modality and NO capability trait, deliberately, because nothing had been
written to that radio yet.
Result: a live "Program radio" button that walked the operator through a
destructive-write confirmation and then failed at the backend with "Kenwood
TM-D710 cannot be programmed over the cable". Same shape as #65 — the gate
existed, the button just never consulted it.
The port section still belongs there (every registered driver implements
`identify`, and confirming the cable is useful), so `showCable` keeps that job
and a new `canWriteOverCable` gates the write affordances: the Program button
and the backup-then-write-then-verify safety banner, which was promising a run
that could not happen.
The codeplug summary keeps showing — it is the codeplug's FIT against the
model, and the receive-only count is the most useful thing in the dialog for a
radio you cannot write yet — but "Programming 62 channels into" is a promise,
so it reads "This codeplug holds 62 channels for" when nothing can be written.
Verified by rendering the dialog headless against stubbed capabilities: with
none, Close and Identify and no banner; with `program_codeplug`, the banner and
Program radio come back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrC78t5gKpZi3eJPhYN4tx
…hase 4)
The refusal trick works on `MU` too, and better: where a memory the radio
dislikes is acknowledged and quietly not stored, an out-of-range MENU value
draws an explicit `?`. So `d710_menu_bounds` sweeps a parameter, reads the line
back, and the first refused value is the size of the enum behind that menu.
All 42 in 131 seconds, menu restored exactly.
That is stronger evidence than the sheet the last two Kenwoods were built from,
and it caught FIVE errors in it:
p2, p6 "volume 0-7" -> 7 values (0..=6). The manual says "a level from
1 to 7", so display = stored PLUS ONE and 7 is
refused. A direct mapping would have been off by
one across the whole range.
p26 "brightness off/max" -> 9 values. Menu 501, Off + Level 1-8 — which
also settles session 120's 501-vs-504 ambiguity
by count alone, since CONTRAST has 16.
p29,p30 "PF key 0-16" -> 12 values, NON-CONTIGUOUS: 0x00-0x0A and 0x16.
p31-p34 "Mic PF key 0-16" -> 32 values, 0x00-0x1F.
35 of 42 are exposed. The other seven — the six PF-key assignments and p25,
which no source names — have measured ranges and undetermined meanings, so they
are left out rather than guessed, with a test asserting they stay out. The
non-contiguous PF-key set is the clearest case for that: even the shape of the
enum is unusual and nothing names the functions.
One sheet, both halves: `gen_tmd710_settings.py` parses `MEASURED.md` and emits
the Rust table and the profile schema together, refusing any row whose option
list is not exactly as long as the range the radio accepted. A test asserts the
pair still agrees afterwards.
The write is a PATCH, never a rebuild. `MU` sets all 42 at once, so anything the
profile does not carry — including all seven unexposed parameters — goes back
exactly as it came; otherwise changing the beep volume would silently rewrite
the operator's PF key assignments. Asserted in a test and again on the radio.
Proven on hardware, through the traits the app actually calls rather than the
command underneath them, because a working read path has twice hidden a dead
write path here:
d710_settings_roundtrip: read -> write one field -> re-read -> restore
the whole 42-parameter line came back byte-identical
The driver now claims SettingsReader and SettingsWriter. It claimed neither
before this, deliberately, because "nothing has ever been written to this radio"
was true.
Grading, stated rather than assumed: sizes are MEASURED; option ORDERS are
mostly inferred from the manual and the published table, which agree with each
other and now with the radio on 37 of 42 counts. A printed option list is
display order, not the stored index — that cost the TH-D75 a shipped wrong
meaning. MEASURED.md grades every row and names the four worth a screen check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrC78t5gKpZi3eJPhYN4tx
The driver claims `CodeplugProgrammer` — record-by-record from the database, the AnyTone's shape rather than a clone image, because a live-mode radio has no image to write. ★ This is the only NON-ATOMIC write in the app, and that is a requirement on the error path rather than a caveat in a doc comment. Every other radio here commits a whole image or patches a file the radio reads later; this one sends a memory, waits for the radio to take it, and sends the next, a thousand times. A failure halfway leaves the radio holding a MIXTURE of the new codeplug and what it had — nothing else in this repo can do that — so `stopped_at` names the slot it stopped on, how many landed, and the path to the backup. "Programming failed" tells an operator nothing when their radio is in that state. The backup is a transcript: all 1000 `ME`/`MN` lines, in the format `d710_restore` already reads, taken before the first byte goes out. Failing to save it aborts the run rather than proceeding unprotected. Per-channel problems are skips, not failures. A codeplug with one 159.8 Hz tone still programs the other sixty-one channels and says which one did not go — the encoder refuses rather than substituting a near value, and its reason is what the operator sees. Structural problems (wrong model, over capacity) stop the run instead. Both filters run, in the order the rest of the app uses them: `exclusion_reason` first, so the program and the export preview cannot disagree about what is in the codeplug, then the encoder. A 220 MHz repeater passes both and gets a memory — receive-only, not dropped — which is the case that separates this radio from the TH-D72 beside it in the seed. No zones, scan lists or contacts. The radio has ten memory groups and program-scan limit pairs and neither has been measured, which is why the seed row says `banks_supported: false`; channel lists flow into one flat pool. `sanitize_name` is now shared, because the program path names channels with `expanded_name` (the app's own disambiguated name) and that string needs the same 8-character cut and comma substitution the encoder does. Two places doing it differently would be two different names on the radio for one channel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CrC78t5gKpZi3eJPhYN4tx
Tim pulled settings off the radio and none of the APRS settings were there. He
is right, and it is not a gap in the schema — it is a gap in the TRANSPORT.
`MU` carries 42 menu parameters and stops at the 500-series. The TM-D710's APRS
and TNC settings are the 600-series menus and there is no `MU` parameter for a
single one of them, so no amount of work on `MU` reaches them. On a radio whose
headline feature is APRS, that is most of the point of the thing missing.
MCP-2A does not use `MU` either. It puts the radio into a block-transfer mode
and reads a memory image, so this radio is NOT purely live-mode after all — it
has a second transport, and everything `MU` cannot reach lives in there.
"0M PROGRAM\r" -> "0M\r" the display shows PROG MCP
R <addr:2 BE> <len:1> -> W <addr:2> <len:1> <data> (len 0 = 256)
host sends 06, radio answers 06, then the next
"E" -> 06 0D 00
32 512 bytes, 0x0000-0x7EFF, in 15 seconds.
★★ Two published details were wrong and each cost a dump. Both look exactly
like the radio refusing, and neither is:
1. The address is BIG-endian. The published note says little-endian, and
0x0000 — the first address anyone reads — is the same two bytes either
way, so the error survives being "tested". Stepping to 0x0100 sent `00 01`,
which the radio read as 0x0001: a 64 KiB dump that came back drifting
exactly one byte per block, the same content sliding. Settled by reading
offset 0x10, whose contents were already known from block 0.
2. The host must acknowledge each block with 0x06 and the radio answers 06.
Nothing published mentions it. Without it the first R of a session returns
a block and every one after times out. Three probes died on this before a
raw stream capture showed a header arriving as `06 57 00 00` — a status
byte, then W, then the address. I had been reading a stream one byte out
of step and calling it a refusal.
The map, and it cross-validates: memory channels at 0x1700, 16 bytes each,
frequency as 32-bit LITTLE-endian Hz — `f8 df a8 1a` = 447 275 000 = ME 000,
matching the transcript exactly for 000/001/002. (The frequency inside the
image is little-endian while the address on the wire is big-endian. Assuming
one from the other is how the first dump went wrong.) Memory names at 0x5800,
8 bytes each. Six 394-byte config blocks at 0x0200 + n*0x200 — the live
settings plus PM1-PM5, the radio's five Programmable Memory profiles.
Already located in the APRS half: 0x02E0 is Tim's callsign `WW8L`, FF-padded,
with `HELLO ` at the same offset in the untouched PM blocks. And the 42 MU
parameters are in the image too — the PF-key signature `00 02 14 15 0C 0E`
appears once, at 0x036B — so MU and the image are two views of the same
settings, the same relationship the TH-D72 has.
This commit is the transport and the map only. It is READ-ONLY, and it still
changes the radio's state: entering the mode puts `PROG MCP` on the display and
leaving it there strands the operator until they power-cycle, so the exit runs
inside a `catch_unwind` and `E` is sent whether the dump succeeds or not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrC78t5gKpZi3eJPhYN4tx
The TM-D710 (#113) shipped a 35-field settings schema built on its `MU` command. Every field was measured on the radio and every one was correct — and there was no APRS in it at all, on a radio whose headline feature is APRS and whose model name says so. Tim found it by pulling settings in the app. The manual names 115 menus. `MU` reaches 42. The 6xx group is 32 APRS and TNC menus and `MU` touches none of them — the largest group on the radio. One grep of the manual would have shown that on day one. The failure was SCOPE, not accuracy, which is why nothing looked wrong from the inside: when every measurement is right and the set is simply incomplete, there is no error to trip over. A published table covers what its author needed, and the gap between that and the radio is invisible without the radio's own list to hold it against. Step 1 gains a census that has to be done BEFORE reading any source in depth: what the radio is for (a TNC, a GPS, a second receiver — each is a family of settings, and manufacturer naming often carries it), the complete menu map by group from the manual, and the transport that reaches each group. A group with no transport is a finding, not an omission. The gate now requires the numbers, and `templates/PLAN.md` has a census table to fill in. Step 6 gains the reconciliation at the other end: "35 fields" is not a result, "35 of the 42 this transport reaches, and it reaches 42 of the radio's 115" is. Plus a cheap mechanical version — a model with `aprs_capable: true` and no APRS field in its schema is a contradiction a test can catch, and the D710 shipped exactly that pairing for a whole session. Two traps added. The second is the one that stings: "`MU` is not exhaustive — three menus have no parameter" had been sitting in the findings since session 120. It was the same fact as "an entire feature is unreachable", written small. A noted gap is a question — what else, and how would I know? — not a footnote. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CrC78t5gKpZi3eJPhYN4tx
…EADME (#113) `main` moved a long way under this branch — the Binteradio BT-9000 landed as the eighth radio, taking with it a shared guard this branch had never seen. `every_seeded_radio_appears_in_the_readme` (radios/wiring.rs) now FAILS CI for any seeded working driver missing from the README's Supported table, which is exactly what a nine-driver registry with the TM-D710 still under "Planned" produces. Moved to Supported, and the row says what this radio actually is: live-mode programming with no image file, so its backup is a transcript. Also: the drivers array is 9 now, and the two settings-capability arms merged rather than one replacing the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
ww8l
force-pushed
the
issue-113-kenwood-tmd710
branch
from
September 3, 2026 00:03
88193c7 to
528f6de
Compare
`d710_program_mode_dump` walked forward until a read failed, stopped at `0x7F00`, and reported 32 512 bytes as "the image". It is not the image. The radio answers nothing at `0x7F00` — re-measured, `timed out after 0 of 4 bytes` — and then answers again at `0x8000`. CHIRP's clone-mode driver skips exactly that block with the comment `# Skip block 7f !!??` and keeps reading to `0x9BFF`, plus two odd tails at `0xFEF0` and `0xFF00`. So about 7 KB has never been read, and it is the only place left for the 32 APRS and TNC menus of the 600-series — the feature this radio is named for. `d710_program_mode_dump_full` reads the whole ritual and writes a 64 KiB file with `FF` for anything unread, so a file offset IS a radio address. CHIRP concatenates its blocks instead, which shifts everything above the hole down by 0x100; that convention is fine for a driver and wrong for a measurement. `d710_leave_program_mode` exists because a probe that fails mid-block never reaches its own `E`, and the radio then answers `ID` with nothing at all — indistinguishable from a dead cable at exactly the moment someone starts unplugging things. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
…#113) Nothing had ever been written to this radio's image. The ladder from the `new-radio` skill, adapted from a container to a transport, run on status text 3 — empty on Tim's radio, so there was no operator data to lose, and visible on the radio's own screen, which is the half a read-back cannot supply: 1. identity write of the 42 bytes already there, read back unchanged 2. `CPMAGIC TEST 129` written, read back 3. out of program mode, back in, read again — still there Rung 3 is the one that counts. This protocol has no checksum and no commit step, so a value that survives leaving and re-entering the mode is the only evidence that anything was stored rather than echoed. On the BT-9000 an APRS block answered 0x06 four times and never changed a byte. ★★ A NARROW write commits. CHIRP writes all 156 blocks wrapped in an invalidate/revalidate dance — FF over the header bytes at 0x0000 and 0x8000, every block, then the headers back. This writes 42 bytes, touches no header, and sticks. That was the open question, and it is the difference between "to change a status text, rewrite the operator's entire radio" and not. `write_block` is the counterpart to `read_block` and the asymmetry is the trap: a READ is acknowledged by the host and the radio answers that acknowledgement; a WRITE is acknowledged by the radio and there is nothing to send back. 0x0F is the program-mode error state, not a refusal of the write in hand. `d710_restore_aprs_block` puts the live block back from a saved full dump, which is what makes a front-panel measurement pass reversible — the reason to build the write path before the campaign rather than after it. It writes only the live block; PM1-5 are the operator's saved profiles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
…driver (#113) `image.rs` owns the session now: enter, read, write, the full-image ritual, and `E` from `Drop`. The probes call it, so the code that will eventually read APRS settings for an operator is the same code that was proven on the radio — 39 840 bytes read back byte-identical to the harness version it replaces. Three things it encodes that each cost a session: - the address is big-endian, and 0x0000 reads the same either way, so the error survives being tested and the dump drifts one byte per block - a READ is acknowledged by the host and the radio answers that; a WRITE is acknowledged by the radio with nothing to send back. Backwards leaves the stream one byte out of step for everything after it - 0x7F00 is a hole, not the end. `read_plan` skips it the way CHIRP does, and `Image::slice` refuses an address that was never read rather than serving the FF filler — on this image FF is also an ordinary stored value, so "the field is empty" and "the field was never fetched" must not be the same answer `Drop` sends `E` because a radio left in PROG MCP answers nothing at all, which looks exactly like a dead cable; it cost a power cycle this session to learn. Eight tests against a fake that speaks both halves, ASCII then binary. They cannot prove the wire format — they replay the one measured on the radio — but they prove the sequencing: the hole skipped, the acknowledgement asymmetry, `E` sent when the caller never asks, an unread region refused, and an acknowledged write that did not commit showing up only on read-back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
Tim, this session: "don't do a bunch of PRs, just wait on it until we're pretty much done with a radio model." The standing rule is "open a PR for finished work", and a radio build produces a lot of things that look finished and are not the deliverable — a transport proven on hardware, a schema, a ladder rung. The radio is the deliverable. Keep pushing the branch; that is what runs CI on three OSes and it costs nothing. One PR, when channels and settings both work and the ladder is climbed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First half of #113, following the
new-radioskill. Phase 1 is complete and measured on the radio; Phase 2 is partial. The radio does not appear in the app yet — that is Phase 3.The TM-D710 is a fourth programming modality
No clone image and no card file. One ASCII command per memory,
\rterminated, the radio answering in kind:Every existing driver clones an image (UV-5R, TD-H3), writes binary records at flash addresses (AnyTone), or patches a file the radio wrote to a microSD card (FT5D, ID-52, TH-D75). Consequences: a write here is not atomic — a failure halfway leaves the radio half-programmed, which nothing else in this repo can do — and a "backup" is a transcript rather than a blob.
Measured, not inherited
ID TM-D710, K-type — non-Gkenwood_live.pyassumes 9600; this radio is silent thereN— 962 of 1000, zero unexpected repliesMUreads with nothing changedTwo published claims died on contact with the radio, both from the table this was about to generate code from:
0=off, 1=max. It is Menu 501,OFF/LEVEL 1~LEVEL 8. Proven by setting LEVEL 3 and watchingp26go8 -> 3alone. Generating a schema from that enum would have shipped an 8-level control as a checkbox. It was ambiguous on value alone — Menu 501 BRIGHTNESS and Menu 504 CONTRAST both default to 8, and the radio read 8 — so it was settled by changing it, not by position.MUis not exhaustive.p28is Menu 503 andp29is Menu 507, so 504 CONTRAST, 505 DISPLAY REVERSE and 506 have noMUparameter at all. Settings the operator can see are not all reachable this way.Also measured: the first command after opening the port can answer
?because a previous write left the parser mid-line. One?is not a refusal —ask_settlingretries once, a second?is real. That is measured behaviour, not defensive coding.The Phase 2 gate
memory.rsmodels theMEline field by field, and its gate is the live-mode form of the byte-identical re-encode every card radio here is held to: all 38 captured slots re-emit character-identically. Field widths are preserved rather than normalised —0and000are the same number and not the same line.Fields whose meaning is not established are carried through as the radio's own text, so a slot round-trips long before every field is understood. Shift is decoded against Tim's actual repeaters (447.275 and 145.310 minus, 147.360 plus), not against the documentation that describes them.
The full capture lives in gitignored
scratchpad/, so four real lines are embedded in the tests and a second test checks the whole corpus when it is present on this machine.Capabilities: none, deliberately
The driver identifies and nothing else — the scaffolding stance the FT5D was registered under. It could claim
SettingsReadertoday on the strength ofMUand does not: the same capability flag puts a settings write in front of an operator, and nothing has ever been written to this radio. The registry guard carries that reasoning. A D710G is named and refused — different menu set, not measured.Not done, and what it is waiting on
MEline from an app channel — blocked on the tone and DCS index tables. The captured indices (12,08,18) have no established meaning here, and a published table would be a guess about what a number means. One measurement pass settles it.tx_bands, andrx_bandsmodelling the receiver's real gaps. Until then no model names thisdriver_key, so nothing is reachable from the UI.Verification
npm run cigreen: 446 tests, clippy-D warningsclean. Nothing to verify in the dev app — with no seed entry, this changes nothing an operator can see, by design.🤖 Generated with Claude Code
https://claude.ai/code/session_013cZgbB4FwqZ3Z4cdHt6vMH