One native SDR decoder for the whole aviation + maritime radio stack.
xng decodes ACARS, VDL Mode 2, HFDL, Inmarsat Aero, Inmarsat STD-C/EGC, Iridium, AIS, and Mode S/ADS-B in a single Rust binary — replacing acarsdec, vdlm2dec, dumpvdl2, dumphfdl, JAERO, Scytale-C, gr-iridium, and iridium-toolkit with consistent, tested decode cores that share one capture, one message model, one application layer, and one set of outputs (including first-class airframes.io feeding). All nine modes are implemented, validated, and merged — including the complete Iridium stack (ring alerts through ACARS-over-SBD with a wideband burst-hunting front end) — and shipped as tagged releases with binaries for Linux (x86_64/arm64, tarball + .deb), macOS Apple Silicon, and multi-arch Docker images.
# Two ACARS channels from one RTL-SDR, fed to Airframes:
xng listen --sdr driver=rtlsdr -r 2400000 -c 131.500M \
--channels 131.550,131.725 \
--feed-airframes --station-id XX-KSEA-ACARS112:01:13.402 [acars] 131.550 MHz ACARS N401UA UA1989 lbl=H1 ok | #M1B...
12:01:14.118 [acars] 131.725 MHz ACARS N831UA UA0233 lbl=B6 ok [ADS-C 47.5512 -122.3052 34000 ft]
| Existing tools | xng | |
|---|---|---|
| Decoders | One binary per mode (acarsdec, dumpvdl2, dumphfdl, JAERO, …), each with its own CLI, output format, and quirks | One binary, one CLI, every mode |
| SDR usage | One SDR per decoder | Many channels of one mode from a single capture; one dongle can watch 12 ACARS channels |
| Validation | Varies | Every decode core is validated against real off-air recordings or the reference implementation's own test vectors, with the captures vendored into CI so conventions can never silently regress |
| Application layer | libacars bolted on, or nothing | Built-in ARINC 622 (ADS-C positions, CPDLC rendered as readable text — REQUEST CLIMB TO FL360), media advisory, H1 sublabels — shared by every ACARS carrier (VHF, VDL2, HFDL, Aero, Iridium SBD) |
| Outputs | Per-tool formats | Pretty console, JSON/JSONL, acarsdec-compatible UDP, Airframes feeding, Prometheus metrics, and the multiplexed gRPC/QUIC asf-2.0 protocol — identical across all modes |
| Tooling | None | Interactive TUI (spectrum, waterfall, message browser), auto-scanner that proposes ready-to-run configs, site survey/soak reports with gain tuning, IQ-file inspection, built-in self-test |
| License | Mostly GPL | MIT/Apache-2.0 dual license; cores are clean-room from public specs or ported from MIT/BSD projects with attribution |
The provenance discipline is part of the engineering: every core has a
PROVENANCE.md recording exactly what came from where, and the off-air
validation campaigns are documented finding-by-finding (several on-air
conventions — invisible to loopback testing — were caught only this way).
| Mode | --mode |
Band | What you get | Validation |
|---|---|---|---|---|
| VHF ACARS (ARINC 618) | acars (default) |
118–137 MHz | ACARS + applications | Live off-air (RTL-SDR), CRC-verified, fed to production Airframes end-to-end |
| VDL Mode 2 (ICAO Annex 10) | vdl2 |
136.6–137 MHz | ACARS-over-AVLC, AVLC link events, XID handoff parameters (incl. ground-station lists), ATN-B1: X.25/CLNP/COTP transport (+facilities, ES-IS, IDRP route updates with path attributes and NLRI), protected-mode CPDLC with the full element tables and phraseology, CM logon and ground PDUs, ground-station naming via --gs-file |
Off-air capture vs dumpvdl2 ground truth |
| HFDL (ARINC 635) | hfdl |
2.8–22 MHz | Squitters, logons, positions, ACARS, over-the-air system table; channel-selectivity filtering (+4.5–5 dB measured sensitivity) | Off-air 21 931 kHz capture, field-exact vs dumphfdl |
| Inmarsat Aero L (JAERO port) | aero |
1545–1547 MHz | P-channels 600/1200 bps + 10.5 kbps, ACARS/ADS-C/CPDLC, C-channel assignment SUs (voice-circuit frequencies from call setup); C-channel voice circuits (8.4 kbps OQPSK): AMBE voice-frame extraction + call-progress/telephony signal units | Real Inmarsat recordings: 600 bps + 10.5k both decode off-air; C-channel RF loopback |
| Inmarsat Aero C bursts | aero-c |
C-band | R/T-channel signal units | RF loopback |
| Inmarsat STD-C / EGC | std-c |
1537–1542 MHz | NCS frames, EGC SafetyNET/FleetNET text, logical-channel messages | Off-air EGC capture, field-exact vs reference |
| Iridium | iridium |
1616–1626.5 MHz | Ring alerts (live satellite positions), broadcasts, ACARS over SBD, pager messages (IMS) with multi-part reassembly, voice-channel classification (VDA/VO6/VOD/VOZ/VOC) with AMBE extraction, IP-channel frames (IIP ARQ / IIQ / IIR), wideband burst hunting across the band | Every layer validated: bit-perfect demod of gr-iridium's reference burst (direct and via the wideband hunter) + field-identical decode vs the iridium-toolkit oracle |
| AIS (ITU-R M.1371) | ais |
161.975/162.025 MHz | NMEA AIVDM (UDP + TCP servers) plus field decode for types 1–27: positions/kinematics (class A/B, SAR, long-range), static & voyage data, binary and safety messages, aids to navigation, DGNSS, link/channel management, group assignment | pyais-exact field vectors + canonical published test vector |
| Mode S / ADS-B | adsb |
1090 MHz | CPR positions (airborne, surface via --receiver-pos), velocity, squawk, altitude replies, Comm-B/BDS registers (callsign, selected altitude, track/turn, heading/speed — pyModeS-validated), per-aircraft tracking, SBS + Beast outputs |
Published vectors (1090 Riddle) + field-exact vs pyModeS |
All multi-channel modes decode any number of channels from one capture.
Wrapped external decoders (xng extern) remain available as a
second-class path — they get every xng output and the application layer.
| Device | --sdr |
Backend | Notes |
|---|---|---|---|
| RTL-SDR | driver=rtlsdr |
SoapySDR | The budget workhorse for VHF (ACARS, VDL2, AIS) and — with an L-band antenna + LNA — Aero, STD-C, Iridium, ADS-B |
| Airspy R2 / Mini | driver=airspy |
native (libairspy, --features airspy) |
24 MHz–1.75 GHz, 12-bit; serial=… (hex) selects a unit, bias=1 powers an LNA. Validated live (Mini: off-air ACARS at 6 MS/s) |
| Airspy HF+ / Discovery | driver=airspyhf |
native (libairspyhf, --features airspyhf) |
The classic HFDL receiver; 768 kS/s divides cleanly into every xng HF/VHF channel rate |
| SDRplay (RSP series) | driver=sdrplay |
SoapySDR | The Soapy module wraps the proprietary API |
| Anything else | per its Soapy module | SoapySDR | HackRF, LimeSDR, USRP, BladeRF, … |
--gain is in dB everywhere, and omitting it selects hardware AGC. The
native Airspy backends map dB sensibly onto the actual hardware controls
(R2/Mini: 22-step linearity gain; HF+: attenuator/preamp, bigger = more
gain). With a native backend compiled in, its driver name routes to it
automatically; add backend=soapy to force SoapySDR instead. IQ-file
input (xng decode) needs no hardware or SDR libraries at all.
Grab a release —
tarballs for Linux x86_64/arm64 and macOS Apple Silicon, .deb packages
for Debian/Ubuntu (which declare the runtime libraries), and SHA256SUMS.
The binaries need libsoapysdr at runtime (plus libairspy/libairspyhf
for native Airspy):
sudo apt install ./xng_0.9.0_arm64.deb # pulls runtime deps
# or
tar xzf xng-v0.9.0-x86_64-unknown-linux-gnu.tar.gz && sudo cp xng-*/xng /usr/local/bin/Multi-arch Docker images (amd64/arm64/armv7) are published per tag:
docker run --rm ghcr.io/airframesio/xng:latest --versionRequirements: a stable Rust toolchain, a protobuf compiler, and (for live SDR use) SoapySDR with your vendor module.
# Debian / Ubuntu
sudo apt install protobuf-compiler libsoapysdr-dev soapysdr-module-all
# macOS
brew install protobuf soapysdr
# Build (binary at ./target/release/xng)
cargo build --release
# Run the test suite (includes the vendored off-air captures)
cargo test --workspaceAirspy owners can skip the SoapyAirspy shim: native backends for the R2/Mini (libairspy) and the HF+ / Discovery (libairspyhf) are built in with feature flags:
sudo apt install libairspy-dev libairspyhf-dev # or: brew install airspy airspyhf
cargo build --release --features airspy,airspyhfNo hardware? Everything works from IQ recordings (xng decode,
xng tui --file), and cargo build --no-default-features skips SoapySDR
entirely. A Dockerfile and a Debian packaging script are included.
docker build -t xng . && docker run --rm xng --versionxng devices # what SDRs are attached?
xng selftest # end-to-end pipeline sanity check
# No idea what's receivable at your site? Let the scanner find out:
xng scan --sdr driver=rtlsdr --gain 28 --modes acars,vdl2,ais --dwell 120 --out scan.json
# → prints verdicts per channel and ready-to-paste `xng listen` command lines.
# For HFDL it even learns new frequencies from the over-the-air system table.
# Then qualify the site properly: a 15-minute soak across the whole ACARS
# plan, with an empirical gain sweep first, ending in a per-channel report
# (frames, CRC rate, levels) plus reception advice:
xng survey --sdr driver=rtlsdr --mode acars --tune-gain --out survey.json# VDL Mode 2: four channels including the worldwide CSC
xng listen --sdr driver=rtlsdr --mode vdl2 -r 2400000 -c 136.800M \
--channels 136.650,136.800,136.925,136.975
# HFDL on an HF-capable SDR (channels per the public system table)
xng listen --sdr driver=sdrplay --mode hfdl -r 768000 -c 10060.000k \
--channels 10027k,10060k,10063k,10081k,10084k,10087k
# Same, on an Airspy HF+ Discovery via the native backend (no Soapy
# module needed; build with --features airspyhf). Hardware AGC unless
# --gain is given; add serial=... to pick among several units.
xng listen --sdr driver=airspyhf --mode hfdl -r 768000 -c 10060.000k \
--channels 10027k,10060k,10063k,10081k,10084k,10087k
# Inmarsat Aero L-band (patch antenna + LNA)
xng listen --sdr driver=rtlsdr --mode aero -r 2400000 -c 1546.000M \
--channels 1545.880,1546.045
# Inmarsat STD-C: maritime safety broadcasts in plain text
xng listen --sdr driver=rtlsdr --mode std-c -r 2400000 -c 1537.500M \
--channels 1537.700,1537.100
# Iridium, wideband: point at the band, get everything — bursts are
# hunted across the whole capture (ring alerts, broadcasts, and the
# duplex-hopping SBD/ACARS traffic). Triggered by --channels equal to
# the capture center:
xng listen --sdr driver=rtlsdr --mode iridium -r 2000000 -c 1626.000M \
--channels 1626.000
# Iridium, fixed channels: just the simplex ring-alert/messaging
# frequencies (cheaper; live satellite positions every few seconds)
xng listen --sdr driver=rtlsdr --mode iridium -r 2000000 -c 1626.250M \
--channels 1626.271,1626.104
# AIS: both channels from one capture
xng listen --sdr driver=rtlsdr --mode ais -r 2400000 -c 162.000M \
--channels 161.975,162.025
# Mode S / ADS-B (consumes the whole capture)
xng listen --sdr driver=rtlsdr --mode adsb -r 2000000 -c 1090.000M --channels 1090xng survey qualifies a site for one mode: it monitors every channel in
the mode's plan (rotating capture windows when the plan exceeds the SDR
bandwidth), prints interim tables as it goes, and ends with per-channel
statistics — frames, CRC pass rate, frames/min, levels — plus reception
advice and a ready-to-run listen command. Ctrl-C ends early but still
reports.
# 15-minute ACARS soak over the full plan, gain picked empirically
xng survey --sdr driver=rtlsdr --mode acars --tune-gain --out survey.json
# Scan first, then soak only active channels — plus the mode's core
# worldwide channels, which short scans routinely undersell
xng survey --sdr driver=rtlsdr --gain 28 --mode vdl2 --scan --duration 1800
# Specific channels, live message output, messages archived to JSONL
xng survey --sdr driver=rtlsdr --gain 28 --mode acars --duration 3600 \
--channels 130.025,131.550,131.725 --show-messages --jsonl soak.jsonlxng iq-info capture.cf32 -r 2000000 -c 131500000 # duration, power, spectral peaks
xng decode capture.cf32 -r 2400000 -c 131.500M --channels 131.550,131.425
xng decode vdl2.cf32 --mode vdl2 -r 50000 -c 136.975M --channels 136.975 --jsonLive message browser with a detail pane (press v to flip between a
human-readable rendering and raw JSON), per-channel statistics, spectrum
with channel markers, and a waterfall — over a live SDR or a replayed
file:
# Zero config: channels, center, and sample rate come from the mode's
# built-in plan — as many channels as fit the capture width and CPU.
# Native-backend devices are asked which rates they support (an Airspy
# Mini gets 3 MS/s automatically, not the plan's 2.4):
xng tui --sdr driver=rtlsdr
xng tui --sdr driver=airspy --mode vdl2
# Explicit tuning still works (and is required for --file replay)
xng tui --sdr driver=rtlsdr -r 2400000 -c 131.500M --channels 131.550,131.125
xng tui --file capture.cf32 -r 2400000 -c 131.500M --channels 131.550Every mode and every command shares the same output options:
--feed-airframes --station-id XX-KSEA-ACARS1 # Airframes (feed.airframes.io)
--udp host:5550 # acarsdec-compatible JSON
--json # raw JSON to stdout
--jsonl messages.jsonl # JSONL file
--metrics 0.0.0.0:9090 # Prometheus (frames, CRC, levels)
--sbs 0.0.0.0:30003 # SBS/BaseStation TCP (Mode S)
--beast 0.0.0.0:30005 # Beast binary TCP (Mode S)
--nmea-tcp 0.0.0.0:10110 # NMEA AIVDM TCP (AIS)
--mqtt mqtt://user:pass@broker:1883 # MQTT (JSON to <prefix>/<mode>)
--mqtt-topic xng # MQTT topic prefix
--asf2-grpc http://ingest:6001 # asf-2.0 over gRPC
--asf2-quic ingest:6011 # asf-2.0 over QUIC (TLS verified)ACARS traffic can be filtered by label before it reaches any output:
--filter-labels H1,Q0 passes only those labels; --exclude-labels SQ
drops the listed ones. Non-ACARS messages always pass. VDL2 console
lines can name ground stations via --gs-file stations.json (a JSON
object mapping hex AVLC addresses to names).
Decode performance is benchmarked against the strongest open decoders on off-air captures — VDL2: 44 frames vs dumpvdl2's 41; ADS-B: 161 unique vs dump1090-fa's 162 (plus 7 it misses); AIS: 68 % of AIS-catcher and closing — with every result fenced by a CI regression gate (bench/, docs/notes/BENCHMARKS.md).
asf-2.0 (docs/ASF2.md) is xng's multiplexed feeding
protocol: one protobuf schema carrying every channel/SDR/mode over a
single gRPC or QUIC connection, with reconnect and backpressure handling.
xng ingest is the reference server:
xng ingest --grpc 0.0.0.0:6001 --quic 0.0.0.0:6011Existing decoder deployments can join the xng bus (and get asf-2.0, Airframes feeding, and the application layer — ADS-C and CPDLC decode even from wrapped ACARS):
dumpvdl2 ... | xng extern --format dumpvdl2 --asf2-grpc http://ingest:6001
xng extern --format dumphfdl --feed-airframes --station-id XX-... \
-- dumphfdl --soapysdr driver=sdrplay ...ACARS from any carrier flows through one application layer
(xng-acars, ported from MIT libacars):
- ADS-C (ARINC 622): full decode — positions, altitudes, contract tags — conformance-tested against real off-air messages.
- CPDLC, both dialects: FANS-1/A (over ACARS) and ATN-B1
(over VDL2/CLNP) rendered as readable text with decoded arguments —
REQUEST CLIMB TO FL360,AT 14:32 EXPECT M0.84, free text, vertical rates, headings, multi-element messages, and route clearances (ASSIGNED ROUTE DEST KSFO, ROUTE J501 OAK). - MIAM (ARINC 841): single-transfer CORE PDUs decompressed (DEFLATE), file-transfer signalling decoded.
- OHMA: Boeing aircraft-health JSON unwrapped (base64 + zlib) into structured output.
- Multi-block reassembly: long messages spanning ACARS blocks are stitched back together (libacars-equivalent keying and timeouts), and the application layer re-runs over the complete text.
- Media advisory, H1 sublabel/MFI handling.
| Crate | Role |
|---|---|
xng-types |
Normalized message model shared by everything |
xng-dsp |
Channelizer, DDC, FIR/NCO, Viterbi, Reed-Solomon, CRCs, scramblers |
xng-sdr |
SoapySDR, native Airspy (libairspy/libairspyhf), and IQ-file sample sources |
xng-acars |
ACARS application layer (ARINC 622, ADS-C, CPDLC, media advisory) |
xng-proto |
asf-2.0 protobuf schema + conversions |
xng-mode-* |
One decode core per mode, each with a spec-faithful modulator for loopback tests, vendored validation fixtures, and a PROVENANCE.md |
Each core also ships examples/ harnesses (offair, dumpbits, …) used
for the validation campaigns — point them at your own captures.
Architecture, research notes, and the roadmap live in
docs/ARCHITECTURE.md and
docs/notes/. The pre-rewrite xng (a dumphfdl session
wrapper) is preserved in legacy/.
Dual-licensed under MIT or Apache-2.0, at
your option. Decode cores are implemented clean-room from public standards
(ICAO, ARINC, ITU-R, ETSI) or ported from permissively licensed projects
(libacars, JAERO, iridium-toolkit — MIT/BSD) with attribution; GPL
projects are used as fact references only, and the full sourcing record
is in docs/REFERENCES.md plus per-crate
PROVENANCE.md files.