Documentation
Getting Started
Documentation Index Installation Details Quickstart
Architecture
Alert Geometry Coverage Data Flow Sequences Design Standards Display System Architecture Eas Decoding Summary Eas Monitor V3 Architecture Sdr Service Architecture System Architecture Theory Of Operation
Development
Agents Certification Reliability Plan Component Library Contributing Fcc Part11 Compliance Matrix Javascript Api Releasing Sdr Frequency Validation User Interface Guide
Hardware
Alpha Led Guide Argon40 Zigbee Setup Gpio Guide Gps Hat Setup Hwsetup Helper Neopixel Led Control Sdr Setup Serial To Ethernet Adapters Vfd Display Setup Waveshare Rs232 Wifi Setup
Guides
Alert Signals Analytics And Reporting Api Key Management Application Settings Audio Monitoring Audit Log Review Database Backups Disk Space Cleanup Eas Config Tool Eas Test Signal Pipeline Hardware Quickstart Health Monitoring Help Https Setup Icecast Streaming Setup Ipaws Feed Integration Local Authorities Local Mail Server Manual Eas Events Mfa Totp Setup Notifications One Button Upgrade Setup Instructions Smart Setup Ssl Web Ui Guide Tailscale Setup Tts Normalization
Troubleshooting
Audio Sdr Fix Tool Firewall Requirements Polling Not Working Sdr Master Troubleshooting Guide Troubleshooting 504 Timeout Tts Troubleshooting
Security
Audit Log Integrity Security
Reference
About Alpha M Protocol Changelog Dependency Attribution Diagrams Ecig Cap To Eas Implementation Guide V1 0 Fips Data Sources Mdc1200 Nrsc4B Same Standard Nws Alert Parameters Nws Zone Catalog Ohio Eas Documentation Overview Protocols Overview Rbds Standard Repository Statistics Sage Endec Same Vtec Event Linking
Policies
Privacy Policy Sms Messaging Terms Of Use Trademark Policy

MDC1200 — Motorola Selective Calling

Implementation: app_utils/mdc1200.py · Tests: tests/test_mdc1200.py · Verification: a dedicated MDC1200 decoder such as mdc-decoder (mainline multimon-ng has no MDC demodulator)

MDC1200 is Motorola's 1200-baud FFSK signalling protocol for two-way LMR (Land Mobile Radio) systems. It carries a small amount of digital data — typically a 16-bit unit ID and an op-code/argument pair — over the same voice audio channel a radio normally uses for speech, so a receiving subscriber radio can identify who is transmitting and what event is being signalled (PTT-ID, emergency alarm, request-to-talk, status update, etc.).

EAS Station™ emits MDC1200 as one of the configurable pre/post-alert signal profiles. The packet sits outside the SAME signalling — it never overlaps the SAME header, attention tone, narration, or EOM — so it is invisible to EAS-aware decoders while being fully decodable by subscriber radios on a shared LMR channel.


1. Use case: feeding EAS into an LMR system

The driving design goal of MDC1200 support in EAS Station™ is seamless forwarding of EAS audio into a two-way radio dispatch system. With MDC1200 signals enabled and the audio output wired into a base-station radio's auxiliary input (with PTT keying handled by an external GPIO / VOX / COR loop), the on-air sequence becomes:

sequenceDiagram autonumber participant Iface as Radio interface<br/>(GPIO / VOX / COR) participant Sta as EAS Station™ audio participant Sub as Subscriber radios Iface->>Sub: PTT keys up Sta->>Sub: MDC1200 PTT-ID Pre<br/>(op 0x01, arg 0x80, unit_id) Note right of Sta: 1 s silence<br/>(MDC PLL settles) Sta->>Sub: SAME header burst × 3<br/>(decoded by ENDECs and SAME radios) Sta->>Sub: Attention tone (8 s) Sta->>Sub: Voice narration (TTS / operator audio) Sta->>Sub: SAME EOM × 3 Note right of Sta: 1 s silence Sta->>Sub: MDC1200 PTT-ID Post<br/>(op 0x00, arg 0x80, unit_id) Iface-->>Sub: PTT releases

