Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9e37c85
Kenwood TM-D710: Phase 1 capture and the memory line (#113)
ww8l Aug 22, 2026
a1772b9
TM-D710: the write path, verified by read-back (#113)
ww8l Aug 22, 2026
d13da4d
TM-D710: the tone and DCS fields are indices, measured (#113)
ww8l Sep 1, 2026
49724be
TM-D710: build an ME line from a library channel (#113, Phase 2)
ww8l Sep 1, 2026
6118a75
TM-D710: seed the model, with rx_bands measured off the radio (#113, …
ww8l Sep 1, 2026
05a33e6
Don't offer "Program radio" for a driver that cannot write (#113)
ww8l Sep 2, 2026
2f6c8c6
TM-D710: 35 menu settings, every range measured on the radio (#113, P…
ww8l Sep 2, 2026
5772b71
TM-D710: program a codeplug over the cable (#113, Phase 5)
ww8l Sep 2, 2026
4f938ce
TM-D710: find the settings image behind `0M PROGRAM` (#113)
ww8l Sep 2, 2026
85a9e29
new-radio: count the radio's menus before believing a source
ww8l Sep 2, 2026
528f6de
TM-D710: rebase onto the BT-9000 release, and list the radio in the R…
ww8l Sep 3, 2026
bfcaea9
TM-D710: 0x7F00 is a hole, not the end of the image (#113)
ww8l Sep 3, 2026
921663e
TM-D710: the image write path, proven on the narrowest field there is…
ww8l Sep 3, 2026
461f64f
TM-D710: move the program-mode transport out of the harness into the …
ww8l Sep 3, 2026
4fb1d4c
CLAUDE.md: one PR per radio, opened when the model is essentially done
ww8l Sep 3, 2026
8b6416a
TM-D710: twelve APRS settings fields, measured on the radio (#113)
ww8l Sep 4, 2026
6d5aa8a
TM-D710: the APRS menu census, from the RIGHT model's manual (#113)
ww8l Sep 4, 2026
d934014
TM-D710: close the image map at the desk, and correct two claims (#113)
ww8l Sep 4, 2026
399488e
TM-D710: menu 625 is not in the image, measured at the desk (#113)
ww8l Sep 5, 2026
501d300
TM-D710: run the default-vector search forwards — ten menus located (…
ww8l Sep 5, 2026
d9a8ccc
TM-D710: a probe for the addresses the read plan never asks for (#113)
ww8l Sep 5, 2026
0c13cae
TM-D710: the read plan was not the ceiling, and eight fields measured…
ww8l Sep 6, 2026
2210da7
TM-D710: the packet-filter mask, and thirteen fields measured (#113)
ww8l Sep 6, 2026
610bced
TM-D710: restore from a diff, so the operator is not the backup syste…
ww8l Sep 6, 2026
c3b4bf2
TM-D710: RETRACT "menu 625 is not in the image" — it is, at +0x35C (#…
ww8l Sep 6, 2026
f223bca
TM-D710: the menu 612 result is VOID, not negative (#113)
ww8l Sep 6, 2026
9921ef9
TM-D710: menu 612 at +0x421 and menu 624 at +0x350 (#113)
ww8l Sep 6, 2026
027ba3b
TM-D710: hardware ladder step 3, through the app's own pipeline (#113)
ww8l Sep 6, 2026
aa800c8
TM-D710: hardware ladder step 3 PASSED on the real radio (#113)
ww8l Sep 6, 2026
622c845
TM-D710: the 600-series settings, wired into the form (#113)
ww8l Sep 6, 2026
5db2de8
TM-D710: ladder step 5 passed, nine more 6xx fields measured (#113)
ww8l Sep 6, 2026
34da57b
TM-D710: hardware ladder step 4 PASSED — the ladder is complete (#113)
ww8l Sep 6, 2026
c1228ed
TM-D710: two text settings, the menu-number audit, and grouped layout…
ww8l Sep 6, 2026
52a5bd8
TM-D710: write down where this stops and what is still owed (#113)
ww8l Sep 6, 2026
90b6b9a
TM-D710: position, status text and icon measured and shipped (#113)
ww8l Sep 7, 2026
3adc182
TM-D710: record Tim's "good enough" call as a decision, not a check (…
ww8l Sep 7, 2026
ae7e718
TM-D710: the model guard covered one exit out of four (#113)
ww8l Sep 7, 2026
b3a1444
Standing rule: /code-review before committing, every time (#113)
ww8l Sep 7, 2026
42e3d9a
Correct the standing rule: build and stop, don't commit or review (#113)
ww8l Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 61 additions & 4 deletions .claude/skills/new-radio/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,38 @@ not the opening move, and this has paid off on every radio where it was tried.
| GitHub RE repos for the model or its siblings | Config-file structure, command tables, enum tables |
| **The radio's own manual** | Menu order, option lists, defaults — and it is a *published source*, not a fallback |

### ★ First enumerate what the RADIO has, not what a source describes

Do this before reading any source in depth, and write it down. A published table
covers what its author needed; the gap between that and the radio is invisible
unless you have the radio's own list to hold it against.

1. **What is this radio FOR?** Read the model's feature list — the manual's
first pages, the manufacturer's product page. A built-in TNC, a GPS, D-STAR,
a second receiver, cross-band repeat: each is a whole family of settings.
Manufacturer naming often carries it (Kenwood's `D` in TM-**D**710 and
TH-**D**75 means the data/APRS half; the TM-V71 is the same radio without it).
2. **Enumerate the complete menu map** from the manual — every group, and how
many menus are in each. This is the denominator for everything after it.
3. **For each group, name the transport that reaches it.** A group with no
transport is a **finding**, not an omission, and it belongs in `PLAN.md`
before a line of code.

⚠ **One command's coverage is not the radio's settings.** The TM-D710 (#113)
shipped a 35-field settings schema built on its `MU` command, every field
measured on the radio and correct — and **no APRS at all**, on a radio whose
headline feature is APRS. `MU` carries menus 000-5xx; the APRS and TNC settings
are the 600-series and there is no `MU` parameter for one of them. The radio's
own `aprs_capable` flag was set to `true` in the same session. Nobody counted
the menus, so nobody noticed the settings stopped at 500.

The earlier note that "`MU` is not exhaustive — menus 504, 505 and 506 have no
parameter" was already in `FINDINGS.md`. It was read as a three-menu gap instead
of the question it actually was: *what else is missing, and how would we know?*

**Ask that question out loud in `PLAN.md`, with a number.** "The manual lists N
menus in G groups; this transport reaches M of them; the other N-M are <where>."

Then classify, because it decides how much of this process applies:

- **Clone of a family already supported** — AT-D868UV/D578 against the D890UV,
Expand All @@ -49,9 +81,14 @@ Then classify, because it decides how much of this process applies:
record-by-record programming (AnyTone).

**Gate:** a `PLAN.md` in `scratchpad/<driver_key>/` naming the sources found,
the programming medium, the family, and what the user owns. Template in
`templates/PLAN.md`. Nothing is written before this exists — it is also the
thing that makes a resumed session cheap.
the programming medium, the family, and what the user owns — **plus the menu
census above: how many menus the radio has, how many the chosen transport
reaches, and where the rest live.** Nothing is written before this exists; it is
also the thing that makes a resumed session cheap.

⚠ If the census cannot be completed because a group's transport is unknown, that
is the finding to report, not a detail to settle later. A radio shipped with a
whole feature's settings missing looks finished from the inside.

## 2. Anchor on a file the radio wrote

Expand Down Expand Up @@ -178,7 +215,19 @@ Then wire *both* ends, and check each off explicitly:
- [ ] **`apply_settings` called by the export path**
- [ ] the table↔schema agreement test

**Gate:** a test proving an export carries memories **and** settings together.
- [ ] **the coverage check against step 1's menu census** — the schema's field
count and groups reconciled against the menus the radio actually has, with
every absence named

**Gate:** a test proving an export carries memories **and** settings together,
and a **stated count**: N of the radio's M menus are exposed, and the M-N are
listed with a reason. "35 fields" is not a result; "35 of the 42 this transport
reaches, and the transport reaches 42 of the radio's ~90" is.

⚠ A cheap mechanical version of that reconciliation: the seed row already
asserts what the radio can do. A model with `aprs_capable: true` and no APRS
field in its settings schema is a contradiction the test suite can catch on its
own, and the TM-D710 shipped exactly that pairing for a whole session.

⚠ The fourth box is the one that nearly shipped broken. The read path worked and
the form filled correctly, so nothing looked wrong — the values simply never
Expand Down Expand Up @@ -235,6 +284,14 @@ if the folder is empty, the process above still stands on its own.

## Traps, each of which has already cost time

- ★ **A source's coverage is not the radio's.** Every field measured off one
command can be right and the set still be badly incomplete — the TM-D710
shipped a correct 35-field settings schema with no APRS on an APRS radio,
because `MU` stops at menu 500 and nobody counted the menus. Enumerate what
the radio HAS first, then hold every source against it.
- ★ **A noted gap is a question, not a footnote.** "`MU` is not exhaustive —
three menus have no parameter" sat in the findings for two sessions. It was
the same fact as "an entire feature is unreachable", written small.
- A working **read** path hides a dead **write** path. Verify the write.
- A printed option list is **display** order, not the stored index. One radio
prints High/Medium/Low and stores Low as 0.
Expand Down
27 changes: 27 additions & 0 deletions .claude/skills/new-radio/templates/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,33 @@ re-measured.
**What the user has:** radio / cable / microSD card / programming software
(RT Systems, OEM CPS, none) / availability for hardware steps.

## ★ What this radio IS — the census

Fill this in **before** reading any source in depth. A published table covers
what its author needed; the gap is invisible without the radio's own list.

**Headline features** (from the manual's first pages, not from a driver): TNC /
APRS · GPS · D-STAR · DMR · second receiver · cross-band repeat · weather alert
· … Each one is a whole family of settings, and the model name often says so
(Kenwood's `D` in TM-**D**710 is the data/APRS half; the TM-V71 is the same
radio without it).

**Menu census** — the denominator for everything downstream:

| menu group | what it covers | how many | transport that reaches it |
|---|---|---|---|
| 0xx | | | |
| 1xx | | | |
| … | | | |
| **total** | | **N** | **M reached, N-M elsewhere** |

⚠ A group with **no** transport is a finding, not an omission — write it here
and say so out loud, with the number. "This command reaches M of N menus; the
other N-M are in \<where\>." The TM-D710 shipped a correct 35-field settings
schema with **no APRS at all** on an APRS radio, because `MU` stops at menu 500
and nobody ever counted. Every field in it was measured and right; the set was
the problem.

## Shape of the work

`driver_key = "<manufacturer>_<model>"`, `export_format = "<key>"`,
Expand Down
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ substantial work under `src-tauri/src/radios/`.

## Standing workflow

- **⚠⚠ Build. Do not commit, do not push, do not run `/code-review`. Stop and
say the work is ready.** Tim, 2026-09-06: *"Just build the thing and when I'm
ready to push I'll ask for code review."* Committing and pushing are **his
calls, on his timing** — `/code-review` is a command he invokes when he is ready
to push, not a step for this agent to run on its own.

⚠ The failure this replaces is real: in s134 this agent finished each piece of
work, ran `npm run ci`, committed and pushed — five times, unasked — and Tim's
objection ("you just haul off and start doing CI") was about being moved past,
not about which checks ran. A green `npm run ci` is not permission to commit and
is not a review; it runs the tests that already exist, so it cannot find the bug
nobody thought to test for.

So: finish the work, leave it in the working tree, and report what changed and
what is unverified. Commit only when asked, and push only when asked.
- **Verify in dev, then commit, then push.** `npm run tauri:dev` runs against a
separate `.dev` app identifier, so dev never shares the production database.
- **CI is free and unmetered.** The repo went public on 2026-08-22, so standard
Expand All @@ -31,6 +46,11 @@ substantial work under `src-tauri/src/radios/`.
three OSes before it lands, which is the point: a branch that has never been
verified anywhere but the author's Mac should not reach `main`. This reverses
the old rule, which existed only because a PR cost metered minutes.
- **⚠ A radio model is finished work only when the whole model is done.** Keep
pushing the branch — that is what runs CI — but do not open a PR per phase or
per hardware step, and do not treat an open one as something to keep
merge-ready commit by commit. One PR, opened when the radio is essentially
complete: channels and settings both working, the hardware ladder climbed.
- **`main` is still verified on its own.** CI runs on push to `main` as well, so
a merge of two green branches gets checked as the combination — this project
has shipped bugs that existed nowhere else. Landing by local merge is still
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ card — or exported as CSV for tools that expect it.
| **Icom ID-52** | D-STAR + Analog | VHF / UHF TX, 108–174 / 225–479 MHz RX | microSD — patches the radio's own `.icf` file | 1000 memories in 100 groups; memories and menu settings restore in one operation |
| **Kenwood TH-D72** | APRS + Analog | 2 m / 70 cm TX, 118–174 / 320–524 MHz RX | Direct USB — read, write, settings | 1000 memories; 113 menu settings over the radio's own `MU` command |
| **Kenwood TH-D75** | D-STAR + APRS + Analog | VHF / 1.25 m / UHF TX, 0.1–524 MHz RX | microSD — patches the radio's own `.d75` file | 1000 memories in 30 groups; memories and menu settings, including the APRS setup |
| **Kenwood TM-D710** | APRS + Analog | 2 m / 70 cm TX, 118–524 MHz RX | Serial cable — read, write, settings | 1000 memories; 95 settings across **two transports** — 35 over the radio's `MU` command and 60 more, including the APRS position, status texts and station icon, out of the settings image `MU` cannot reach. Programmed live, one memory at a time over the COM port on the rear of the **operation panel** (not the main unit), so there is no image file and a backup is a transcript of the radio's own lines |
| **Binteradio BT-9000** | Analog FM/NFM | 18–64 / 136–174 / 200–260 / 400–520 MHz TX, 18–520 MHz RX | Direct USB — read, write, settings | 960 channels in 15 fixed zones; 42 menu settings. Also sold as the Radtel RT-950 Pro, Bajeton BJ-9000 and Tenway TP-900 Pro — the radio reports itself as `RT-950` |

Direct USB programming reads the radio's current image, applies your changes, backs up the
Expand Down Expand Up @@ -59,7 +60,6 @@ settings together, then verify on the actual radio before shipping.
| **AnyTone AT-D578UV** | DMR + Analog mobile | [#47](https://github.com/ww8l/codeplug-magic/issues/47) |
| **AnyTone AT-D868UV** | DMR + Analog handheld | [#51](https://github.com/ww8l/codeplug-magic/issues/51) |
| **Icom ID-51** | D-STAR + Analog handheld | [#50](https://github.com/ww8l/codeplug-magic/issues/50) |
| **Kenwood TM-D710** | APRS + Analog mobile | [#113](https://github.com/ww8l/codeplug-magic/issues/113) |
| **Icom ID-5100** | D-STAR + Analog mobile | [#49](https://github.com/ww8l/codeplug-magic/issues/49) |
| **Icom IC-9100** | HF / VHF / UHF base | [#45](https://github.com/ww8l/codeplug-magic/issues/45) |
| **Icom IC-7610** | HF / 6 m SDR base | [#46](https://github.com/ww8l/codeplug-magic/issues/46) |
Expand Down
21 changes: 12 additions & 9 deletions src-tauri/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,18 @@ mod tests {
// Models are reintroduced one at a time (migration 0005 trimmed the
// original set): currently the Baofeng UV-5R, TIDRADIO TD-H3, AnyTone
// AT-D890UV, Yaesu FT5D, Icom ID-52, Kenwood TH-D75, Kenwood TH-D72
// and the Binteradio BT-9000. (0015 removed
// the Vero VR-N76 placeholder.) None of the last three has a migration
// of its own — seeding INSERTs new (manufacturer, model) rows, so a new
// model reaches existing databases on the next startup without one.
// the Binteradio BT-9000 and Kenwood TM-D710. (0015 removed the Vero
// VR-N76 placeholder.) None of the last four has a migration of its
// own — seeding INSERTs new (manufacturer, model) rows, so a new model
// reaches existing databases on the next startup without one.
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM radio_models")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
count.0, 8,
"expected the UV-5R, TD-H3, AT-D890UV, FT5D, ID-52, TH-D75, TH-D72 and BT-9000 \
seeded models"
count.0, 9,
"expected the UV-5R, TD-H3, AT-D890UV, FT5D, ID-52, TH-D75, TH-D72, BT-9000 \
and TM-D710 seeded models"
);

let models: Vec<(String,)> =
Expand All @@ -82,7 +82,10 @@ mod tests {
let names: Vec<&str> = models.iter().map(|m| m.0.as_str()).collect();
assert_eq!(
names,
vec!["AT-D890UV", "BT-9000", "FT5D", "ID-52", "TD-H3", "TH-D72", "TH-D75", "UV-5R"]
vec![
"AT-D890UV", "BT-9000", "FT5D", "ID-52", "TD-H3", "TH-D72", "TH-D75", "TM-D710",
"UV-5R"
]
);

// Seeding twice must remain idempotent.
Expand All @@ -91,7 +94,7 @@ mod tests {
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count2.0, 8, "seeding should be idempotent");
assert_eq!(count2.0, 9, "seeding should be idempotent");

// A new database starts with NO talkgroups. The BrandMeister list used
// to be compiled in and seeded here; it is downloaded on request now,
Expand Down
Loading