Binteradio BT-9000 support (#43) - #126
Merged
Merged
Conversation
The BT-9000 is one badge on an OEM platform also sold as the Radtel
RT-950 Pro, Bajeton BJ-9000 and Tenway TP-900 Pro. The platform names
itself in the protocol -- the clone session opens with the ASCII string
`PROGRAMBT9000U` -- which is why searching the Binteradio badge finds
nothing and searching the Radtel one finds a complete MIT-licensed
reverse-engineering of the format.
The radio reports its model as `RT-950` whatever the case says, so the
handshake checks that token and never the badge.
Hardware ladder, on Tim's radio:
1. identity write PASSED 33,024 of 33,152 bytes byte-identical
2. one-name write PASSED and there is no checksum anywhere
3. full codeplug PASSED 30 channels across all 15 zones, via
this driver's own encoder and transport
4. band probe CANNOT BE RUN -- see below
Four defects in the inherited protocol, each measured here:
* A block ACK can take 15 seconds. The first identity write died at
0x8080 with a 3 s timeout and nothing wrong with the data: a flash
erase at a segment boundary.
* Radio 0x8080-0x80FF is a firmware journal, not VFO storage. Writing
it makes the radio append a snapshot of its own VFO state there and
discard ours. The write segment stops at 0x80 and a test asserts it.
* The APRS block cannot be written. Its payload must go unobfuscated
to draw any response, and the 0x06 it then answers is a lie -- the
block never changes, verified four times. APRS is read-only here, so
`aprs_capable` is false rather than offering a form that does nothing.
* Frequencies are little-endian packed BCD, not big-endian, and names
have two sentinels: 0x00 for never-named, 0xFF padding once set.
Two things the radio taught us that no source did:
* It validates NOTHING. It stored 127 in settings fields whose maxima
are 9, 2, 3 and 1, and it stored every band probe from 27.5 to 580
MHz. So the band probe cannot be run from the image at all, and
tx_bands stays at the manual's 136-174 / 400-520 until each channel
is confirmed on the radio itself. Under-claiming excludes a channel
visibly; over-claiming writes a dead memory and reports success.
* An ACK is not a commit. Every claim here was verified by reading the
image back.
Channel encodings measured by writing candidate values and reading the
radio's own screen: CTCSS, DCS at both ends of a 210-entry table, power
(0=High, the reverse of the TD-H3's mapping in this same crate) and
bandwidth. Zones are index arithmetic with no names in the radio, so
`zones_supported` is false.
Settings stay unwired and the schema empty on purpose. Sixteen function
fields are measured and screen-confirmed, but nothing carries them to the
radio yet, and a settings form with a dead write path is a trap this
project has already shipped once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrC78t5gKpZi3eJPhYN4tx
…43) Wires the BT-9000's non-channel settings through SettingsReader and SettingsWriter, generated from a graded measurement sheet. The sheet is the source. `scratchpad/binteradio_bt9000/MEASURED.md` names ~43 candidate fields in the function block and grades each one, and `gen_bt9000_settings.py` emits the Rust field table and the profile-form schema from one parse of it. Rows that are not settled are reported on stderr and left out. That bar is higher here than on other radios in this crate because this one validates nothing -- it stored 127 in four fields whose maxima are 9, 2, 3 and 1 -- so a wrong encoding is stored rather than refused, and the encoder is the only backstop there is. Three fields are emitted, not sixteen. The earlier claim that "sixteen function fields are measured and screen-confirmed" was wrong twice over: * Only eight ever reached the radio's screen. Two probe batches were written and read back; FINDINGS.md records screen readings for the first batch and none for the second. * An ordered list confirmed at ONE index is not settled. `3 -> DEEP` is equally consistent with the printed order and with any permutation putting DEEP last, which is how the TH-D75 shipped a control that wrote Volume Link when the operator picked Level 1. An enum now needs a second, non-endpoint index before it is emitted. `pass_a.py`, `probe_reverse.py` and `SCREEN-CHECK.md` are the campaign that closes the gap: one function-block write settles 27 fields in a single menu walk, five more are probed alone because live VOX, a short backlight, a keypad lock or a Chinese menu would sabotage the walk, and a reverse diff locates the fields with no known byte -- including Work Band, which is the one desk-reachable lead on tx_bands and 220 MHz. Two defects found while wiring it: * A settings write would have rewritten every channel. `upload` addresses all six write segments -- 33 KB, about four minutes -- to change a squelch level. Added `upload_segments`/`download_segments` and SETTINGS_SEGMENTS so a settings write touches the one 256-byte function segment: 0.35 s, with a test asserting the target. * `write_settings` had no caller anywhere in the UI. The capability was declared, the command registered and the api.ts binding present, but the only two controls reaching `writeRadioSettings` are the TD-H3's and the AnyTone's own program dialogs -- so a radio on the generic UI could read its settings into the form and had no way to send them back. That was ALSO true of the TH-D72, whose settings write is hardware-proven. WriteToRadioBar is gated on `caps.write_settings`, so both radios get it and any future one does automatically. The encoder refuses an out-of-range value rather than clamping, because the radio refuses nothing. A `select` label this app cannot name is skipped with a note instead, so a value another tool left on the radio survives a round trip. Verified in dev against the seeded model: both bars render, the three fields group by menu, and the write is blocked with an explanation while the profile has unsaved edits -- the command sends the stored profile. Nothing new was run against the radio; the hardware ladder is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
Found reviewing what the last commit made possible rather than what it
changed. `SETTINGS_SEGMENTS` was derived from `WRITE_SEGMENTS`, and a
`Segment` carries its own command byte -- so `verify()` handed it to
`download_segments`, which would have put `0x57` on the wire as a block
header with nothing behind it. On the platform whose reverse-engineering
notes document a radio with permanently degraded transmit after a
desynchronised write stream, that is not a cosmetic mix-up.
The read and write tables describe the SAME blocks with different
opcodes, which is exactly what made the mistake easy. So:
* `SETTINGS_READ_SEGMENTS`, drawn from `READ_SEGMENTS`, and the
read-back uses it.
* `check_commands` runs at the top of both transports, so each refuses
a segment from the other's table before any I/O.
* The shape test now asserts the opcode on both constants, not just
the address and length -- the address and length were identical
between the two, which is why they alone caught nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
The scratch Python has already put values in the function block and read
them back, so this is not asking whether the radio stores settings. It
asks whether THIS DRIVER's narrowed write does -- the same distinction
that made step 3 worth re-running for channels after the Python had
already proved the radio accepts an image.
Three assertions, all against a fresh read-back and never against an ACK,
because this radio acknowledges blocks it does not commit:
* `write_settings` reaches the radio and its own read-back verdict is
true.
* `read_settings` decodes back exactly what was asked for, including
the two "Level 1-9" fields two bytes apart that store their values
differently.
* ★ Every segment except `function` is byte-identical to the pre-write
image. That is what the narrowed write exists for: the whole-image
upload would rewrite 960 channel records to change a squelch level,
and this fails if a single one of them moves.
It restores the settings the radio started with before returning, so the
measurement campaign's baseline survives the run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
…reate (#43) Found by reviewing what the code will be able to do after the NEXT planned change rather than what it does now. `encode_channel` sets the per-channel TX-enable bit unconditionally (`m[15] = 0x02 | narrow`). That is inert today only because the BT-9000's `rx_bands` and `tx_bands` are the same two spans, so `channel_fit` can never return `ReceiveOnly` for this radio. Widening `rx_bands` is an expected outcome of the band work: this radio receives broadcast FM, AM and SSB, and its `F` handshake blob hints at a third span at 200-260 MHz. The moment the two lists diverge, every out-of-TX channel starts being programmed transmit-enabled -- on a radio that validates nothing and will key up wherever it is told. Not fixed, deliberately. The fix depends on byte 15 bit 1, which the inherited reverse-engineering calls "TX enable" and nobody has measured, and clearing an unverified bit is how radios have been damaged on this platform. The FT5D's opposite choice does not transfer either: it encodes receive-only channels as ordinary memories because "the radio polices its own TX bands", and this one polices nothing. So instead: a test beside the band lists that fires the moment they diverge and says what has to be settled first, and a measurement added to the campaign -- program a channel with the bit cleared, select it, press PTT. If the radio refuses, the bit means what the source says. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
Ran /code-review over main...HEAD before the merge rather than after,
which is the lesson from the CSV importer: a screen check does not sample
the input, and two channel-breaking parser bugs shipped that way.
The worst two were in the settings write control added earlier today.
* ★★★ "Write to radio" was ENABLED right after "Download from radio",
and would have written the previously saved values back over the ones
just read. `dirty` compared the form against `baseline`, which exists
to mean "not typed by the operator" and therefore absorbs a radio read
on purpose -- so the one path that most needed the guard was the one
path it did not cover. Now compared against a separate snapshot of
what the database actually holds, updated only by a successful save.
Verified on screen: after a download, the button is disabled and says
to Save first.
* A blank number field aborted the ENTIRE write. The form stores a
cleared input as "", `strip_out_of_range` only inspects numbers, and
`encode_field` rejected it as Invalid -- so clearing SQL stopped VOX
Level and Power On Display reaching the radio too. Treated as "leave
it alone", like a missing key.
Three more in the same control:
* A verified write discarded `res.note`, which is where the list of
DROPPED fields lives. Reporting "verified ✓" while silently binning
the record of what never made it is worse than not reporting.
* `verified: null` was treated as failure. It means the radio offers no
in-session read-back, not that one disagreed.
* The bar rendered for the AnyTone, whose settings commit reboots the
radio and re-enumerates USB -- so it reports `verified: null` plus an
`expected_path` to diff in a fresh session, and a generic bar showed
neither. Now limited to radios on the generic programming UI; a radio
with its own dialog already offers this where the specifics can be
explained.
And two in the driver:
* The failure hints promised the operator that the pre-write backup
"can be uploaded back over the same cable", and no control in the app
could do it -- `restore_image` was false. Implemented `ImageRestorer`
rather than weakening the sentence: this is the radio whose platform
has a documented unit with permanently degraded transmit, and hardware
ladder step 1 already proved the operation. The restore verifies by
read-back, because an ACK here is not a commit.
* `mode: None` encoded as NARROW while `export::channel_fit` resolves
the same NULL to "FM" when deciding the channel is programmable, so a
channel admitted as wide FM was programmed narrow. `mode` is nullable
and reachable from a CSV import with no mode column. Narrow now
requires an explicit narrow mode. AM remains a documented gap: byte
15 bit 0 is claimed to select it and has never been measured here.
Last one is shared UI the BT-9000 newly reaches: the generic Program
dialog's safety banner promised "your profile's radio settings" on all
six radios it serves, and only the UV-5R's `program_codeplug` carries
them. Added an explicit `carries_profile_settings` on ImageProgrammer --
declared, not inferred, since populating `req.settings` says nothing
about whether a driver uses it -- and the banner now names the separate
Write to radio control instead when the radio has one.
Also adds the step-4 gate the process asks for and this radio never had:
a test that runs the app's OWN pipeline against a real database. Four
channels in, three out -- the 220 MHz repeater is excluded rather than
written, which on a radio that stores whatever it is handed is the only
thing between it and a memory that keys up out of band.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
…43) Ran Pass A through C with Tim at the radio. Every field in the schema now has its encoding read off the radio's own screen; the generator still withholds everything else. Four things the radio contradicted, each of which would have shipped a wrong value on a radio that validates nothing: * ROGRE is not a switch. The manual prints `OFF/ON` and the source calls it a 0/1 flag; the radio offers OFF, Beep, Tone 1200. * The MDF display fields are `NAME, FREQUENCY, CHANNEL NUM.` -- the exact REVERSE of the source's `Channel/Freq/Name`. All three display lines would have been wrong. * RP-STE and RPT-RL are 11-entry millisecond lists, not booleans. * Back Light and Keypad Lock both start at 5 sec. The manual prints "0.5sec" for both, and a schema built from it would have offered an option the radio does not have, twice. And one refuted outright: `0x0B` is NOT PTT-ID. The image verifiably held 2, the radio's own list is `OFF, BOT, EOT, BOTH` so index 2 is EOT, and the menu read OFF. A reverse probe then found S-CODE moving CHANNEL record byte 12 -- so both of those Signaling items edit the current channel, not the radio, and neither belongs in a radio profile. The menu census was wrong in three separate ways, not the one the sheet knew about: Radio is +1 (Work Band), Signaling has SIX items (three DTMF timing entries the manual omits, pushing DTMFST from 3 to 6), and VFO&CH changes its CONTENTS with the radio's mode. Never derive a menu number for this radio from the manual. Work Band is in NO segment of the clone image -- channels, VFO, function, DTMF, modulation and APRS were all byte-identical across a change. It cannot be a profile field. Its options are frequency RANGES (18-64 MHz, 64-999), which is the receiver describing coverage far beyond the seeded 136-174/400-520, but that bears on rx_bands only and the tripwire added earlier still gates widening it. Two fixes to code shipped this morning, both found on hardware: * ★ `verify()` compared all 256 bytes of the function segment. Only 0x00-0x45 is live settings: 0x46-0x7F is 0xFF filler and 0x80-0xFF is a firmware-maintained SHADOW of the live block (+0xD0 onward is byte-for-byte identical to it) which moves on its own. A restore that landed perfectly was reported as a 13-byte mismatch. Narrowed to FUNCTION_LIVE_LEN. * A settings write acknowledged every block and did not commit; an identical second write landed. Since the read-back is what caught it and the write is idempotent and takes 0.35 s, it now retries ONCE and says so in the report. Exactly once -- hammering a write path is how radios have been damaged on this platform. ⚠ Tested and REJECTED: narrowing the settings write to the live half to leave the firmware's shadow alone, by analogy with the VFO journal. The half-segment write commits but is NOT acknowledged -- the inverse of the APRS block -- and this driver treats a missing ACK as a hard abort, so it would stop mid-write on data that had already landed. The full-segment write stays. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
One write, ten readings, all correct. Every byte the source claimed for a
key slot is confirmed, and the 22-entry function list is pinned at BOTH
ends -- indices 0, 1, 2, 3, 4 and 6 at the bottom, 16, 17, 18 and 19 near
the top, every one in the manual's printed order. No reversal or shift
survives that.
`language` stays out, and that is the point of the grading rather than an
oversight. It reads English at its factory 0, and the manual says the
list is English/Chinese -- but the manual also said ROGRE was OFF/ON and
the radio has three options there. A wrong index writes the radio into
Chinese, which is a genuinely hostile failure for one low-value field, so
it waits for someone to scroll the list. One reading settles it.
That leaves 41 of 45 rows emitted. The other three are withheld for
measured reasons, not missing work: Work Band is in no segment of the
clone image, and S-CODE and PTT-ID both edit the current CHANNEL rather
than the radio.
Two fixes to `pass_a.py` first, both from earlier mistakes this session:
* It takes `--port` now, with no default and no auto-detection. Tim had
two radios on this Mac at once -- a POTA rig on a second CH340 -- and
a probe that guesses its port is a probe that can write to the wrong
radio. (`write.py` already refuses anything not answering `RT-950`,
and `find_port` already refuses to guess between two candidates; this
closes the third hole.)
* It builds its image from a FRESH read of the radio instead of from
`01_original.bin`. Rebuilding from the factory capture is how Tim's
Menu OutTime got reset twice in one session, once as pure collateral
from a probe that had no business touching it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
…lds (#43) Tim confirmed `Setting → 7. Language` offers exactly two options: English and one rendered in Chinese glyphs. With a two-entry list and the factory 0 displaying English, index 1 has nowhere else to go -- the same reasoning that settled Power On Display, and the reason a two-entry list is the documented exception to needing a second confirmed index. Held back deliberately last commit rather than assumed, because the manual also called ROGRE `OFF/ON` when the radio has three options there, and a wrong index here writes the radio into Chinese. ⚠ The label "Chinese" is ours. The radio renders that option in Chinese characters so there is no ASCII string to copy, and the schema names it the way the manual does. Only the INDEX reaches the radio, so the label is a display choice rather than a claim about what is stored. That completes the settings half of this radio. 42 of 45 rows emitted; the remaining three are not radio-profile settings at all and are withheld on measured grounds, not missing work: * Work Band -- in NO segment of the clone image. Changing it moved nothing in channels, VFO, function, DTMF, modulation or APRS. * S-CODE -- a reverse probe caught it moving CHANNEL record byte 12. * PTT-ID -- 0x0B refuted outright, and per-channel like S-CODE. Every emitted field's encoding was read off this radio's own screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
Tim asked whether we were 100% certain. We were not, so the untried
avenues got enumerated and run instead of defended. All of them use
documented command bytes only -- no guessing, on the platform with a
documented permanently-degraded unit.
Two real hypotheses died, and both deserved the test:
* "The 0x06 was a timeout artifact." s127's probe waited 2 seconds and
this radio can take 15 to acknowledge a block that erases flash, so
"no response" to the obfuscated write might have been impatience. It
was not. At a 16 second wait the plain write still answers 0x06 in
0.0 s and the obfuscated one answers 0x54 -- the APRS READ opcode,
not an acknowledgement.
* "The commit is deferred to power-off." This firmware demonstrably
defers work: it keeps a VFO journal and a settings shadow, and a
half-segment write to the function block commits WITHOUT
acknowledging. A write landing only across a restart was plausible.
Written, not read back, power-cycled, read: unchanged.
And one mechanism ruled out rather than assumed: the write is not
partial. Reading 0x54 across 0x0000-0x0200 shows only 0x0000 holds data,
so the single 0x80 block the CPS sends covers the whole space.
That is eight attempts across two sessions and three distinct mechanisms.
`aprs_capable: false` now rests on measurement rather than on a count of
tries.
⚠ Left as a clue for whoever decompiles `RWDataOperation`: an obfuscated
write draws 0x54 rather than silence or an ACK. A radio answering a write
with a READ opcode suggests our framing is being parsed as something
other than a write.
Adds `aprs_try.py` (write attempts, read-only by default, refuses any
radio not answering RT-950) and `aprs_space.py` (read-only space map).
Nothing to restore: the block never changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
The image could not answer this -- the radio stores every frequency it is
given, so all 13 band probes "survive" a round trip regardless. The only
instrument is the radio, so each probe was selected and keyed into a
dummy load.
27.500, 50.125 key (27.5 needs the 18-64 Work Band selected)
108.000 NO -- receives AM, refuses to transmit
136.000, 145.100, 174.000 key
200.000, 223.500, 260.000 key
400.000, 431.100, 520.000 key
580.000 not settable on the VFO at all
Two things follow. The radio DOES gate transmit -- 108 refusing is what
makes the other twelve "yes" answers mean anything, and without that
negative control the whole test would have been worthless. And 220 MHz is
real: the F blob's third pair `0200 0260` is a transmit band the manual
never mentions.
tx_bands 18-64, 136-174, 200-260, 400-520
rx_bands 18-999
⚠ The spans between tested points are excluded because nothing was keyed
there, not because anything refused: 64-108, 108-136, 174-200, 260-400
and 520-580 are unmeasured. The three narrow spans take both their own
edges from a successful key; 18-64 is the range the radio's own Work Band
menu declares, with two confirmations inside it.
That makes rx wider than tx for the first time, which is what the
tripwire added earlier this session existed to gate. It was waiting on
byte 15 bit 1 -- claimed to be per-channel TX-enable and never verified,
and clearing an unverified bit on this platform is how radios have been
damaged. Measured now: a channel written with the bit clear refuses the
PTT while its neighbour with it set keys normally. So:
* `encode_channel` takes `tx_enable` and clears the bit for a
receive-only memory, rather than setting it unconditionally;
* `patch_image` takes the model and asks `channel_fit`;
* the tripwire is replaced by the assertion it was guarding -- that the
bands DO differ and receive-only is therefore live code.
⚠ `hw_ladder`'s band probe now builds a deliberately permissive model, so
its probes still go out transmit-enabled. With the seeded model they
would be written PTT-disabled, which is right for a codeplug and would
defeat a probe whose whole question is whether the radio keys there.
The pipeline test now covers all three verdicts with frequencies keyed on
the real radio: 223.500 included, 108.000 receive-only and written with
the bit clear, 1200.000 dropped.
⚠ Not confirmed: RF actually leaving the PA. These readings are "the
radio keys", taken without a wattmeter in line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
…that way (#43) Two shipped radios were missing from the Supported table. The TH-D72 went out in v26.8.29 and never reached it. The BT-9000 was still sitting under "Planned" on the day it shipped, under "BTECH / Btrianium" -- which is not its manufacturer. A radio someone owns and cannot tell is supported may as well not be supported. * TH-D72 added: 1000 memories, 113 menu settings over the radio's `MU` command, 2 m / 70 cm TX with 118-174 / 320-524 MHz RX. * BT-9000 added under its real name, with the badges it also sells under (Radtel RT-950 Pro, Bajeton BJ-9000, Tenway TP-900 Pro) and the fact that it reports itself as RT-950 -- which is what an owner searching for their radio will actually find. * TM-D710 added to Planned against #113, where it is in progress. And a guard, because Tim's note was that this has been missed more than once: `every_seeded_radio_appears_in_the_readme` asserts that every driver in the registry is named in the Supported table and NOT in the Planned one. Keyed on `display_name`, so the table and the app cannot drift into describing a radio differently. ⚠ It caught a bug in itself first: matching against everything after the Planned heading reported the AT-D890UV as still planned, because the credits section names it too. Bounded to the Planned table. The receive-only paragraph now says how it is actually done -- the app sets the radio's per-channel transmit inhibit where one exists -- and notes that band limits are measured rather than copied from the manual, with the BT-9000's undocumented 220 MHz as the example. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
Binteradio BT-9000 support, and the README finally listing every radio that works. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
The guard added minutes ago passed on macOS and Ubuntu and failed on Windows, reporting the AT-D890UV as "ships AND is still listed under Planned" when it is in neither state. Cause: it bounded the Planned table by splitting the file into paragraphs on "\n\n". A Windows checkout has CRLF, so every paragraph boundary is "\r\n\r\n", the split found nothing, and the fallback matched a later block that names a shipped radio in passing. Rewritten line-based, trimming '\r' per line and ending each table at the first non-table line after it starts. The extraction is now a function taking &str, so it is testable -- and the new test runs the same document through it twice, once with LF and once with CRLF, asserting the two agree. That reproduces the Windows condition on any machine, and it would have failed against the old parser. ⚠ Worth noting what this cost and what it saved: `npm run ci` is macOS only, and the pre-push hook was green for a defect that only Windows could see. The three-OS run on the PR is what caught it, one commit before a merge and a release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM
Tim caught that the review had not been run since 4c31e94, one commit before the merge. Nine of eleven findings are fixed here; the other two are answered below. ★ The two HIGH ones are the same bug, fixed once and left in two other places. This branch established that function-block bytes 0x80-0xFF are a firmware-maintained shadow that moves on its own, and narrowed the settings read-back to the live area -- but `restore_image` and `verify_after_write` both kept comparing the full segment. So a restore that landed perfectly would tell the operator "the radio does NOT hold this backup", on the one path somebody reaches for when a write has already gone wrong, and a good codeplug program would steer them toward an unnecessary restore. All three now go through one `comparable()` helper, so a fourth caller cannot forget. ★ `rx_bands` claimed 18-999 MHz on the strength of a Work Band menu label, while the highest confirmed point is 520 and the next probe up could not be dialled on the VFO at all. That made a 902 MHz repeater -- RepeaterBook carries them -- `ReceiveOnly` rather than `Excluded`: a memory slot consumed and reported as written on a radio that cannot tune it. That is the silently-empty-slot failure already recorded here for the ID-52, introduced by over-claiming. Capped at 520. Also: * `write_settings` verified TWICE on the success path and discarded the first result, so a flaky second handshake could report a proven-committed write as unverified. Verify once, keep the answer, re-verify only after an actual retry. * The retry's re-handshake was the only fallible step in that function that dropped the backup path from its error, and it fires after a write has gone out. * `verify` indexes FIELDS addresses into a slice truncated to 0x46 with no guard. The highest is 0x44 and the sheet still owes rows; a field at 0x46 would never be verified AND would panic inside the mismatch-reporting loop, which runs only when a write failed. Test added. * The README guard matched display names as substrings: "Icom ID-51" is a substring of the planned "Icom ID-5100", so it would have failed the day the ID-51 ships. Matched as the bolded cell now, with a prefix-collision case in the parser test. * ⚠ The seed guard replacing the band tripwire did not check what its message claimed -- a `t != r` disjunct made it true for bands that merely differ, so an rx band strictly INSIDE tx would have passed while receive-only was dead code. Rewritten to test real coverage and checked against that counter-example. * `covers_hf` and `covers_220` still said false while the seed claims TX on 18-64 and 200-260. Display only, but it is what the band chips show and what an operator picks a radio by. Not changed, with reasons: * Truncating the `function` WRITE segment to 0x80 to spare the shadow, by analogy with the VFO journal. Tested on the radio this session and REJECTED: a half-segment write commits WITHOUT being acknowledged, and this driver aborts on a missing ACK, so it would stop mid-write on data that had already landed. * The 18-64 MHz transmit span takes neither edge from a measurement -- 27.500 and 50.125 keyed inside it and the edges come from the radio's own Work Band label. Flagged to Tim as the one deliberate judgement call rather than changed unilaterally. 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.
Adds the Binteradio BT-9000 as the app's eighth radio, and fixes the README to list every radio that actually works.
The radio
One badge on an OEM platform also sold as the Radtel RT-950 Pro, Bajeton BJ-9000 and Tenway TP-900 Pro. It names itself in the protocol — the clone session opens with
PROGRAMBT9000U— and reports its model asRT-950whatever the case says. Searching the Binteradio badge finds nothing; searching the Radtel one finds a complete MIT-licensed reverse-engineering of the format.960 channels in 15 fixed zones, 42 menu settings, direct USB.
Verified on the radio
What the radio contradicted
Measuring rather than trusting the sources changed five things that would otherwise have shipped wrong:
OFF, Beep, Tone 1200— the manual and the source both call it a boolean.NAME, FREQUENCY, CHANNEL NUM.— the exact reverse of the source's claim, so all three display lines would have been wrong.0.5secfor both.0x0Bis not PTT-ID. Refuted outright — and a reverse probe then caught S-CODE moving a channel record byte, so both of those menu items edit the current channel and neither belongs in a radio profile.220 MHz transmit is real, appears in no published source for this radio, and was found by programming a channel there and keying up.
tx_bandsis18-64, 136-174, 200-260, 400-520; the receiver goes to 999.Two things this radio does that others here do not
127in fields whose maxima are 9, 2, 3 and 1, and every band probe from 27.5 to 580 MHz. The encoder is the only backstop, so it refuses out-of-range rather than clamping.aprs_capableis false on an APRS radio, on purpose. Settings writes are verified by read-back and retried once.Beyond this radio
write_settingshad no caller anywhere in the UI — the capability was declared and only two bespoke dialogs reached it, so any radio on the generic screen could read its settings and had no way to send them back. That included the TH-D72, whose settings write is hardware-proven. Fixed with a capability-gated control.Full measurement record in
scratchpad/binteradio_bt9000/(gitignored):MEASURED.mdis the source the schema generates from,PASS-A-RESULTS.mdthe transcript.🤖 Generated with Claude Code
https://claude.ai/code/session_01EUipy7p4gqmziJmxKnjmJM