Receiving Motorola (or compatible) subscribers will:

  • Display the calling unit ID on screen (typically as 4 hex digits).
  • Selectively unmute for that ID if selective-call is enabled, so EAS Station's transmissions only break squelch on radios authorised to hear them.
  • Log the call in the radio's call list / dispatch console.
  • Trigger user-configured tones / lights / vibrate tied to the unit ID.
  • Cleanly close the call on the post-ID, so the radio's display doesn't latch indefinitely.

Typical fleets where this is useful: county EM dispatch repeaters, volunteer fire / EMS / Skywarn / RACES nets, school and utility radio systems, industrial site-wide LMR. Anywhere there is already an MDC1200-capable fleet, EAS Station™ can plug in as just another (clearly identified) talkgroup member.

Out of scope (and intentionally so): EAS Station™ produces audio. PTT keying — the act of putting the radio into transmit — is handled by the radio interface (GPIO, COR, VOX, VOIP gateway, etc.) and is not part of the MDC1200 implementation. Configure your radio interface to key on the same trigger that starts EAS Station™ audio playback.


2. Physical layer

Parameter Value Notes
Modulation FFSK (Fast FSK / coherent CPFSK) Phase-continuous, deviation = ±300 Hz around the 1500 Hz centre
Symbol rate 1200 baud exactly
Mark frequency (logical 1) 1200 Hz 1 cycle per bit
Space frequency (logical 0) 1800 Hz 1.5 cycles per bit
Bit order on-air MSB-first per byte (Note: this differs from SAME, which is LSB-first)
Audio channel Voice band (300–3000 Hz) Sits in the same audio path as speech, so any narrowband FM voice channel can carry it
Required audio bandwidth ≈ 2.4 kHz Easily fits in 12.5 kHz / 25 kHz LMR channels

Phase continuity at mark-to-space transitions is essential — receivers FFSK demodulate by integrating the phase, so any glitch corrupts the next several bits. EAS Station™ re-uses the same eas_fsk.generate_fsk_samples renderer the SAME modem uses, which carries phase across the symbol boundary.

The renderer is parameterised on the audio sample rate (defaults to the EAS Station™-wide setting, typically 16 kHz or 22.05 kHz). At 16 kHz the on-air durations are:

Frame Bytes Bits Audio @ 1200 baud Samples @ 16 kHz
Single packet (PTT-ID, Emergency, …) 26 208 173.33 ms ~2773
Double packet (Call Alert, Selective Call) 40 320 266.67 ms ~4267

3. Frame format

EAS Station™ emits two MDC1200 frame variants depending on the op-code:

  • Single packet — 26 bytes (PTT-ID, Emergency, Request to Talk, Remote Monitor, Custom): one 14-byte interleaved payload bracketed by the standard preamble + sync prefix and a 4-byte trailing post-preamble that gives receivers a clean idle transition after the CRC passes.
  • Double packet — 40 bytes (Call Alert, Selective Call, and any other op-code/arg pair listed in :data:MDC1200_DOUBLE_PACKET_OPS): a second 14-byte interleaved payload is appended after a 4-byte inter-packet preamble that re-syncs the receiver between blocks. The second payload carries a target unit ID (the radio that should be paged or unmuted) while the first payload still carries the transmitting station's source ID and the op-code.

3.1 Single packet layout

flowchart LR P["Preamble<br/>3 bytes<br/>00 00 00"] S["Frame Sync<br/>5 bytes<br/>07 09 2A 44 6F"] PL["Payload<br/>14 bytes<br/>(info + FEC, interleaved)"] POST["Post-preamble<br/>4 bytes<br/>00 00 00 00"] P --> S --> PL --> POST classDef preamble fill:#fef3c7,stroke:#92400e,color:#78350f; classDef sync fill:#dbeafe,stroke:#1e40af,color:#1e3a8a; classDef payload fill:#dcfce7,stroke:#166534,color:#14532d; classDef post fill:#fef3c7,stroke:#92400e,color:#78350f; class P preamble; class S sync; class PL payload; class POST post;

