MDC1200 / Motorola Signaling

GopherTrunk decodes MDC1200 (“Motorola Data Communications”) — the analog in-band data burst Motorola two-way radios key at the start (and optionally the end) of a transmission. It carries the radio’s unit ID (ANI — automatic number identification) plus emergency, status, call-alert, radio-check and selective-call signaling on otherwise-analog conventional VHF / UHF voice channels. On a system that is just FM voice, MDC1200 is what tells you which radio is talking and surfaces emergency / status events.

Added in response to #438.

Modulation

MDC1200 is a 1200-baud FFSK burst using the CCIR tones mark = 1200 Hz (binary 1) and space = 1800 Hz (binary 0), carried inside the narrowband-FM voice channel — the same modulation class GopherTrunk already demodulates for MPT 1327, so the DSP frontend reuses internal/dsp/demod.FFSK. Unlike APRS the line code is plain NRZ (no NRZI differential decode).

Pipeline

IQ chunks (Fs Hz, complex64)
  → FM demod (internal/dsp/demod.FM)
  → real resampler to 9600 Hz (1200 baud × 8 oversample)
  → FFSK tone discriminator (mark 1200 Hz / space 1800 Hz)
  → Mueller-Müller symbol-timing recovery (8 sps → 1 sample/symbol)
  → DC-tracking NRZ slicer
  → 40-bit sync framer (internal/radio/mdc1200/receiver)
  → op/arg/unit-ID parse + CRC-16 check (internal/radio/mdc1200)
  → events.KindMDC1200Message on the bus
  → storage.MDC1200Log → mdc1200_log SQLite table
  → GET /api/v1/mdc1200/messages → /mdc1200 web panel

The frame layout, after the 40-bit sync word 0x07 09 2A 44 6F (most-significant bit first), is 112 payload bits column-interleaved over a 16×7 grid. De-interleaved and packed LSB-first, the header bytes are:

Bytes Meaning
data[0] op (operation code)
data[1] arg (operation argument)
data[2:4] unit ID (big-endian)
data[4:6] CRC-16 of data[0:4] (little-endian on the wire)
data[6:] redundancy (over-the-air FEC; not yet exploited)

The CRC is CRC-16/CCITT with reflected in/out, polynomial 0x1021, initial value 0x0000 and final XOR 0xFFFF. The sync hunt tolerates a few bit errors and accepts the bit-complemented sync word so a flipped FM discriminator (inverted tone sense) still decodes.

Operations

The decoder resolves a human label for the common Motorola CPS opcodes (PTT ID / ANI, emergency, status, radio check, call alert / page, selective call, radio inhibit / enable, remote monitor). The op/arg table is best-effort and intentionally non-exhaustive — many vendor-specific and extended opcodes exist; unrecognised pairs surface the raw op/arg so nothing is silently dropped. The unit ID and CRC are always decoded regardless of the label.

Double packets (extended two-block messages, op 0x35 / 0x55) are framed as two consecutive 112-bit blocks; the second block’s header bytes are attached but its vendor-specific payload interpretation is left to a follow-up.

Configuration

Each entry pins one SDR to a conventional analog voice channel:

mdc1200:
  channels:
    - serial: "vhf-antenna"
      frequency_hz: 154_000_000   # the analog voice channel to monitor
      drop_bad_crc: false         # true to drop CRC-failed bursts

Leave drop_bad_crc false to see CRC-failed bursts on the panel (flagged with crc_ok=false and dimmed); flip it on for noisy channels.

What’s surfaced

  • Bus eventevents.KindMDC1200Message, payload storage.MDC1200Message.
  • Storage — the mdc1200_log SQLite table (op, arg, unit ID, operation, body, raw hex, crc_ok), indexed by time and unit ID.
  • RESTGET /api/v1/mdc1200/messages?limit=N (default 200, max 5000); 503 when the daemon runs without storage.path.
  • Web — the /mdc1200 panel polls every 5 s, tinting emergency bursts red and dimming CRC failures.

Licensing note

The MDC1200 protocol facts implemented here (sync word, interleave geometry, CRC parameters, opcode semantics) are public protocol details. This is a clean-room Go implementation; no third-party decoder source — including the GPL-licensed reference libraries — is incorporated, keeping the decoder under the project’s Apache-2.0 license.