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/receiver owns IQ → bits (C4FM, mirrors the P25 Phase 1 frontend); internal/radio/m17 owns 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 rendered BROADCAST. 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 eventevents.KindM17LinkSetup, payload storage.M17LinkSetup (src, dst, mode, CAN, hex META, CRC-OK flag, display body).
  • Storagestorage.M17Log writes one m17_log row per reassembled LSF (indexed on received_at and src).
  • RESTGET /api/v1/m17/linksetups?limit=N returns 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).