3.2 Double packet layout

flowchart LR P["Preamble<br/>3 bytes<br/>00 00 00"] S["Frame Sync<br/>5 bytes<br/>07 09 2A 44 6F"] PL1["Payload #1<br/>14 bytes<br/>op + arg + src ID + CRC<br/>+ FEC, interleaved"] IPP["Inter-packet preamble<br/>4 bytes<br/>00 00 00 00"] PL2["Payload #2<br/>14 bytes<br/>target ID + reserved<br/>+ CRC + FEC, interleaved"] P --> S --> PL1 --> IPP --> PL2 classDef preamble fill:#fef3c7,stroke:#92400e,color:#78350f; classDef sync fill:#dbeafe,stroke:#1e40af,color:#1e3a8a; classDef payload fill:#dcfce7,stroke:#166534,color:#14532d; class P,IPP preamble; class S sync; class PL1,PL2 payload;

After differential ("XOR") modulation is applied across the entire frame buffer (26 bytes for single, 40 bytes for double) in one pass, the on-air bytes are different from the values shown above, but the logical content the receiver recovers is exactly this layout. The 4-byte inter-packet preamble and the trailing post-preamble both decode to a continuous mark-tone segment, giving receivers a re-sync window between blocks (double) or a clean tail-off (single) — the differential modulator passes a steady mark whenever the input bit doesn't change, and four 0x00 bytes contain no transitions.

3.3 Preamble — 3 bytes of 0x00

After differential modulation, an all-zero input produces a steady stream of mark tones (because no transitions occur, and the encoder inverts the "transitioned?" bit so "no change" → 1 → mark). This gives the receiver's PLL ~20 ms to lock to the 1200 Hz tone before the sync word arrives.

3.4 Frame sync — 5 bytes / 40 bits

The canonical Motorola pattern, recognised by every MDC1200 decoder:

Byte 0 1 2 3 4
Hex 0x07 0x09 0x2A 0x44 0x6F

Concatenated MSB-first:

0000 0111  0000 1001  0010 1010  0100 0100  0110 1111

After differential modulation, the on-air sync becomes 04 8D BF 66 58 (or its bit-inverse FB 72 40 99 A7 on a polarity-flipped receiver). EAS Station™ does not need to care about the on-air form because the encoder applies the same modulation as the receiver expects.

A receiver typically declares lock when at least 32 of the 40 sync bits match (a Hamming-distance threshold of ≤8) — this is what mdc1200.c style implementations call sync_bit_ok_threshold.

3.5 Payload — 14 bytes

Built in three steps from a 7-byte information block, in this order:

flowchart TD I["7-byte information block<br/>op · arg · idH · idL · crcL · crcH · status"] F["14-byte FEC block<br/>(info + 7 parity bytes)"] P["14-byte interleaved payload<br/>(= 'Payload' on the wire)"] I -->|"K=7 R=1/2 convolutional encoder<br/>taps {0, 2, 5, 6}"| F F -->|"16 × 7 bit interleaver<br/>distance-16 spreading"| P classDef stage fill:#eef2ff,stroke:#4338ca,color:#1e1b4b; class I,F,P stage;

The 7-byte information block lays out as:

Offset Field Width Description
0 opcode 8 bits What event is being signalled
1 arg 8 bits Op-code-specific argument
2 unit_id (high) 8 bits High byte of the 16-bit subscriber ID
3 unit_id (low) 8 bits Low byte of the 16-bit subscriber ID
4 CRC (low byte) 8 bits Low byte of CRC-16 over op,arg,idH,idL
5 CRC (high byte) 8 bits High byte (note: CRC is on-the-wire little-endian)
6 status 8 bits 0x00 for PTT-ID variants, 0x76 for STS / MSG

