Part 8 of From Spec to Shipping, a 14-part series on how a protocol decoder actually gets written — from standards documents and independent references to code you can trust on air. Part 7 laid out the rules for tests that keep the power to call your code wrong. This part is the campaign where every one of those rules — and most of the series’ others — got exercised at once: the Motorola Type II / SmartNet decoder, shipped on framing that matched no real reference, caught by an operator who could never lock, and rebuilt constant-by-constant from decoders proven on air. It is the villain’s biggest scene and the method’s clearest demonstration.
TL;DR: GopherTrunk’s original SmartNet decoder framed on a 24-bit sync
0xA4D7AA, a 32-bit OSW and BCH(64,16,11) — a format matching no real reference, so every operator hunt ended incchunt: hunt failedwhile every synthetic test stayed green (#1143). The rebuild (internal/radio/motorola/) ports the real format from OP25rx_smartnetand trunk-recorder’sSmartnetParser: 8-bit sync0xAC, 84-bit frames trusted only when the next sync lands 76 bits later, stride-19 deinterleave,parity[i]=info[i]^info[i-1]ECC, 27 data + 10 CRC bits with the data inverted on the wire (masks0xCC38/0x0D5, CRC complemented) — and a physical layer re-laid as 3600-baud 2-FSK at ±1.2 kHz into an 18 kHz DDC with a slow DC tracker. Pinned by reference-literal tests plus a failing-first real-air regression; still awaiting the reporter’s capture for on-air verification, and the post says so.
Key takeaways
- A fabricated format is worse than a missing one. Placeholder framing plus round-trip tests produced a decoder that looked finished for months — the most expensive state a feature can be in, because nothing flags it but an operator’s silence.
- Rebuild from implementations proven on air, not from prose. OP25 and trunk-recorder have decoded live SmartNet systems for years; every constant in the new decoder cites one of them, per Part 2’s first rule.
- The physical layer was fabricated too. Not just the framing: the modem was wrong (MSK-flavoured, wide DDC) — a reminder that a fabricated stack is fabricated all the way down, and the audit has to start at the antenna.
- Green synthetics still prove nothing. The rebuild’s own tests are reference-pinned and failing-first — and the decoder is still marked unverified until a real capture decodes, because that gate (Part 10) is the whole lesson.
Cheat sheet
| Concern | What it does | Where it lives |
|---|---|---|
| Frame codec | sync + deinterleave + ECC + CRC-10 → 27-bit OSW | internal/radio/motorola/frame.go (DecodeOSWPayload) |
| Bracket framer | trusts a frame only when the next sync lands 76 bits later | internal/radio/motorola/process.go (ControlChannel.Process) |
| OSW semantics | 16-bit address + group bit + 10-bit command; 1–3-OSW sequences | internal/radio/motorola/osw.go, control.go |
| Band plans | channel number → frequency; 800 standard/rebanded/splinter, 900 | internal/radio/motorola/bandplan.go (ParseBandPlan) |
| Physical layer | 3600-baud 2-FSK ±1.2 kHz, DC tracker, M&M timing | internal/radio/motorola/receiver/receiver.go |
| Channel rate | 18 kHz DDC target (5 samples/symbol) | internal/scanner/ccdecoder/ddc.go (motorolaDDCTargetRateHz) |
| Reference pins | sync bits, interleave permutation, XOR masks vs OP25 literals | frame_test.go; real-air regression in process_test.go |
In this post
- The symptom — green CI, and a hunt that never locks.
- The diagnosis — framing that matched no reference anywhere.
- The real format — the OSW wire chain, constant by constant.
- The physical layer, re-laid — 2-FSK, 18 kHz, and a DC tracker that earns its keep.
- Pinning it down — the tests that can catch this class of bug, and the gate still open.
The symptom
The report was as undramatic as protocol bugs get: an operator
configured a SmartNet system, and GopherTrunk’s control-channel hunt
cycled candidates forever — cchunt: hunt failed — no control-channel
lock, every pass. No decode errors, no CRC storms, no partial locks.
Meanwhile the repository’s SmartNet tests were green, had always been
green, and covered encode, decode, corruption handling and grant
sequencing.
That combination — zero on-air function with total synthetic success — is the signature From the Issue Tracker Part 17 taught us to dread, and it should now trigger a specific reflex: audit the constants against an independent reference before touching the DSP. A weak-signal problem degrades; a wrong-constant problem is binary. This was binary.
The diagnosis
The audit was short and brutal. The original package framed on a 24-bit
sync word 0xA4D7AA, wrapped a 32-bit OSW in two BCH(64,16,11)
codewords, and none of it — not the sync, not the width, not the code —
appears in OP25, trunk-recorder, gr-smartnet, or the venerable
mottrunk.txt notes that all of them descend from. The framing had not
been mis-transcribed from a reference; it had been fabricated, and
the round-trip tests enshrined it: the encoder produced the invented
format, the decoder consumed it, and the two agreed perfectly about a
protocol that exists nowhere —
Part 7’s
trap at maximum severity. The new frame.go header states the history
in the code itself, so the next reader inherits the scar tissue along
with the constants.
SmartNet has no public standards document, which is how the vacuum
formed. But it has something
Part 2
rates even higher than a spec: two independent implementations proven
on live systems for years — OP25’s rx_smartnet.cc/.h and
trunk-recorder’s SmartnetParser — that agree with each other. The
rebuild ported every constant and transform from them, cited line by
line.
The real format
The genuine control channel is austere: 84-bit frames, back-to-back, forever.
// internal/radio/motorola/frame.go (shape)
const (
SyncBits = 8 // frame sync length
OutboundSyncHex uint32 = 0xAC // 10101100
PayloadBits = 76 // coded payload after sync
FrameBits = SyncBits + PayloadBits // 84
// Data rides inverted on the wire (rx_smartnet.h:
// ID_XOR 0x33C7, CMD_XOR 0x32A).
idXORMask uint16 = ^uint16(0x33C7) // 0xCC38
cmdXORMask uint16 = ^uint16(0x32A) & 0x3FF // 0x0D5
// CRC-10 registers (rx_smartnet.cc crc_check).
crcInit uint16 = 0x0393
crcOp uint16 = 0x036E
crcPoly uint16 = 0x0225
)
An 8-bit sync word is alarmingly short — random data matches it every
256 bits — and the reference design compensates structurally: frames
run back-to-back, so the framer trusts a frame only when the next
frame’s sync arrives exactly 76 bits after the previous one ended
(process.go, a port of rx_smartnet::rx_sym; pinned by
TestProcessRequiresBracketSync). Bracketing syncs plus the CRC-10
make the short sync safe.
Inside the bracket, the 76 payload bits deinterleave at stride 19 —
wire position k + l*19 reads out to sequence position k*4 + l —
into 38 (info, parity) pairs with parity[i] = info[i] ^ info[i-1];
two consecutive flipped parity syndromes pinpoint a flipped info bit
between them, a single-error-correcting convolutional parity check. The
first 37 corrected bits are 27 data + 10 CRC (the 38th pair is spare),
and the data is inverted on the wire: the address and command
fields un-invert through the XOR masks above, and the CRC field is
bitwise-complemented. The surviving 27 bits form one Outbound Status
Word — 16-bit address, one group bit, 10-bit command — and the
protocol’s most striking economy: there are no opcodes. A command
value that falls inside the system’s band plan is a voice channel
number; grants, system-ID broadcasts and extended functions span one to
three consecutive OSWs sequenced by a state machine (control.go),
with 0x308/0x30B opening the multi-OSW sequences and 0x2F8 as
idle. Which also explains two config keys: motorola_band_plan selects
800_standard/800_rebanded/800_splinter/900 (channel→frequency
formulas from trunk-recorder’s get_freq), and the old motorola_bch_mode
is now accepted-but-ignored — it configured a FEC layer the real
protocol never had.
The physical layer, re-laid
The audit couldn’t stop at the framing, because a fabricated stack is
rarely fabricated in only one layer. The real control channel is
3600-baud binary FSK at roughly ±1.2 kHz deviation — not the
MSK-flavoured ±900 Hz modem the old path assumed — and the rebuild
mirrors trunk-recorder’s proven smartnet_fsk2_demod: FM discriminator,
a slow DC tracker, a one-symbol boxcar filter, Mueller-Müller timing,
zero-threshold slicer (internal/radio/motorola/receiver/).
Two physical choices carry most of the value. The channel now
downconverts to an 18 kHz target (5 samples/symbol,
motorolaDDCTargetRateHz in ccdecoder/ddc.go) instead of riding the
48 kHz C4FM-family default — whose ±24 kHz passband also admitted
25 kHz-spaced neighbours straight into the discriminator. And the DC
tracker is load-bearing, not hygiene: at ±1.2 kHz deviation, a few
hundred hertz of residual carrier offset is a large slicer bias —
dcTrackSeconds = 0.1 holds the tracker long against the ~23 ms frame
so NRZ content is untouched, short enough to pull in oscillator drift,
and TestReceiverToleratesCarrierOffset pins the tolerance. Protocols
with wide deviation can shrug that bias off; a 3600-baud narrow-shift
FSK cannot, and knowing which kind you have is a physical-layer
reading skill straight from
Part 1.
Pinning it down
The new test suite is built so this class of failure cannot re-enter quietly, layering exactly the nets from Part 3 and Part 7:
| Net | What it pins | Test |
|---|---|---|
| Reference literals | sync bits vs OP25’s SMARTNET_SYNC_MAGIC |
TestOutboundSyncBitsMatchReference |
| Reference literals | the documented interleave permutation, probed bit-by-bit | TestDeinterleavePermutationMatchesReference |
| Reference literals | 0xCC38 / 0x0D5 masks vs ID_XOR/CMD_XOR |
TestXORMasksMatchReference |
| Failing-first, real-air | a real-format stream → lock + grant; old decoder decodes nothing | TestProcessDecodesRealAirFormat |
| Structure | chunk-boundary survival, bracket-sync requirement, ECC/CRC behaviour | process_test.go, frame_test.go |
The reference-literal tests are the only ones that can catch constant drift — a round-trip can’t, since both sides drift together. The real-air regression is the failing-first proof: run against the pre-#1143 decoder, it decodes zero OSWs from a stream in the format real systems transmit.
And then the honest line, because the method demands it: this decoder is synthetic-green, not on-air-verified. The #1143 reporter’s 854.5625 MHz capture (Airspy R2, 3 MS/s cfile) is the outstanding gate, and until it decodes, the rebuild is a strong hypothesis with excellent provenance — nothing more. (One aside from that capture’s metadata: its “≈550.3 ppm” frequency-correction warning almost certainly reflects the capture tool’s 11 ms probe window latching a different momentarily-strong carrier in the 3 MHz span, not a genuinely wild oscillator — sample-count math says the file really is 3 MS/s.)
How that principle shaped the Go code
- Provenance in the header, history included.
frame.goopens by naming its sources and the fabricated format it replaced, so the next audit starts with the full story. - Semantics stay with their reference. OSW field meanings cite
OP25/trunk-recorder in
osw.go; band-plan formulas citeSmartnetParser::get_freq— every layer’s authority is greppable. - Obsolete config degrades loudly but harmlessly.
motorola_bch_modelogs “obsolete and ignored” instead of failing configs written against the old decoder. - The fixture encoder is the real format’s inverse.
EncodeOSWFrameexists for tests — and emits frames back-to-back with the trailing sync, because Part 7’s faithful-transmitter rule applies to the fix as much as it did to the bug.
Where this goes next
SmartNet’s constants were fabricated; the next part’s were merely
wrong — and the wire had no way to say so.
Part 9
takes the same discipline to RPC protocols, where callSetAntenna = 600
silently invoked a different handler on a real server for a release —
and the fixes are the same nets, aimed at a .hpp instead of a PDF.
FAQ
Why did every SmartNet test pass while no real system ever decoded? Because encoder and decoder shared the same invented format — the self-consistent trap. Round-trip tests verify that two functions are inverses, not that either matches the air; when both were authored from the same fabricated constants, green was guaranteed and meaningless.
Where does the real SmartNet framing knowledge come from if there’s no
public spec?
From implementations proven on live systems: OP25’s rx_smartnet and
trunk-recorder’s SmartnetParser, which descend from gr-smartnet and
the mottrunk.txt reverse-engineering notes. Two independent, on-air
lineages that agree — which
Part 2
argues beats any single document, prose specs included.
How can an 8-bit sync word possibly be reliable? Alone, it isn’t — random bits match it every 256 positions. The real design trusts a frame only when it is bracketed: the next frame’s sync must arrive exactly 76 bits after the last one ended, which the back-to-back control channel guarantees, and the CRC-10 gates whatever survives. Short sync, structural redundancy — a pattern worth recognizing in other legacy formats.
What does “data inverted on the wire” mean for the OSW?
The 27 information bits are transmitted complemented. The decoder
un-inverts the address and command fields by XOR with 0xCC38 and
0x0D5 (the complements of OP25’s ID_XOR/CMD_XOR masks) and
compares against a complemented CRC field — three constants that,
wrong, produce plausible-looking garbage rather than errors, which is
why each is pinned against upstream literals.
Is the rebuilt decoder verified on a real SmartNet system yet? Not yet, and the code says so. The framing is reference-pinned and the regression suite fails against the old decoder, but per the #764/#771 discipline a green synthetic is not an on-air verdict — the reporter’s 854.5625 MHz capture is the open verification gate, and the issue stays open until it decodes.
Series navigation
Part 8 of 14 · ← Part 7: Tests That Can Disagree With You · Next → Part 9: Wire Protocols Without Schemas