Skip to content

Kenwood TM-D710: Phase 1 capture and the memory line (#113) - #114

Open
ww8l wants to merge 2 commits into
mainfrom
issue-113-kenwood-tmd710
Open

Kenwood TM-D710: Phase 1 capture and the memory line (#113)#114
ww8l wants to merge 2 commits into
mainfrom
issue-113-kenwood-tmd710

Conversation

@ww8l

@ww8l ww8l commented Aug 22, 2026

Copy link
Copy Markdown
Owner

First half of #113, following the new-radio skill. 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, \r terminated, the radio answering in kind:

ME 000  ->  ME 000,0447275000,0,2,0,0,1,0,12,12,000,05000000,0,0000000000,0,0
ME 999  ->  N
MU      ->  MU 0,4,0,1,0,4,1,0,10,…      all 42 menu settings in one line

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

Identity ID TM-D710, K-type — non-G
Baud 57600. CHIRP's kenwood_live.py assumes 9600; this radio is silent there
Empty slot N — 962 of 1000, zero unexpected replies
Round trip 17 ms; all 1000 slots in 17.2 s
Noise floor zero across two MU reads with nothing changed

Two published claims died on contact with the radio, both from the table this was about to generate code from:

  • LA3QMA documents the brightness menu as 0=off, 1=max. It is Menu 501, OFF/LEVEL 1~LEVEL 8. Proven by setting LEVEL 3 and watching p26 go 8 -> 3 alone. 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.
  • MU is not exhaustive. p28 is Menu 503 and p29 is Menu 507, so 504 CONTRAST, 505 DISPLAY REVERSE and 506 have no MU parameter 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_settling retries once, a second ? is real. That is measured behaviour, not defensive coding.

The Phase 2 gate

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. Field widths are preserved rather than normalised — 0 and 000 are 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 SettingsReader today on the strength of MU and 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

  • Building an ME line 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.
  • Phase 3 — seed entry, tx_bands, and rx_bands modelling the receiver's real gaps. Until then no model names this driver_key, so nothing is reachable from the UI.
  • No write of any kind has been attempted on this radio.

Verification

npm run ci green: 446 tests, clippy -D warnings clean. 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

ww8l and others added 2 commits August 22, 2026 16:01
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant