A Python encoder and decoder for Inmarsat Classic Aero baseband WAV audio,
built in the style of the other codec skills in this workspace (inmarsat-c,
acars, sstv, ...). ACARS message goes in, Classic Aero WAV comes out; feed
the WAV back through the decoder and you get the ACARS message.
Classic Aero is the Inmarsat aviation satellite data link that carries ACARS traffic to/from aircraft on long-haul oceanic, polar, and trans-continental flights, using A-BPSK and A-QPSK modulation at 600/1200/10500 bps on geostationary L-band satellites. It is also the only open-hobbyist path to seeing satellite ACARS in the wild (most other systems are either proprietary or SBB, which is a different physical layer).
Classic Aero defines four logical channel types on the satellite link:
| Channel | Direction | Use |
|---|---|---|
| P | Ground -> Air, continuous TDM | Signalling, broadcast ACARS, ATS data link |
| R | Air -> Ground, slotted-Aloha burst | Short uplink signals, log-on requests |
| T | Air -> Ground, reservation-TDMA burst | Longer uplink messages by priority |
| C | Both ways, SCPC voice | Cockpit telephone calls (AMBE-coded) |
This skill implements:
| Rate | Channel | Modulation | Status |
|---|---|---|---|
| 600 bps | P, R, T | A-BPSK (MSK-equivalent) | Encode + decode |
| 1200 bps | P, R, T | A-BPSK (MSK-equivalent) | Encode + decode |
| 10500 bps | P, R, T | A-QPSK (Offset QPSK) | Encode + decode |
| 8400/21000 bps | C | A-QPSK voice (AMBE) | Out of scope (proprietary vocoder) |
All nine P/R/T × 600/1200/10500 combinations round-trip cleanly through WAV
(see scripts/jaero_test.py). The C-channel voice service uses the
proprietary AMBE vocoder, for which no open-source codec is
redistributable — it is out of scope permanently.
The encoder:
- Builds an ACARS/ARINC 618 byte sequence with odd-parity, SOH, STX, ETX, CRC-16, and DEL markers.
- Chops it into Signal Units (SUs): one Initial SU (type 0x71) carrying 2 bytes of ACARS, followed by Subsequent SUs (0xC0-bit set) each carrying 8 bytes.
- Packs the 6 or 26 SUs into a P-channel frame, scrambles the raw bits with a 15-stage LFSR, applies rate 1/2 convolutional FEC (K=7), interleaves the coded bits in 64 × N blocks (N = 6, 9, or 78), wraps with a 16-bit header and a 32-bit unique word tail, and modulates the channel bit stream as MSK-equivalent A-BPSK (600/1200 bps) or Offset QPSK (10500 bps) onto real audio.
- Writes 16-bit PCM mono WAV at 48 kHz.
The decoder does the reverse: reads the WAV, demodulates to bits, hunts for the 32-bit unique word, extracts each frame, deinterleaves, Viterbi-decodes, descrambles, parses the 12-byte SUs, checks each SU's CRC-16, reassembles the ACARS userdata, verifies odd parity and the ACARS CRC, and emits the fields.
Each R-channel burst carries exactly one extended SU (19 octets = 152 bits, with the last 16 bits being the CRC). The interleaver block is 64 × 5 = 320 channel bits, which decodes through FEC to 160 raw bits (152 SU + 8 tail).
Each T-channel burst carries a 48-bit T-header (4 payload octets + 16-bit CRC) followed by N × 96-bit standard SUs and a 112-bit flush tail. The interleaver block is 64 × (3N + 5) channel bits. The encoder takes an arbitrary SU count and packs the SUs; the decoder sweeps 1..31 SUs and picks the SU count whose header CRC and per-SU CRCs all validate.
TX: LINK LAYER -> SCRAMBLER -> FEC ENCODER -> INTERLEAVER -> MODULATOR
RX: LINK LAYER <- DESCRAMBLER <- FEC DECODER <- DE-INTERLEAVER <- DEMODULATOR
The scramble-before-FEC ordering is Classic Aero's convention, inherited directly from the Inmarsat SDM. It differs from some textbook modems that interleave between FEC and modulation with scrambling applied last — match this order exactly or the receiver will see noise.
| Parameter | Value | Source |
|---|---|---|
| Unique word (MSK) | 0xE156AE93 (32 bits) | aerol.cpp:947 |
| Convolutional polys | G1=0o171, G2=0o133 (K=7, rate 1/2) | jconvolutionalcodec.cpp, aerol.cpp:937 |
| Scrambler polynomial | x^15 + x^14 + 1 | aerol.h:AeroLScrambler |
| Scrambler init state | {1,1,0,1,0,0,1,0,1,0,1,1,0,0,1} | aerol.h:AeroLScrambler |
| Interleaver dims | 64 rows × N cols, row perm (i*27) mod 64 | aerol.cpp:525-536 |
| P-channel N per rate | 6 / 9 / 78 at 600 / 1200 / 10500 bps | aerol.cpp:setSettings |
| R-channel block | 64 × 5 cols = 320 coded bits | aerol.h:RTChannelDeleaveFECScram |
| T-channel block | 64 × (3N + 5) cols for N SUs | aerol.h:RTChannelDeleaveFECScram |
| CRC-16 | poly 0x1021 reflected, init 0xFFFF, final NOT | aerol.h:CrcClass |
| Standard SU | 96 bits (12 octets), CRC at [10-11] LE | aerol.cpp:ISUData::update |
| Extended SU (R) | 152 bits (19 octets), CRC at [17-18] LE | aerol.cpp:RISUData::update |
| A-QPSK defaults | Fs=48000, center=8000, RRC α=1.0 | oqpskdemodulator.cpp |
Requires Python 3.9+ and NumPy:
pip install numpyscipy is optional. The pipeline is pure-NumPy.
python3 scripts/jaero_encode.py out.wav --rate 1200 --channel P \
--tail N12345 --label H1 --block-id A \
--text "POS N51.5 W010.2 FL360 M0.82"python3 scripts/jaero_encode.py out.wav --rate 1200 --channel R \
--r-msg-type 0x22 --r-user-data "POS_REQ"python3 scripts/jaero_encode.py out.wav --rate 1200 --channel T \
--tail N54321 --label H1 --text "TDMA BURST TEST"python3 scripts/jaero_encode.py out.wav --rate 10500 --channel P \
--tail N88888 --label H1 --text "FAST LINK 10500"python3 scripts/jaero_decode.py out.wav --rate 1200 --channel P
python3 scripts/jaero_decode.py out.wav --rate 1200 --channel R
python3 scripts/jaero_decode.py out.wav --rate 1200 --channel T
python3 scripts/jaero_decode.py out.wav --rate 10500 --channel PAdd --json for structured output and --verbose for per-SU detail.
python3 scripts/jaero_test.py
# Results: 56/56 PASSEDTests cover: CRC, Viterbi (clean and under bit errors), scrambler self- inverse with frozen vector, interleaver roundtrip at N ∈ {6, 9, 78}, UW correlator, RRC filter properties, A-BPSK mod/demod roundtrip at 600/1200, A-QPSK mod/demod roundtrip at 10500, SU build/parse, ACARS userdata chop and reassembly, full WAV roundtrip at all three P-channel rates, AWGN robustness at 6 dB and 10 dB SNR, R-channel bit and WAV roundtrip at all three rates, T-channel bit and WAV roundtrip with CRC-verified header and per-SU checks at all three rates.
- ICAO Doc 9925 "Manual on the Aeronautical Mobile Satellite (Route) Service" — authoritative source for channel rates, modulation, frame sizes, SU sizes. Part III Chapter 3 is the operative section. Available for purchase from ICAO at https://store.icao.int. Not redistributed in this repo (copyrighted).
github.com/jontio/JAERO— the reference C++ implementation used to verify scrambler, interleaver, UW, FEC, CRC, and R/T burst layout.samples/— reference WAVs (one per rate × channel pair; nine files).scripts/jaero_common.py— all DSP primitives, FEC/scrambler/interleaver, SU framing, ACARS layer, and both A-BPSK (MSK) and A-QPSK (OQPSK) modulators/demodulators; P / R / T encode + decode.scripts/jaero_encode.py,scripts/jaero_decode.py— thin CLI glue over the common module.scripts/jaero_test.py— self-contained test suite (56 tests).
- Header fields in the T-channel 6-octet header are placeholder content (AES ID + SU count + flags). The exact bit layout of the real Inmarsat T-header is in the Inmarsat SDM rather than Doc 9925, so JAERO itself interprets only the SU-count-plus-CRC portion. Our header round-trips through our own decoder but may not match a real Aero downlink byte-for- byte.
- P-channel messages are limited to one frame per message for now. The ACARS userdata must fit in 6 SUs at 600/1200 bps (2 + 5*8 = 42 bytes of ACARS) or 26 SUs at 10500 bps. Multi-frame span (ISU in one frame, SSUs in subsequent frames) is tracked as a future enhancement.
- JAERO interop has not been exercised end-to-end yet. The bit-level constants (scrambler, FEC, interleaver, UW, CRC, SU layout) match JAERO's source exactly, so demodulation in JAERO is expected to work; manual verification is the last outstanding acceptance gate.
- C-channel voice is out of scope, permanently. AMBE vocoder is not redistributable.
- Phase-ambiguity resolution on the OQPSK decoder is primitive. The internal round-trip works because transmit and receive agree on phase, but a JAERO-produced OQPSK WAV may need the 4-way phase rotation search that JAERO's decoder performs — easy to add if interop requires it.