VDES ASM (VHF Data Exchange System — Application-Specific Messages, ITU-R M.2092-1) — implementation notes
Native VDES ASM message decode core for xng-mode-vdes. VDES (ITU-R
M.2092-1, "Technical characteristics for a VHF data exchange system in
the maritime mobile band between 156 MHz and 162.05 MHz") augments AIS with
two new sub-systems: ASM (Application-Specific Messages) on dedicated
channels and VDE (the high-rate VHF Data Exchange links). This crate
decodes ASM only. The ASM channels — ASM 1 = 161.950 MHz and
ASM 2 = 162.000 MHz (the former AIS channels 2027 / 2028) — carry
GMSK at 9600 bit/s, h = 0.5 (±2400 Hz deviation), Gaussian filter
BT = 0.5 — the same link family as AIS (ITU-R M.1371), but reserved for
the application-specific (DAC/FID binary) traffic moved off the AIS position
channels. Clean-room: no decoder was copied or ported — only standards/spec
text. The crate splits into a spec-anchored DECODE/transport core (HDLC
deframing, CRC-16/X-25, AIS Message 6/8 transport, DAC/FID payloads) and a
GMSK IQ front end. The DECODE core is verified against independent
spec-cited bit vectors; the DEMOD is validated ONLY by a synthetic
modulate→AWGN→demod path — no real off-air VDES IQ exists.
Status: WIRED, SYNTHETIC-ONLY validation. Full runtime mode:
Mode::Vdes, MessageBody::Vdes, --mode vdes, CLI/scan paths, and a
VdesChannelDecoder that owns an xng_dsp::Ddc. The transport/payload
layer is anchored to spec-cited ground-truth bit vectors; the IQ→bits front
end is exercised only by a synthetic modulate→complex-AWGN→demod path.
There is no real off-air capture in this crate — VDES has sparse public
deployment and no published off-air ASM IQ.
wideband capture IQ
→ Ddc mix by freq_offset_hz, decimate to CHANNEL_RATE (48 kS/s)
→ demod::GmskDemod freq discriminator + DC tracker + 9600 Bd timing → NRZI decode → 1 bit/symbol
recovered NRZI-decoded bit stream
→ frame::HdlcDeframer 0x7E flag hunt, bit destuffing, octet assembly, CRC-16/X-25 FCS, per-octet bit reversal
→ frame::VdesFrame (msg_type, mmsi, wire_bytes, message_bits MSB-first)
→ asm::decode AIS Msg 6/8 transport header (source/dest MMSI + DAC/FID) + DAC=1 / DAC=200 payloads
→ asm::Asm → to_message → xng_types::Message bus form
Two entry points:
VdesChannelDecoder::new(input_rate, freq_offset_hz)— channelized IQ entry (mirrors the AIS/NAVTEX/POCSAG*ChannelDecodercontract).input_rateis any capture rate ≥ the 48 kHz channel rate (a non-integer multiple is resampled by the DDC);freq_offset_hzis the ASM channel center relative to the capture center.process(iq)feeds the DDC + demod, pushes each NRZI-decoded bit into the HDLC deframer, and returns aVec<frame::VdesFrame>for every CRC-valid frame that completed in the chunk. Wheninput_rate == CHANNEL_RATEand offset is 0 the DDC is skipped (IQ is already channelized). ReturnsErr(String)only if the DDC cannot be constructed.asm::decode(message_bits)— the verified bit→ASM transport core: reads the AIS Message 6/8 header (message ID, source MMSI, DAC/FID, dest MMSI) and the DAC/FID application payload. ReturnsNonefor any message ID other than 6 / 8.
to_message(f, frequency_hz, level_dbfs, source) normalizes a VdesFrame
into the bus Message: mode = Mode::Vdes, body
MessageBody::Vdes { kind, details } where kind is the ASM transport class
("asm-addressed" / "asm-broadcast") and details is a JSON object with
msg_id, source_mmsi, optional dest_mmsi, dac, fid, and (when
recognised) a nested app object. decode.crc_ok = true (every emitted
frame passed the HDLC FCS by construction), fec_corrected = None, RSSI from
the channel level, and the raw wire octets (FCS included) travel as raw.
to_message returns None when the frame's message ID is not 6/8 — the
runtime counts those as seen-but-not-a-message.
The public IQ constants:
CHANNEL_RATE = 48_000.0S/s — 5 samples/bit at 9600 Bd (48000 = 5·9600), so the bit clock is an integer sample count.CHANNEL_PASSBAND_HZ = 8_000.0(one-sided) — passes the ±2400 Hz GMSK swing (BT=0.5 at 9600 Bd) inside a 25 kHz channel plus realistic tuning offset, well below the 24 kHz Nyquist of the channel rate.ASM1_HZ = 161_950_000,ASM2_HZ = 162_000_000— the ITU-R M.2092-1 Annex 1 ASM channel center frequencies.
The 9600 Bd GMSK demodulator, a textbook frequency discriminator (clean-room
DSP) reusing the same approach as xng-mode-ais, but for the slightly wider
ASM pulse (M.2092-1 BT = 0.5 vs AIS's BT = 0.4):
- per-sample frequency discriminator
arg(x · conj(x_prev)); - a slow DC tracker (
FREQ_ALPHA = 0.002) that absorbs residual carrier frequency offset (tuning error, ship + receiver ppm) so only the FSK swing remains; - per-bit integrate-and-dump at 9600 Bd (
SAMPLES_PER_BIT = 5) with a zero-crossing timing nudge (TIMING_GAIN = 0.15): a sign change in the discriminator marks a bit boundary and pulls the timing phase toward it; - hard slice of the per-bit accumulator into a frequency level (±1), then NRZI decode — a transmitted 0 is a level change, a 1 is no change (per M.2092 / M.1371). One bit per symbol is appended.
GmskDemod::new asserts CHANNEL_RATE == BAUD · SAMPLES_PER_BIT.
BAUD = 9_600.0 (ITU-R M.2092-1 Annex 1, ASM). level_dbfs() reports
smoothed channel power. NRZI removes the absolute-polarity ambiguity of raw
NRZ at the demod stage, so (unlike POCSAG) the deframer does not need to try
both polarities.
The link layer is HDLC per ISO/IEC 13239 — the same profile AIS uses (ITU-R M.1371):
- Flag hunt: a rolling raw-bit window matches the
0x7Eflag (0,1,1,1,1,1,1,0in arrival order) to start collecting. - Bit destuffing: a 0 following five consecutive 1s is removed; seven or
more consecutive 1s abort the frame; six consecutive 1s (
0111111) signals the closing flag, and the trailing flag bits are stripped from the collected buffer. The closing flag is kept available to also open the next frame. - Octet assembly: destuffed bits are packed into wire octets with bit i
= the i-th arrived bit (arrival-LSB-first).
hdlc_frame_ok(fromxng_dsp::checksum) checks the trailing 16-bit CRC-16/X-25 FCS; frames that fail are dropped. - Field-order conversion: the payload octets (FCS dropped) are reversed
per octet (LSB-first wire order → MSB-first field order) to produce
message_bits— the form the ASM transport decode consumes.
Length bounds: MIN_BITS = 56 (a Message-8 broadcast with empty payload:
56 header bits) and MAX_BITS = 1280 (well above the longest single-slot
ASM). close() rejects frames that are not octet-aligned, fail the FCS, or
carry fewer than 38 message bits. The VdesFrame exposes msg_type
(bits 0..6), mmsi (source, bits 8..38), wire_bytes, and message_bits.
ITU-R M.2092-1 carries ASMs using the AIS binary-message transport
verbatim: the addressed-binary (AIS Message 6) and broadcast-binary
(AIS Message 8) structures of ITU-R M.1371-5, and the same DAC/FID
application-identifier catalogue (a 10-bit Designated Area Code + a 6-bit
Function Identifier). decode(bits) reads the transport header by absolute
(offset, width) over the MSB-first message bit string:
Message 8 (broadcast ASM) — kind = "asm-broadcast":
| Field | Bits | Width |
|---|---|---|
| message ID (= 8) | 0..6 | 6 |
| repeat indicator | 6..8 | 2 |
| source MMSI | 8..38 | 30 |
| spare | 38..40 | 2 |
| DAC | 40..50 | 10 |
| FID | 50..56 | 6 |
| application data | 56.. | — |
Message 6 (addressed ASM) — kind = "asm-addressed":
| Field | Bits | Width |
|---|---|---|
| message ID (= 6) | 0..6 | 6 |
| repeat indicator | 6..8 | 2 |
| source MMSI | 8..38 | 30 |
| sequence number | 38..40 | 2 |
| destination MMSI | 40..70 | 30 |
| retransmit flag | 70 | 1 |
| spare | 71 | 1 |
| DAC | 72..82 | 10 |
| FID | 82..88 | 6 |
| application data | 88.. | — |
decode returns None for any message ID other than 6 / 8. The decoded
Asm carries msg_id, source_mmsi, dest_mmsi (Some only for
Message 6), dac, fid, and an app JSON value.
The DAC/FID catalogue is the one shared with AIS Message 6/8, catalogued by
IMO SN.1/Circ.289 ("Guidance on the use of AIS application-specific
messages") for DAC=1 and UNECE SC.3/176 (Inland AIS) for DAC=200. Seven
spec-cited payloads are decoded; each arm of app_decode cites its governing
clause, and every field's offset/width/scaling is anchored to the BSD gpsd
driver_ais.c tables used as a fact reference (re-asserted against an
independent bit packer, plus a pyais cross-check for DAC=200 FID=10):
- DAC=1 FID=11 — Meteo/hydro (IMO236, legacy layout) — distinct from
FID 31: latitude FIRST (24-bit) then longitude (25-bit), packed
ddhhmm, unsigned temperature encoding. - DAC=1 FID=16 — Number of persons on board (Circ.289 Annex; ITU-R
M.1371-5 Annex 5 §3.10): a 13-bit unsigned count,
0 = N/A(omitted). - DAC=1 FID=17 — VTS-generated/synthetic targets: the first 122-bit target report (target id type/id, lat/lon, COG, SOG, UTC second).
- DAC=1 FID=18 — Clearance time to enter port (addressed): message linkage, UTC month/day/hour/minute, port name, UN/LOCODE, position.
- DAC=1 FID=31 — Meteorological and hydrological data (Circ.289 Annex; ITU-R M.1371-5 Annex 8): a 360-bit application block. Longitude FIRST (25-bit signed) then latitude (24-bit signed), both 1/1000 minute (raw / 60000 → degrees) with 181° / 91° N/A sentinels; position accuracy; UTC day/hour/minute; average wind speed + gust (7-bit kt, 127 = N/A); wind-gust direction; wind direction (9-bit deg, 360 = N/A); air temperature (11-bit signed 0.1 °C, -1024 = N/A); relative humidity (7-bit %, 101 = N/A); and the deeper IMO289 weather block — dew point, pressure + tendency, visibility, water level + trend, surface current, waves/swell, sea state, water temperature, salinity, ice.
- DAC=200 FID=10 — Inland ship static & voyage (UNECE SC.3/176): ENI, length/beam (0.1 m), ERI ship/cargo type, hazard-cone count, draught, loaded/unloaded, data quality flags. (pyais-verified against two real AIVDM sentences.)
- DAC=200 FID=55 — Inland persons on board: crew / passengers / shipboard-personnel counts (255 = N/A, omitted).
All N/A sentinels are honoured — omitted, never emitted as junk values.
Variable-length / repeated-block payloads with no single hand-verifiable
vector (DAC=1 FID 14 tidal, 20 berthing, 22/23 area notice, 25 dangerous
cargo; FID 17 second-and-later targets; regional DACs 235/250/366) are
left as data_hex rather than guessed (skip-don't-fake).
details() flattens the header (msg_id, source_mmsi, dest_mmsi if
present, dac, fid) plus a nested app object (omitted when empty) into a
single JSON object for the bus body.
Any DAC/FID not in the recognised set falls through to a hex dump of
the application payload: app = {"data_hex": "<hex>"} (the application data
from its start offset, MSB-first per octet). The transport header (source
MMSI + DAC/FID, and dest MMSI for Message 6) is always decoded, so an
unrecognised application is fully attributed and its binary body preserved
verbatim — nothing is fabricated. (The sibling xng-mode-ais crate
decodes a much larger DAC/FID set for the AIS position channels and is where
to extend if needed.)
The DECODE/transport layer verifies against spec-cited ground truth — hand-built bit vectors assembled from the ITU-R / IMO field layout, not an encode→decode self-loopback. The DEMOD front end is validated only by a synthetic modulate→complex-AWGN→demod path. There is no real off-air VDES IQ anywhere in this crate (none is published).
The transport tests (tests/asm_decode.rs) use an independent MSB-first
bit packer (pack / pack_i) that lays down (value, width) pairs in
document order per the cited clause; the decoder reads by absolute
(offset, width). The two share no code, so a wrong offset or width
mismatches the hand-laid vector — this is not a self-encode/self-decode
loopback.
| Layer | Fact | Spec cite | How verified |
|---|---|---|---|
| Message 8 transport | broadcast header: source MMSI, DAC 40/FID 50, data 56 | ITU-R M.2092-1 Annex 1 + ITU-R M.1371-5 Message 8 | broadcast_msg8_header_dac_fid_source (source MMSI, DAC=1/FID=16, kind="asm-broadcast", no dest) |
| Message 6 transport | addressed header: source + dest MMSI, DAC 72/FID 82, data 88 | ITU-R M.2092-1 Annex 1 + ITU-R M.1371-5 Message 6 | addressed_msg6_header_with_dest_mmsi (source/dest MMSI, DAC/FID, kind="asm-addressed") |
| DAC=1 FID=16 | 13-bit persons-on-board, 0 = N/A | IMO SN.1/Circ.289 Annex; M.1371-5 Annex 5 §3.10 | broadcast_msg8_header_dac_fid_source (count 167 round-trips) |
| DAC=1 FID=31 | met/hydro physical fields, lon-first, sentinels | IMO SN.1/Circ.289 Annex; M.1371-5 Annex 8 | dac1_fid31_met_hydro_fields (4.0°E/52.0°N, wind, temp 15.3 °C, humidity 80%) |
| N/A sentinels | day 0, hour 24, minute 60, wind 127, dir 360, temp -1024, humidity 101, lon 181°, lat 91° | IMO SN.1/Circ.289 | na_sentinels_are_omitted_not_emitted_as_junk (all omitted; position-accuracy flag stays) |
| Unknown DAC/FID | header decoded, body preserved as data_hex |
(fallback policy) | unknown_dac_fid_preserves_raw_payload (DAC 999/FID 5 → data_hex = "abcd") |
| HDLC + FCS | flag hunt, bit destuffing, CRC-16/X-25, octet/field bit order | ITU-R M.2092-1 Annex 1 / ISO/IEC 13239 / M.1371 | roundtrip_with_stuffing (heavy-stuffing type-8 round trip), rejects_bad_fcs (corrupt FCS drops the frame) |
SYNTHETIC DEMOD validation (explicitly reported as synthetic — NO real
RF), in tests/end_to_end.rs, using modulate.rs:
modulate_msk_awgn_demod_decodes_asm— modulate a DAC=1 FID=16 broadcast ASM as MSK-shaped (rectangular-pulse) 9600 Bd IQ, add complex AWGN, run the fullVdesChannelDecoder(DDC → discriminator → timing → NRZI → HDLC → FCS → ASM), assert one frame with the exact source MMSI / DAC / FID / persons-on-board.modulate_gmsk_awgn_demod_decodes_asm— the realistic M.2092-1 ASM waveform (Gaussian frequency pulse, BT=0.5, h=0.5) through the same chain in light AWGN.wideband_capture_with_carrier_offset— a 2.4 MS/s wideband capture with the ASM channel at +50 kHz plus a deliberate 600 Hz carrier offset; exercises the DDC channelization and the demod's DC tracker absorbing the CFO.synthetic_ber_at_moderate_snr— 40 independent bursts (varying MMSI, payload, and noise seed) at a fixed SNR; requires the overwhelming majority (≥ trials − 2) to deframe and decode correctly, exercising the timing/offset loops across bit patterns rather than a single vector. A synthetic AWGN figure, not a real-RF claim.corrupted_frame_is_rejected— a flipped wire bit drops the frame (FCS) rather than emitting a bogus ASM.
The modulator (modulate.rs) is a self-generated reference, NOT an external
oracle: its waveform parameters (9600 Bd, ±2400 Hz deviation, BT=0.5
Gaussian pulse, 32-bit ASM training/ramp-up before the opening flag,
MSB-first fields) are the published M.2092-1 spec, but it only proves the
demod inverts this modulation. The transport/payload core stays
spec-anchored by its own independent-packer bit-vector tests.
VDES has sparse public deployment and the full spec detail (especially
VDE and the satellite component) is not freely available. The following are
skipped, not guessed (recorded in PROVENANCE.md "DEFERRED"):
- No real off-air validation. The entire DEMOD chain is validated only by the synthetic modulate→complex-AWGN→demod path; no recorded VDES ASM capture exists in this crate, and none is published off air. All SNR/BER figures are synthetic AWGN, not real RF.
- ASM only — no VDE links. VDE-TER (terrestrial) and VDE-SAT (satellite) high-rate data exchange use different modulation (π/4-QPSK / 8-PSK / 16-APSK), FEC, and framing per M.2092-1 — out of scope; no public worked examples to ground a clean-room decoder. The satellite VDES component is likewise not implemented.
- No coded long-ASM format. M.2092-1 specifies an optional 3/4-rate convolutional code + interleaver for the long ASM format; the implemented PHY is the uncoded GMSK + HDLC link (the AIS-style ASM transport). The coded variant is deferred — no public reference vector to ground it.
- Limited DAC/FID catalogue. Only DAC=1 (IMO international) FID=16 and
FID=31 have decoded application bodies. All other DAC/FID values (regional
DACs, the remaining IMO FIDs, inland-AIS DAC=200, the wider IALA ASM
registry) fall through to
data_hex— the header is decoded and the body preserved, but per-FID fields are not fabricated. - FID=31 weather tail deferred. Only the leading grounded scalar fields of the met/hydro block are decoded; the WMO-coded weather tail (and the wind-gust-direction field) are skipped.
- Operator-known channel offset.
VdesChannelDecoder::newrequires the caller to supplyfreq_offset_hz; there is no automatic ASM channel / carrier acquisition. The demod's slow DC tracker absorbs only residual tuning error, not a coarse offset. crc_okis alwaystruefor emitted frames. Only frames that pass the HDLC FCS reach aVdesFrame, so every bus message hascrc_ok = trueby construction; there is no soft/partial-FCS reporting.- No position output for FID=16. Only DAC=1 FID=31 carries a lat/lon; a persons-on-board (FID=16) or an unknown-DAC ASM has no map location. VDES messages surface as text/record only — not on the dashboard "beacons" layer.
- NRZI is decoded in the demod (
0 = level change,1 = no change), so the bit stream the HDLC deframer sees is already polarity-resolved — unlike POCSAG, the deframer does not try both polarities. - Bit order changes layer to layer: wire octets are assembled
arrival-LSB-first, then reversed per octet to give the MSB-first
message_bitsthat ASM fields use. The FCS is checked on the LSB-first wire octets; do not mix the two. CHANNEL_RATE = 48000is exactly5 · 9600; changing it breaks the integer samples/bit invariant (asserted bychannel_rate_is_integer_bit_multipleandGmskDemod::new).- The closing HDLC flag is retained to also open the next frame; do not assume one frame per flag or that the buffer is cleared between adjacent frames.
- FID=31 packs longitude before latitude (M.1371-5 Annex 8 order), opposite the lat-then-lon convention of many AIS position messages.
asm::decodereturnsNonefor any message ID other than 6/8, andto_messagepropagates that asNone— such frames are counted as seen but never become a bus message.- The ASM burst leads with a 32-bit training/ramp-up (M.2092-1), longer than the AIS 24-bit training; the modulator emits this, but the demod/deframer rely only on the flag hunt, not the training length.
- ITU-R Recommendation M.2092-1 ("Technical characteristics for a VHF data exchange system in the maritime mobile band between 156 MHz and 162.05 MHz"), Annex 1 — the authoritative VDES spec: ASM channel frequencies (ASM 1 / ASM 2), GMSK 9600 Bd / h=0.5 / BT=0.5 PHY, the HDLC link layer, and the reuse of the AIS Message 6/8 binary transport + DAC/FID catalogue for ASM. VDE and satellite components defined here are out of scope.
- ITU-R Recommendation M.1371-5 — AIS Message 6 (addressed binary) / Message 8 (broadcast binary) transport header bit layout, reused verbatim by M.2092-1 for ASM; Annex 5 §3.10 (persons on board) and Annex 8 (met/hydro data).
- IMO SN.1/Circ.289 ("Guidance on the use of AIS application-specific messages") Annex — the DAC=1 (IMO international) application catalogue: FID=16 persons on board, FID=31 meteorological & hydrological data, with field layouts and N/A sentinels.
- ISO/IEC 13239 — HDLC framing (flags, bit stuffing, CRC-16/X-25 FCS), the link profile shared with AIS.
docs/notes/POCSAG.md/docs/notes/NAVTEX.md— sibling GMSK/FSK modes whose*ChannelDecoder/ DDC front-end structure this crate mirrors (and which, like VDES, are spec-anchored decode + synthetic-only or off-air demod validation).crates/xng-mode-vdes/PROVENANCE.md— sourcing policy, the decoded transport/payload list, and the per-item "DEFERRED" record.