1 unstable release
Uses new Rust 2024
| new 0.1.0 | May 11, 2026 |
|---|
#522 in Encoding
350KB
3.5K
SLoC
sdr-acars
A Rust ACARS (Aircraft Communications Addressing and Reporting System)
decoder — MSK demodulation, frame parsing with parity FEC, multi-block
reassembly, acarsdec-compatible JSON, and a CLI. A faithful port of
Thierry Leconte's C acarsdec: pure DSP + parsing, no SDR-driver or
UI dependency, so it drops into any Rust radio pipeline that can hand
it samples.
CLI
$ cargo install sdr-acars
$ sdr-acars-cli capture.wav # N-channel WAV at 12.5 kHz IF, one ACARS channel per WAV channel
$ sdr-acars-cli --iq rec.cs16 \ # raw interleaved-i16 complex IQ ...
--rate 2500000 --center 130337500 \
--channels 131.550,131.525,130.450,130.425,130.025,129.125
The WAV path expects audio already AM-demodulated and decimated to the
12.5 kHz IF rate (one channel per WAV channel) — the same input
acarsdec's file mode takes. The --iq path takes a wideband complex
recording and does the channelization itself. Output matches
acarsdec's -o 2 plain-text format.
Library
Two entry points depending on what you can feed it:
Wideband IQ → multi-channel decode
use num_complex::Complex32;
use sdr_acars::ChannelBank;
# fn read_iq_block() -> Vec<Complex32> { Vec::new() }
# fn main() -> Result<(), sdr_acars::AcarsError> {
// US VHF ACARS cluster — fits inside a 2.5 MHz Nyquist window
// centered on the midpoint of the channel extremes (130.3375 MHz).
const US_ACARS: &[f64] = &[
129_125_000.0, 130_025_000.0, 130_425_000.0,
130_450_000.0, 131_525_000.0, 131_550_000.0,
];
let mut bank = ChannelBank::new(2_500_000.0, 130_337_500.0, US_ACARS)?;
loop {
let iq: Vec<Complex32> = read_iq_block();
if iq.is_empty() { break; }
bank.process(&iq, |msg| {
let label = String::from_utf8_lossy(&msg.label);
println!("{} {label} {}", msg.aircraft, msg.text);
});
}
# Ok(())
# }
Pre-decimated 12.5 kHz IF audio → single-channel decode
Drive MskDemod + FrameParser directly — that's what the CLI's
WAV path does, one pair per WAV channel. See src/bin/sdr-acars-cli.rs.
JSON output
serialize_acars_json(&msg, station_id) produces an acarsdec-shaped
JSON object (the output.c::buildjson schema) plus a
reassembled_blocks extension field. Pure data → string — the caller
owns the I/O (write JSONL, feed a UDP socket, …):
# use sdr_acars::{AcarsMessage, serialize_acars_json};
# fn demo(msg: &AcarsMessage) {
let line = serialize_acars_json(msg, Some("MYSTATION"));
println!("{line}"); // {"timestamp":..., "label":"...", ..., "app":{"name":"sdr-acars","ver":"0.1.0"}}
# }
Cargo features
cli(default) — builds thesdr-acars-clibinary; pulls inclap,tracing-subscriber, andhound. Library-only consumers build withdefault-features = falseand skip all three.
Correctness
tests/e2e_acarsdec_compat.rs runs sdr-acars-cli on the test.wav
vendored from acarsdec and diffs the output (volatile fields
stripped) against a committed snapshot of the C tool's output on the
same input. This is the decoder's primary correctness oracle — see
tests/fixtures/REGENERATE.md for how the snapshot is refreshed.
Minimum supported Rust version
1.95. Bumping the MSRV is a minor-version change.
License
LGPL-2.0-only — the same license as upstream acarsdec, whose C
source (the MSK demod, frame decoder + FEC, syndrome table,
channelizer, label parsers, JSON schema, and text printer) is
transcribed directly into this crate. See LICENSE for the
full text and NOTICE for attribution.
An LGPL-2.0 library can be linked from MIT / BSD / Apache / proprietary programs — the LGPL'd parts (this crate) just have to stay LGPL and remain replaceable, per the LGPL's linking terms.
Dependencies
~5–9MB
~91K SLoC