3.5.1 CRC-16

Property Value
Polynomial x¹⁶ + x¹² + x⁵ + 1 (CRC-CCITT, value 0x1021)
Implementation form Reverse polynomial 0x8408, LSB-first feedback
Initial value 0x0000
Final XOR 0xFFFF
Bit order LSB-first per input byte
Coverage 4 bytes: op, arg, idH, idL (the status byte is not covered)
def compute_crc(data):
    crc = 0
    for byte in data:
        crc ^= byte & 0xFF
        for _ in range(8):
            crc = (crc >> 1) ^ 0x8408 if crc & 1 else crc >> 1
    return (crc ^ 0xFFFF) & 0xFFFF

Verified vector:

compute_crc([0x01, 0x80, 0x12, 0x34]) == 0x3E2E

On the wire that becomes 2E 3E (low byte first) at offsets 4–5.

3.5.2 K=7 rate-1/2 convolutional FEC

The 7 information bytes are fed bit-by-bit (LSB-first per byte) into an 8-bit shift register that is initialised to zero and carries state across all 7 bytes (it is not reset per byte — this is the most common stumbling block when re-implementing the codec).

For each input bit, one parity bit is generated as:

parity = sr[6] ⊕ sr[5] ⊕ sr[2] ⊕ sr[0]

where sr[n] is bit n of the shift register after the new input bit has been shifted in at position 0 and the old bit 7 has fallen off. Equivalently, the generator polynomial is g(x) = 1 + x² + x⁵ + x⁶ (taps at positions {0, 2, 5, 6}).

The 56 generated parity bits are packed back LSB-first per byte into 7 output bytes appended to the information block, giving the 14-byte FEC block.

Verified vector:

info = [0x01, 0x80, 0x12, 0x34, 0x2E, 0x3E, 0x00]
fec  = [0x65, 0x80, 0xA8, 0x62, 0xDD, 0x88, 0x08]

The receiver runs an inverse pass that computes the syndrome over a sliding 4-bit window of (parity_calc ⊕ parity_received) and corrects single-bit errors when ≥ 3 of the last 4 syndrome bits are set, then XORs the syndrome with 0xA6 to clear the corrected position. In practice this corrects up to 3–4 corrupted bits per packet.

3.5.3 Bit interleaver — 16 × 7

The 14-byte (112-bit) FEC block is interleaved with a distance-16 mapping that spreads adjacent FEC bits 16 bits apart on the wire — so a contiguous burst error on-air becomes scattered single-bit errors in the de-interleaved stream, exactly the kind the K=7 FEC can repair.

Source-bit walk order (LSB-first per byte) maps to the output bit array as:

output_bit_index k = 0
for i in 0 .. 13:
    for bit_num in 0 .. 7:
        output[k] = (payload[i] >> bit_num) & 1
        k += 16
        if k >= 112:
            k -= 111      ← wrap, advances the column by 1

This produces the canonical 16-row × 7-column block:

output[ 0..15] ← payload bits ( 0, 16, 32, 48, 64, 80, 96, …)
output[16..31] ← payload bits ( 1, 17, 33, 49, 65, 81, 97, …)
…
output[96..111] ← payload bits (15, 31, 47, 63, 79, 95, 111)

Output bits are then packed MSB-first per byte into the final 14 interleaved bytes that make up the on-air payload.

3.6 Second info block (double packet only)

When the op-code is one of the documented double-packet variants (Call Alert 0x63 0x85, Selective Call 0x35 0x80, …) and a target unit ID is configured, a second 7-byte information block is built from the target ID, run through the same K=7 FEC + 16×7 interleaver pipeline as packet 1, and appended to the frame after a 4-byte inter-packet preamble (32 bits of 0x00 → continuous mark tone for receiver re-sync).

