Imported from swiftraccoon/kenwood (
tmd750/AGENTS.md). Install upstream withnpx skills add swiftraccoon/kenwood --skill tmd750. Copyright stays with the author.
tmd750 (kenwood-tmd750)
Rules
- No dependency on
kenwood-thd75, direct or transitive.tests/seam.rsrejects another model's imports in every source file and in the manifest. - Dependencies stay at the minimum that works, ideally none beyond the shared
workspace crates; the rule is in the root
AGENTS.mdsectionStandard. - Generic transport types are imported directly from
kenwood-transport, never re-exported from this crate'stransportorerrornamespace.src/transport.rsowns model USB discovery and the 9600 baud, RTS/CTS, asserted DTR/RTS preset; the shared backend owns only physical I/O. src/transport.rsmust not implement automaticTransport::reopen.SerialTransport::reopenreturnsTransportError::ReopenUnsupported. Re-selecting a pinned endpoint after a reboot istransport::reenumeration(pure classification, opens nothing) plusradio::readiness, which verifies a fresh identity through the caller'sControlHost; the host owns open, close, drop and enumeration.src/memory/menu_fields.rsis generated bymcp-d75-extract; never hand-edit it. Its doc text lives in theTMD750_TARGETarrays ofmcp-d75-extract/src/rustgen.rs, andmcp-d75-extract/tests/tmd750.rspins one sentence of it.data/mcp_d750_menu_schema.jsonreportswritable_registry_field_count431 andtotal_public_field_count436;MCP_D750_MENU_FIELDSin the generated registry must agree, andtests/registry_agreement.rsenforces it.- Region constants and framing bytes are described in committed code as the official program's transfer behavior, never with decompiled identifiers.
- Committed prose states contracts only: no dated bench narrative, no approval vocabulary, no capture paths, no timings.
CAT contracts (firmware 1.02, type K,2,1, panel USB)
- One command, one reply:
Radio::commandreads until a line answers the command (?,N, or the typed reply with matching band, address or slot), skipping at mostMAX_STALE_LINESother lines, and after a?the nextREJECTION_SETTLE_COMMANDScommands discard what follows their reply withinREJECTION_SETTLE_WINDOW. Needed because the radio repeats the reply to the command that follows certain rejected bare commands (AI,BL,FS,FTdid;AG,IO, unknown mnemonics did not). No unconditional pre-write drain: every transport read is a capture-transcript record in the REPL. - Every setter runs the firmware/type gate, requires an exact echo, then reads
back. Wire domains were established by writing every value and observing
the rejection of the next; a value label the radio did not confirm is marked
in the type's doc, not asserted:
PCHigh/Medium/Low order,DLdual/single,LCvalues,MD 3as NFM, tone and DCS table index basis. - Never send
TX,RX,BE,PS 0,TN,VXorGP 0,x-style GPS-off writes from library code;VXandTNare read-only types withUnqualifiedvariants. ABT 1echo takes about 1.6 s, soSetBluetoothkeeps at leastBLUETOOTH_WRITE_TIMEOUT. MDvalues: 0 FM, 1 DV, 2 AM, 3 NFM, 4 DR (read only;MD band,4isN).VM band,3selects DR.MEandMRtake three-character selectors;MR bandreportsAPfor the APRS channel, whichMEandMRrefuse.ME addr,<20 fields>stores a channel (echo exact; readback identical except an empty URCALL becomesCQCQCQ);ME addr,clears it (replyME addr, thenN); 19- and 20-field forms are?/N;ME Pri,...isN. A zero-frequency record is accepted, so never send one.DC slot,callsign,memoandCS callsignstore their text verbatim and unpadded;DC slot,,is an unset or cleared MY slot, and an unconfiguredCSreadsNOCALL(Nfor a lowercase base or-0,?for an over-length base). On this radioDCis the six-slot MY callsign list selected byDS, not the D75's URCALL/RPT1/RPT2 fields; never copy the D75'sDCnames or its space padding.
Memory channel storage (image layout, verified on firmware 1.02)
- Flags at
0x2000(4 bytes: band code or0xFFempty, lockout, group, raw), records at0x4000(40 bytes, six per page, layout intypes/stored.rs), names at0x10000(16 bytes, NUL padded); one physical index for all three: regular 0-999, program scan interleavedL0,U0,L1,...from 1000 (L05= 1010), priority 1100, APRS 1101. - Verified by writing channels over CAT with every tone mode, both shift
directions, split, reverse and lockout, reading the pages through
McpSession::read_page, and decoding them to the same records; then clearing them. Not established: the band code bits (values 0x01, 0x02, 0x04, 0x05, 0x08 observed), the tone/CTCSS index basis, the name charset beyond ASCII, and the CALL channel slots. UP/DWact on the control band (firstBCfield) and take no argument; selecting a step rounds the VFO down to a multiple of it. The radio acknowledgesUP/DWin about 3 ms, shows the new frequency on the nextFQ, drops a second step sent within about 15 ms, and answers a setting write sent within about 20 ms withN.frequency_up/frequency_downkeepSTEP_SPACINGbetween steps, readFQbefore and after (pollingSTEP_READBACK_ATTEMPTStimes), return no sooner thanSTEP_SETTLEafter the acknowledgement, and fail withError::StepNotAppliedwhen the frequency never changes. Every other setter's echo arrives after the change (24 to 60 ms).
API contracts
McpSession::exitis the only exit API: oneE/ACK, then no baud change, reconnect, retry or CAT on that handle; the caller closes and drops the transport.- Exit retires the handle's protocol state, so further managed traffic returns
McpError::ConnectionRetired; an incomplete exchange returnsMcpError::RecoveryRequiredinstead and prohibits exit. Radio::probe_mcpis the one fixed read-until-exit operation: success isMcpProbeOutcome::AwaitingCatVerification, it never sends CAT on the probed connection, never opens, closes, reopens or selects a transport, and cannot report post-exit CAT.Radio::recoverresolves every journaled page before any radio I/O: each page needs unique journal membership and exactly one matchingPagePatchof the exact page length, elseMcpError::RecoveryIntentCountorMcpError::DuplicateRecoveryPage. An empty journal performs no protocol operation, and the report covers the journaled patch bits only.BytePatch,PagePatch,ConfigHeaderandRadioConfighave private fields, validated constructors and read-only accessors; header failures useFileError, and there is no unchecked header constructor.- Applying or comparing a page requires its exact length.
PatchPlanner::setvalidates the whole assignment before any claim changes, so a rejected field leaves the previous plan intact and never retains a prefix.- The private fixed-text frame writer admits exactly the PM1 page, the PM Off
MY1 page and the 69 channel name-table pages (256-byte grid from
0x10000, inside the writable global region); every other page is refused before a frame is built.ChannelNameUpdatewrites one channel's sixteen name bytes through it with a whole-page guard; an unnamed channel is sixteen NUL bytes.
Reflector Terminal lifecycle
radio::terminal::lifecycle::TerminalLifecycleowns entry, the MMDVM handoff and restoration; it sends CAT, MCP and MMDVM frames and enforces the order, but opens no endpoint and writes no file. The caller implements three traits:ControlHost(serial CAT/MCP endpoint),ModemHost(modem link open and reopen), andTerminalJournal(durable backup, plan, pre-write intent and checkpoint). The two model MCP engines stay separate; this is TM-D750 only.- Gateway Off writes the Terminal pages over the modem link; an active Terminal
route writes over the control endpoint and never sends the modem link CAT.
program_terminalrefuses any target other than the exact qualified identity, requires the fresh Gateway to equal the observed one, records intent before each write, and acknowledges exactly one exit. The checkpoint records only a pass that entered MCP. TerminalRecovery::finishrewrites the reverse plan only when the modem connection was confirmed closed; otherwise the report isBlocked. It runsverify_readinessbefore and after the reverse write and verifies the restored identity and Gateway on a fresh connection before reportingVerified.verify_readinesswaitsSETTLE, re-enumerates the pinned endpoint, and retries only an entirely silentIDtimeout within its budget and attempt cap;observe_controlopens once and sends no reboot wait.TerminalLifecycleandverify_readinessnever reopen a serial endpoint; theControlHost/ModemHostown every open and close. Timing budgets (SETTLE,READINESS_BUDGET,WINDOW,POLL_INTERVAL) are host policy sized from observation, not firmware guarantees.
Commands
Regenerate the registry and manifest with the extractor (the command below
without --check), then rerun it with --check, which fails on any difference
instead of writing. The assembly, ilspycmd and language-file paths are local,
so the command is not reproducible from the repository alone:
cargo run -p mcp-d75-extract -- extract --model tmd750 \
--assembly <mcp_d750.exe> --ilspycmd <ilspycmd> --language-file <english.lng> \
--mcp-version 1.00 --firmware 1.00 \
--output tmd750/data/mcp_d750_menu_schema.json \
--rust-output tmd750/src/memory/menu_fields.rs --strict-known-layout --check
Testing
MockTransportscripting: oneexpect(write, reply)perwrite()call, in order. The session sends a write header and its data as ONE write, so script the concatenated frame. A queued reply is delivered in as manyread()chunks as the caller asks for, with remainders re-queued.expect_hangscripts a timeout;assert_complete()proves every scripted exchange was consumed.