DSC / Marine Distress

GopherTrunk decodes Digital Selective Calling messages — the SOLAS-mandated digital signalling that triggers distress / urgency / safety / routine calls on marine VHF channel 70 (156.525 MHz) and the medium / high-frequency DSC channels (2.187.5, 8.414.5, 12.577, 16.804.5 kHz). DSC is what fires every GMDSS distress alert; the routine calls broadcast the working voice channel two stations are about to switch to. A coast-guard MMSI lighting up the channel-70 stream is near-instant visibility into search-and-rescue activity.

This page documents the end-to-end pipeline: the DSP frontend that turns an IQ stream into decoded sequences, what’s persisted, what’s queryable, and what the web panel renders.

What’s wired

Events bus

  • events.KindDSCMessage — published once per decoded DSC sequence. Payload is a storage.DSCMessage carrying source MMSI + format (distress / all-ships / individual / …) + category (distress / urgency / safety / routine) + nature (for distress alerts) + position (for distress alerts that included one) + time UTC.

Storage

  • New SQLite table dsc_log (append-only migration alongside vessel_log, aprs_log, pager_log):
    • received_at (nanoseconds), format, category, self_mmsi, target_mmsi, nature, time_utc, latitude, longitude, has_position, body, raw_hex
    • Indexes on (received_at) and (self_mmsi, received_at)
  • storage.DSCLog bus subscriber writes one row per KindDSCMessage event. Mirrors the VesselLog / APRSLog lifecycle — subscribes at construction so events published before Run() begins aren’t lost.

REST

  • GET /api/v1/dsc/messages?limit=N — most recent sequences, newest first. Default 200, max 5000. Returns 503 when the storage layer isn’t wired (daemon started without storage.path).

Web panel

  • /dsc — live list with columns:
    • Received (HH:MM:SS, daemon clock)
    • Format (distress / all-ships / individual / group / …)
    • Category (distress / urgency / safety / routine)
    • Self MMSI
    • Target / Nature (target MMSI for individual / group calls; distress nature for distress alerts)
    • Body (one-line summary; distress alerts include the time UTC on a second line)
    • Lat / Lon (em-dash for non-position-bearing messages)
  • Polls every 5 s. Rows tint by category: distress = red, urgency = orange, safety = blue, routine = default.

Protocol layer (internal/radio/dsc)

Pure-Go DSC message parser. The bit-stream layer above (separate PR) handles sync detection, BCH(10,7) syndrome check, and the DX / RX redundancy merge — by the time symbols reach dsc.Decode each entry is one 7-bit value 0..127.

  • BCH(10,7) codec (bch.go) — encode + check helpers for the CRC-3 wrapper ITU-R M.493-15 §3.4 specifies. Generator polynomial g(x) = x³ + x + 1 (binary 1011). The code’s minimum Hamming distance is 2, not 3, so the syndrome reliably detects single-bit errors but doesn’t reliably correct them at this layer — DSC achieves the actual correction via DX/RX redundancy (each character is sent twice on the wire and the receiver compares the two streams).
  • MMSI codec (codec.go) — 5 symbols × 2 decimal digits per symbol decode to a 9-digit MMSI (the 10th digit is the format-extension nibble and ignored).
  • Position codec (codec.go) — 5 symbols carrying a 10-digit position string Q.DD.MM.DDD.MM where Q is the quadrant (0 = NE, 1 = NW, 2 = SE, 3 = SW). The all-9s sentinel for “position unknown” surfaces as HasPosition = false.
  • Type dispatch (dsc.go) — recognises every numbered format (112 = Distress, 116 = AllShips, 114 = Group, 120 = Individual, 102 = Geographic, 123 = AutoIndividual) and decodes the format-specific payload:
    • Distress: self-MMSI + nature of distress + position + time UTC.
    • Non-distress: target MMSI + category + self-MMSI; remaining fields stay on RawSymbols for the follow-up per-format parser.

Spec references:

  • ITU-R M.493-15 (Recommendation, 2019) — DSC message format, symbol table, BCH(10,7) check, nature-of-distress codes.
  • ITU-R M.541 — operational use, station identification, category routing.

DSP frontend

The receiver decodes DSC straight off the air. Pin an SDR to a DSC channel under dsc.channels in the config (channel 70 = 156.525 MHz on VHF; HF DSC rides 2187.5 / 8414.5 / 12577 / 16804.5 kHz):

dsc:
  channels:
    - serial: "marine-antenna"
      frequency_hz: 156_525_000
      drop_bad_fcs: false

Pipeline (one goroutine per channel, subscribing to that SDR’s iqtap broker):

IQ → FM demod → resample to 9600 sps → FFSK discriminator
   (1300/2100 Hz) → Mueller-Müller symbol timing → direct-FSK
   slicer (no NRZI) → 10-bit window → BCH(10,7) phasing sync →
   DX-grid symbol sampling → dsc.Decode → KindDSCMessage
  • internal/radio/dsc/ffsk owns IQ → bits (mirrors the MDC1200 FFSK frontend); internal/radio/dsc/receiver owns bits → message.
  • Polarity is auto-resolved: the phasing hunt accepts the DX character (125) in either tone sense and inverts the sampled symbols when it locked on the complement.
  • DX/RX time diversity: the first slice takes the documented “drop RX, use DX only” path — it locks the 20-bit DX grid via the repeating phasing character and reads DX symbols. Comparing each DX character against its RX twin to recover BCH failures is a yield-improving follow-up. On-wire bit order, tone sense, and DX/RX offset are validated against a synthetic modulator; confirming them against a captured ITU-R M.493 signal is the remaining real-world calibration step.

What’s pending

  • Multi-frame protocol. A few DSC sequence types span multiple slots when transmitted (Auto-Individual ACK chains, multi-recipient calls). The single-frame parser covers the operational majority; the multi-frame path needs a per-MMSI buffer plus a sequence reassembler.

    Live map

Distress alerts that included a position render as red, oversized markers on the shared Leaflet map at the top of /dsc — the larger radius + distress-red colour pull the operator’s eye immediately. Nature of distress (“fire / explosion”, “sinking”, etc.) appears in the marker tooltip. The same <PositionMap> component renders on /aprs, /ais, and /adsb.

References

  • ITU-R M.493-15 (2019) “Digital selective-calling system for use in the maritime mobile service” — https://www.itu.int/rec/R-REC-M.493
  • ITU-R M.541 — operational guidance.