AIS / Marine
GopherTrunk decodes the Automatic Identification System messages commercial vessels broadcast on marine VHF channels 87B / 88B (161.975 / 162.025 MHz). AIS is the “transponder for ships” — every SOLAS-covered vessel (passenger ships, tankers, cargo > 300 GT) transmits Class A position reports continuously, and recreational vessels increasingly carry Class B units. The data is useful for: marine-coast monitoring, traffic deconfliction inside shipping lanes, search-and-rescue coordination, port arrival / departure tracking, and as a free positional ground-truth for receivers that want a known-good wide-area data feed.
This page documents the pipeline scaffolding: what’s wired, what’s persisted, what’s queryable, and what the web panel renders. The DSP layer (9600 Bd GMSK demod → HDLC framer → message parser) is tracked separately under “What’s pending” below.
What’s wired
Events bus
events.KindAISMessage— published once per decoded AIS message off the air. Payload is astorage.AISMessagecarrying MMSI + message type + (for position-bearing types) lat/lon/COG/SOG/ heading + (for static types) vessel name / callsign / destination / ship type / IMO.
Storage
- New SQLite table
vessel_log(append-only migration alongsideaprs_log,pager_log, etc.):received_at(nanoseconds),mmsi,type,body,latitude,longitude,sog,cog,heading,has_position,vessel_name,callsign,destination,ship_type,imo,raw_hex,fcs_ok- Indexes on
(received_at)and(mmsi, received_at)
storage.VesselLogbus subscriber writes one row perKindAISMessageevent. Mirrors theAPRSLoglifecycle — subscribes at construction so events published beforeRun()begins aren’t lost.
REST
GET /api/v1/ais/vessels?limit=N— most recent messages, newest first. Default 200, max 5000. Returns 503 when the storage layer isn’t wired (daemon started withoutstorage.path).
Web panel
/ais— live list with columns:- Received (HH:MM:SS, daemon clock)
- MMSI (with vessel name on a second line for static types)
- Type (position-a / position-b / static-voyage / static-b / base-station / aid-to-nav / …)
- Body (one-line summary)
- Lat / Lon (em-dash for static-only messages)
- SOG / COG (knots / degrees, em-dash when not applicable)
- Polls every 5 s. Messages with
fcs_ok = falsehighlight in yellow as a marginal-signal indicator.
Protocol layer (internal/radio/ais)
Pure-Go AIS message parser turning the bit-stream payload into
the events.KindAISMessage payload the bus / log / REST / UI
scaffolding above expects.
- 6-bit ASCII (M.1371-5 Table 47) — packed-text fields (vessel name, call-sign, destination) decode via the standard 64-entry character table.
- Bit-field readers —
readBitsUintandreadBitsIntpull MSB-first signed / unsigned integers from the unpacked bit stream. Signed-field sign-extension handles the spec’s signed lat/lon fields (28-bit longitude, 27-bit latitude, resolution 1/600000 minute = ~0.18 m). - Type dispatch (M.1371-5 §3.3) — bytes [0..5] are the
6-bit message-type tag, [6..7] the repeat indicator,
[8..37] the 30-bit MMSI. Type-specific layouts:
1,2,3— Class A position report (nav status, SOG, lat/lon, COG, heading, timestamp). 168 bits.4— base-station report (UTC + lat/lon). 168 bits.5— static + voyage data (IMO, call-sign, vessel name, ship type, dimensions, ETA, draught, destination). 424 bits.18— Class B position report. 168 bits.19— Class B extended position report (Class B with vessel name + ship type appended). 312 bits.24— Class B static data report — Part A (vessel name) or Part B (call-sign + ship type + dimensions).
- “Not available” sentinel handling — lat 91° / lon 181°
collapses
HasPositionto false; SOG / COGnot availablevalues pass through as their raw spec sentinels.
Spec references:
- ITU-R M.1371-5 (Recommendation, 2014) — bit-by-bit layout for every message type.
https://gpsd.gitlab.io/gpsd/AIVDM.html— the de-facto reference parser docs (gpsd’s AIVDM decoder), cross-checked against real on-the-air payloads.
Bit-stream pipeline (internal/radio/ais/receiver)
The orchestrator. Threads bits through the HDLC framer (reused
from aprs/hdlc), validates the trailing CRC-CCITT (same
polynomial 0x8408 / init 0xFFFF / final XOR 0xFFFF AX.25 uses —
AIS inherits the HDLC link-layer conventions verbatim per ITU-R
M.1371-5 §4.2), unpacks the payload bytes into the MSB-first bit
slice the AIS message parser expects, and publishes one
events.KindAISMessage per successfully-parsed message.
internal/radio/ais/receiver—Push(bit byte)consumes one LSB-first wire bit. Bus payload isstorage.AISMessagecarrying MMSI + type + (for position-bearing types) lat/lon + COG / SOG + heading + (for static types) vessel name + callsign- destination + ship-type + IMO. Options expose
DropBadFCS(silent-drop CRC-failed messages; default false) andDropNonPosition(silent-drop static / base-station chatter; default false). The receiver counts frames in / parsed / CRC-failed / emitted / too-short for future/metricssurfacing.
- destination + ship-type + IMO. Options expose
DSP frontend (internal/radio/ais/gmsk)
The IQ-to-bits layer. One gmsk.Receiver per configured AIS
channel; the daemon subscribes each to its assigned SDR’s iqtap
broker. Pipeline:
IQ chunks (Fs Hz, complex64)
→ FM demod (internal/dsp/demod/fm.FM)
→ real resampler down to 76,800 sps audio
→ GFSK matched filter (BT = 0.4, span 4 symbols)
→ Mueller-Müller symbol-timing recovery (8 sps → 1 sample/symbol)
→ zero-threshold slicer (raw NRZI bit)
→ NRZI decode (transition = 0, no transition = 1)
→ ais/receiver.Push(bit)
→ events.KindAISMessage on the bus
Stats() surfaces IQ-samples-seen + bits-emitted counters for
/metrics. The bit-stream layer’s own Stats() (frames in /
parsed / CRC-failed / emitted / too-short) is reachable via
Inner().
Configuration
ais:
channels:
- serial: "marine-antenna"
frequency_hz: 161_975_000 # 87B (or 162_025_000 = 88B)
drop_bad_fcs: false # default false; publishes CRC-failed
# messages with FCSOK=false
drop_non_position: false # default false; static-data and
# base-station messages still surface
A misconfigured entry surfaces as a startup warning and is
skipped — same non-essential treatment as paging.pocsag and
aprs.channels.
What’s pending
- Real-fixture validation. The synthetic IQ end-to-end test
for the GMSK frontend is deferred to the same follow-up that
drops captured
samples/ais/recordings; the receiver code is exercised end-to-end by the bit-stream synthetic ininternal/radio/ais/receiver/receiver_test.go(AIVDM type-1 sample → buildAISFrame → HDLC wrap → Receiver.Push → bus event with expected MMSI + lat/lon). - Multi-slot frame reassembly. Several message types span
two AIS slots when transmitted (type 5 + type 19 + type 26).
The current parser handles the single-slot variants; the
multi-slot path needs a per-MMSI buffer plus the channel-A /
channel-B re-orderer.
Live map
Position-bearing rows (Class A / B position reports, base
station, SAR aircraft) light up the shared Leaflet map at the
top of /ais — cyan markers plotted on the OpenStreetMap tile
layer with vessel name (or MMSI) on hover, camera auto-fits
to the current rowset. The same <PositionMap> component
renders on /aprs, /dsc, and /adsb.
References
- ITU-R M.1371-5 (2014) “Technical characteristics for an
automatic identification system” —
https://www.itu.int/rec/R-REC-M.1371 - AIVDM/AIVDO protocol decoding —
https://gpsd.gitlab.io/gpsd/AIVDM.html