Offset Field Width Description
0 target_id (high) 8 bits High byte of the 16-bit target unit ID
1 target_id (low) 8 bits Low byte of the 16-bit target unit ID
2 reserved 8 bits 0x00
3 reserved 8 bits 0x00
4 CRC (low byte) 8 bits Low byte of CRC-16 over target_id_h, target_id_l, 0x00, 0x00
5 CRC (high byte) 8 bits High byte (little-endian on the wire, same as packet 1)
6 status 8 bits 0x00

The CRC over the second info block uses the same algorithm as packet 1 (CRC-CCITT reverse polynomial 0x8408, init 0x0000, final XOR 0xFFFF), independently computed over the 4-byte (target_h, target_l, 0x00, 0x00) prefix. The status byte is not covered, again matching packet 1.

When mdc1200_target_unit_id is NULL or 0, the encoder falls back to a single-packet frame whose unit_id field acts as the target — some receivers tolerate this shortcut, but real Motorola CPS-programmed subscribers normally expect the double-packet form for Call Alert and Selective Call.

3.6.1 Differential ("XOR") modulation

After the preamble, sync, payload(s), and post-/inter-packet preamble(s) have all been assembled into the final pre-modulation buffer (26 bytes for single, 40 bytes for double), each bit is replaced by a "did the input bit change?" flag, MSB-first across the entire buffer, with the previous-bit register initialised to 0. Each completed output byte is then inverted (^ 0xFF).

prev = 0
for byte in buf:
    out = 0
    for bit_num in (7, 6, 5, 4, 3, 2, 1, 0):
        new_bit = (byte >> bit_num) & 1
        if new_bit != prev:
            out |= 1 << bit_num
        prev = new_bit
    output.append(out ^ 0xFF)

The inversion translates "no change" → mark (logical 1), which matches the on-air convention every MDC1200 receiver expects. This step also makes the receiver tolerant of audio polarity inversions: a polarity-flipped receiver sees the bitwise-inverted sync word FB 72 40 99 A7 and trivially flips its decoded data to recover the original.


4. Op-code reference

The op-code/arg pair determines what action the receiving radio takes. EAS Station™ ships these symbolic presets (defined in MDC1200_OP_PRESETS):

Preset op arg Meaning
ptt_id_pre 0x01 0x80 PTT-ID at the start of a transmission
ptt_id_post 0x00 0x80 PTT-ID at the end of a transmission
emergency 0x40 0x80 Emergency alarm
request_to_talk 0x35 0x89 Request-to-talk paging
remote_monitor 0x11 0x80 Remote-monitor command
call_alert 0x63 0x85 Call Alert — page a specific target unit (unit ID = target)
selective_call 0x35 0x80 Voice Selective Call — unmute a specific target (unit ID = target)
custom any any Operator-supplied raw bytes

Target-vs-source semantics. For ptt_id_pre, ptt_id_post, emergency, request_to_talk, and remote_monitor, the mdc1200_unit_id setting is the transmitting station's own ID — receiving radios display "who is talking." For call_alert and selective_call, the same field is reinterpreted as the target subscriber's ID — the receiving radio that should be paged or unmuted. Set mdc1200_unit_id to the destination radio's programmed ID when using these two presets. Multiple targets require sending the signal repeatedly (or grouping IDs at the receiver via talkgroup programming).

For a complete-but-not-formally-published list of additional op-codes (status messages, GPS, etc.), Matthew Kaufman's mdc-encode-decode and several open-source LMR firmware projects are good cross-references. Custom raw bytes are accepted for advanced operators who need to interoperate with non-standard fleets.

4.1 Smart pre/post pairing

When both the pre and post signals are set to MDC1200 and the op-code preset is the default ptt_id_pre, EAS Station™ automatically substitutes ptt_id_post on the post-alert position. This is what _resolve_mdc1200_op_for_position() in app_utils/eas.py does — the helper is intentionally narrow: it only rewrites ptt_id_pre → ptt_id_post on the post side, and only when position == 'post'. All other presets (including ptt_id_post already chosen, emergency, request_to_talk, remote_monitor, or custom) pass through unchanged. This preserves the behaviour of operators who deliberately want the same op-code on both sides — for example sandwiching a broadcast in two emergency-alarm packets to wake every subscriber on a quiet channel.


