M17 link-layer decoder
GopherTrunk decodes the link layer of M17 — the open, Codec2-based amateur digital-voice protocol (4FSK, 4800 sym/s). This slice recovers the metadata of a transmission: who is calling whom, and in what mode. It answers “is M17 active on this frequency, and from which station” without decoding audio — the pure-Go Codec2 voice path is a planned follow-up.
Pipeline
IQ → FM demod → resample to 48 kHz → C4FM matched filter
→ Mueller-Müller symbol timing → 4FSK slice → dibit → bits
→ sync hunt (stream 0xFF5D) → LICH reassembly → LSF parse
→ events.KindM17LinkSetup
internal/radio/m17/receiverowns IQ → bits (C4FM, mirrors the P25 Phase 1 frontend);internal/radio/m17owns bits → Link Setup Frame.- Two LSF routes. The dedicated LSF frame at the head of a transmission is convolutionally coded + punctured; the LICH (Link Information CHannel) embedded in every stream frame carries 1/6 of the LSF, Golay(24,12)-coded. This decoder takes the LICH route — six consecutive LICH chunks reassemble the full 240-bit LSF using only Golay (no convolutional machinery), so a receiver tuned to an in-progress transmission picks up the metadata within ~240 ms.
- LSF fields: DST(48) + SRC(48) + TYPE(16) + META(112) + CRC(16).
Addresses are base-40 callsigns (
DecodeAddress), with the all-ones address renderedBROADCAST. TYPE yields the mode (voice / data / packet) and channel-access number. CRC-16 (poly 0x5935, init 0xFFFF) is checked over DST..META.
What’s wired
- Bus event —
events.KindM17LinkSetup, payloadstorage.M17LinkSetup(src, dst, mode, CAN, hex META, CRC-OK flag, display body). - Storage —
storage.M17Logwrites onem17_logrow per reassembled LSF (indexed onreceived_atandsrc). - REST —
GET /api/v1/m17/linksetups?limit=Nreturns the recent rows, newest first. - Config — pin an SDR under
m17.channels:
m17:
channels:
- serial: "vhf-antenna"
frequency_hz: 144_975_000 # 2 m M17 simplex calling (region-dependent)
What’s pending
- Codec2 voice. The stream-frame payload (frame number + 128-bit Codec2 frame, convolutionally coded) is skipped here; decoding it needs the pure-Go Codec2 port (a later milestone) and the K=5 Viterbi + P2 de-puncture path.
- Dedicated LSF-frame decode. The standalone LSF frame (P1 puncture + Viterbi) would surface metadata one frame sooner than the LICH route; deferred since the LICH route already covers it.
- Calibration. Sync words, LSF layout, CRC, base-40 alphabet, and the LICH structure are from the M17 specification; the Golay matrix and the C4FM slicer deviation / symbol-timing constants are validated against a synthetic encoder and should be confirmed against a real M17 capture (shared caveat with the DSC / FLEX frontends).