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 event —
events.KindMDC1200Message, payloadstorage.MDC1200Message. - Storage — the
mdc1200_logSQLite table (op, arg, unit ID, operation, body, raw hex, crc_ok), indexed by time and unit ID. - REST —
GET /api/v1/mdc1200/messages?limit=N(default 200, max 5000); 503 when the daemon runs withoutstorage.path. - Web — the
/mdc1200panel 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.