5. EAS Station™ integration

5.1 Settings (eas_settings table)

Column Type Default Notes
mdc1200_unit_id INTEGER NOT NULL 1 1..65535 (zero is reserved). The transmitting station's source ID — what receiving radios display as the calling unit. Stored as a decimal integer; the UI and API accept either decimal (1234) or 0x.. hex (0x04D2) on input.
mdc1200_op_code VARCHAR(32) NOT NULL 'ptt_id_pre' One of the preset keys, or 'custom'
mdc1200_op_code_raw SMALLINT NULL NULL 0..255, used only when preset = 'custom'. Accepts decimal or 0x.. hex on input; rendered as 0xNN in the UI.
mdc1200_arg_raw SMALLINT NULL NULL 0..255, used only when preset = 'custom'. Accepts decimal or 0x.. hex on input; rendered as 0xNN in the UI.
mdc1200_target_unit_id INTEGER NULL NULL 1..65535 target ID for the double-packet ops (Call Alert, Selective Call). When set and the op-code is double-packet-eligible, the encoder appends a second 14-byte info block carrying this ID — making the on-air frame ~267 ms instead of ~173 ms. NULL or 0 forces single-packet emission.

Hex input policy. All three byte/word fields (mdc1200_unit_id, mdc1200_op_code_raw, mdc1200_arg_raw) accept full hexadecimal notation including the AF digits. The HTML inputs use the pattern (0[xX][0-9A-Fa-f]{1,N}|\d{1,M}) and the server parses with Python's int(value, 0). This matches Motorola CPS conventions — subscriber unit IDs are typically displayed in 4-digit hex on programming sheets and on subscriber-radio displays — and lets operators paste values directly without conversion. Decimal entry remains supported for fleets that prefer it.

These were added by Alembic migration 20260505_add_mdc1200_to_eas_settings. The auto-heal _PENDING_MIGRATIONS block in webapp/admin/maintenance.py mirrors the same DDL so installs that update DB-first also self-heal.

5.2 Configuration plumbing

load_config() (in app_utils/eas.py) reads the four DB columns into the config dict, allowing env-var overrides matching the existing pattern:

Env var Type Meaning
EAS_MDC1200_UNIT_ID int (decimal or 0x.. hex) Override source unit ID
EAS_MDC1200_OP_CODE str Override preset key
EAS_MDC1200_OP_CODE_RAW int (decimal or 0x.. hex) Override raw op byte
EAS_MDC1200_ARG_RAW int (decimal or 0x.. hex) Override raw arg byte
EAS_MDC1200_TARGET_UNIT_ID int (decimal or 0x.. hex) Override target unit ID for double-packet ops; empty/0 → single packet

_generate_chime() then routes the config into generate_mdc1200_samples(opcode, arg, unit_id, sample_rate, amplitude, status=0x00, target_unit_id=None), which produces the phase-continuous PCM samples. When the op-code is double-packet-eligible (Call Alert, Selective Call) and target_unit_id is provided and non-zero the encoder dispatches to encode_double_packet(); otherwise the classic single-packet encode_packet() path is used.

5.3 Audio assembly

The four call sites of _generate_chime (build_files pre, build_files post, build_manual_components pre, build_manual_components post) wrap the op-code in _resolve_mdc1200_op_for_position(op_code, position) so the smart pairing rule (§4.1) is applied uniformly across CAP-forwarded, OTA-relayed, and manual broadcasts.

5.4 Admin UI

templates/admin.html exposes the settings under Admin → EAS Broadcast Settings → Pre/Post-Alert Signaling with the dropdown options gated by data-eas-tone-show="mdc1200" and mdc1200-custom. The custom sub-fields accept either decimal (0..255) or 0x.. hex notation; webapp/admin/maintenance.py parses both via int(value, 0).


6. Verification & debugging

Note: mainline multimon-ng does not include an MDC demodulator (MDC is not a valid -a argument), so it cannot decode these packets. Use a dedicated MDC1200 decoder instead.

A practical choice is mdc-decoder, which decodes a WAV file directly and prints the unit IDs / op-codes it finds. Other options that decode MDC1200 include mdc-encode-decode (the reference modem this implementation is checked against), fsync-mdc1200-decode (JSON output), DSD+, and SDRTrunk.

# Generate an alert with MDC1200 signals enabled in admin, then point a
# dedicated MDC1200 decoder at the rendered WAV, e.g. with mdc-decoder:
mdc-decoder /var/www/eas-station/static/eas_messages/<your-alert>.wav

# You should see the configured unit ID and op-code reported, e.g.
#   op 0x01 / arg 0x80 (PTT-ID Pre)  for the pre-alert packet
#   op 0x00 / arg 0x80 (PTT-ID Post) for the auto-paired post-alert packet
#   op 0x63 / arg 0x85 (Call Alert)  / op 0x35 / arg 0x80 (Selective Call)
#     with their target ID for the double-packet ops

Inside the codebase, tests/test_mdc1200.py exercises every layer:

Test What it pins down
test_compute_crc_known_vector CRC-16 implementation matches the published 01 80 12 34 → 0x3E2E vector
test_apply_fec_known_vector K=7 FEC matches 01 80 12 34 2E 3E 00 → 65 80 A8 62 DD 88 08
test_interleaver_is_a_bit_permutation Interleaver loses no bits
test_xor_modulate_zero_buffer_emits_steady_mark Preamble assumption (no transitions → all-mark) holds
test_xor_modulate_round_trip Differential decode recovers the original
test_encode_packet_length_and_prefix_structure 22-byte frame, decoded preamble + sync match canonical values
test_generate_mdc1200_samples_is_phase_continuous No glitch ≥ 2·A·sin(π·f/sr) between any two adjacent samples
test_generate_mdc1200_samples_contains_both_carriers Goertzel power at 1200 Hz and 1800 Hz both ≥ 10× off-band power
test_smart_pairing_* PTT-ID pre→post substitution, only for the documented case

Run with pytest tests/test_mdc1200.py -q.


7. Limitations

  • Both the single packet (26 bytes, ~173 ms) and double packet (40 bytes, ~267 ms) variants are implemented. Double-packet ops beyond Call Alert and Selective Call (e.g. GPS coordinates, extended status messages with 4 extra bytes in the second info block) are straightforward to add by extending :data:MDC1200_DOUBLE_PACKET_OPS and supplying additional fields to encode_double_packet().
  • EAS Station™ does not implement an MDC1200 receiver. Decoding incoming MDC packets from monitored radios would require a full FFSK demodulator and the inverse of the encoder pipeline (sync hunt, de-interleave, FEC syndrome, CRC verify). Out of scope for the current signaling feature; could be added as a separate audio-source decoder.
  • PTT keying is the responsibility of the radio interface (GPIO, COR, VOX); EAS Station™ only produces audio.

8. References

  • Algorithmic specification — public algorithm description in the mdc1200.c source of the uv-k5-firmware-custom project (GPL-3.0). The algorithm itself (taps {0, 2, 5, 6}, 16×7 interleaver, reverse-poly CRC-16, differential modulation) is a 1980s Motorola specification not subject to copyright; EAS Station™'s implementation is clean-room from that algorithmic description with no source code copied.
  • Receiver verification — a dedicated MDC1200 decoder such as mdc-decoder or mdc-encode-decode. Mainline multimon-ng has no MDC demodulator and cannot decode these packets.
  • Frame format cross-referencekg-tools mdc-encode.c, the xastir MDC1200 plugin, and several Motorola Service Manual appendices describing CPS programming for MDC-equipped subscribers (XPR, CDM, GM340, …) all describe the same wire format.

This document is served from docs/reference/protocols/MDC1200.md in the EAS Station™ installation.