Decoder for 1st (FGB) and 2nd (SGB) generation COSPAS-SARSAT emergency beacons at 406 MHz.
Supports COSPAS-SARSAT 406 MHz FGB/1G and SGB/2G beacon decoding from IQ recordings and SDR receivers.
Validated on real signals, not only on synthetic test vectors. The decoder has been exercised on direct 406 MHz reception, CNES system/calibration beacons, and the 1544 MHz MEOSAR downlink. An independent validation at the F4KLO La Villette radiotelescope decoded real SGB traffic received with an Airspy at 2.5 Msps and located the Larnaca calibration beacon within a few metres. Across two F4KLO captures, 23 SGB messages from seven system or calibration beacons in four countries on four continents were decoded with a clean BCH result.
F4KLO field report · Real SGB IQ recordings and SigMF metadata · Validation details · French README
Reproduce the complete receive chain on the published real IQ recording:
make demo-real-sgbThis downloads the sample, verifies its SHA-256, then performs acquisition, despreading, BCH validation and field decoding. See the real-signal demonstration notes.
The orbitography implementation also decodes the unprotected UTC calibration time defined by C/S T.006 instead of incorrectly treating those national-use bits as BCH-2.
Branch: main
Repository renamed to dec406; links using the former
Decode_sarsat_406_v1g_v2g name continue to redirect here.
- 1G (FGB): T.001 biphase-L PSK ±1.1 rad, 400 bps, all Location/User
protocols —
dec406_v1g.c - 2G (SGB): BCH(250,202) with full Berlekamp-Massey + Chien error
correction, all T.018 fields —
dec406_v2g.c
- DSSS OQPSK (2G) —
dsss_demod.cdirect processing chain (no sample-rate tracking loop): DC blocker → FFT-correlation frequency acquisition → NCO wipeoff → OQPSK delay → multi-offset boxcar decimation → despread with preamble linear-fit frequency estimation + per-bit Costas PLL → BCH. Oracle tries 4 boxcar offsets × 4 Costas phases (16 combos) before giving up. The acquisition lag search is capped to the burst pre-roll to avoid late noise/data peaks beating the true preamble. The FFT-correlation frequency search tries ±8 kHz first, then widens to ±16 kHz only when needed to cover larger frequency offsets observed on real CNES SGB bursts. - FGB IQ-direct (1G) —
fgb_iq_demod.c: complex baseband BPSK biphase-L decoder without FM-demod → audio detour. Dual-grid CW end detection, multi-phase Costas search (4 initial phases × 13 offsets), Manchester slicer, BCH-1 correction (t=3), and BCH-2 correction for long non-orbitography frames (t=2).
dec406_scan is a unified real-time scanner with automatic SDR backend
selection for interactive use (Airspy Mini, RTL-SDR, PlutoSDR, HackRF).
The RTL-SDR backend uses rtlsdr_read_async() with large buffers. The scanner
runs a spectral burst detector over the 100 kHz band, classifies each burst as
FGB or SGB by bandwidth, and queues decoding in a dedicated worker so capture
and spectral detection continue during expensive decodes. Heartbeats report
SDR ring overruns and decode queue drops separately. For services, the backend
can be forced explicitly with
DEC406_BACKEND=rtl|airspy|pluto|hackrf so there is no startup probing.
Rates depend strongly on propagation, time of day, local noise, and which CNES system beacons are active. Track three SGB buckets separately:
| Metric | Definition |
|---|---|
| SGB acquisition/sync | (BCH OK + FRAME REJECTED) / detected SGB bursts |
| SGB decoder purity | BCH OK / (BCH OK + FRAME REJECTED) |
| SGB end-to-end | BCH OK / detected SGB bursts |
Recent field validation shows that SGB bursts reaching synchronization validate BCH cleanly. Firmin relay checks and local RTL/Yagi tests remain in a high-success range, with FGB staying in its usual class. Treat these as run-specific field checks, not fixed global rates.
make build/dec406_iq # SGB decoder from IQ file
make build/dec406_scan # Real-time scanner (needs librtlsdr-dev)
make # all targetsBinaries produced in build/:
| Binary | Purpose |
|---|---|
dec406_iq |
SGB DSSS/OQPSK demodulator from IQ file |
dec406_fgb_iq |
FGB demodulator from IQ file, the 1G counterpart of dec406_iq |
dec406_hex |
1G/2G decoder from hex string |
dec406_audio |
1G decoder from WAV file (legacy FM-demod pipeline) |
dec406_scan |
Real-time FGB+SGB band scanner (Airspy/RTL-SDR/PlutoSDR/HackRF) |
dec406_ring |
Triggered IQ recorder — detects bursts, writes SigMF, no decoding |
dec406_scan_rtlsdr |
Legacy standalone scanner, RTL-SDR only |
dec406_scan_airspy |
Legacy standalone scanner, Airspy only |
dec406_dsss_test |
DSSS demodulator unit test driver |
generate_2g_hex |
2G test frame generator |
reset_usb |
USB device reset utility |
# SGB from IQ file (float32 complex, default)
./build/dec406_iq signal.iq -s 2457600
# SDRangel ci32_le recording (int32 complex little-endian)
./build/dec406_iq recording.sigmf-data -s 2457600 -I
# RTL-SDR uint8 recording
./build/dec406_iq recording.iq -s 2457600 -uInput formats: float32 complex (default), -u RTL-SDR uint8, -i int16
interleaved, -I int32 SDRangel ci32_le.
./build/dec406_hex 09C4745638D95999A02B33326C3EC4400003FFF00C02832000002B774C24FE4
./build/dec406_hex 8E3301E2402B002BBA863609670908./build/dec406_scan 406.0M 406.1M # AGC, ppm=0
./build/dec406_scan 406.0M 406.1M 30 0 # fixed gain 30 dB, ppm=0
./build/dec406_scan 406.0M 406.1M -1 30 # AGC, ppm=30Arguments are positional: <freq_start> <freq_end> [gain] [extra]. A
negative gain selects the automatic mode, so pass -1 explicitly when
you need to reach the fourth argument. extra means ppm correction on
RTL-SDR, bias-tee on Airspy, and amplifier on HackRF; the startup banner
labels it accordingly.
The scanner reads samples at 2.4576 Msps, detects bursts on a power
spectrogram, classifies them by bandwidth (BW_SPLIT_HZ = 20 kHz; wider
bursts are SGB), and runs the appropriate decoder. On RTL-SDR it resets the
USB device at startup, then captures continuously through the asynchronous
librtlsdr API.
Set RTL_DIAG=1 to log effective RTL throughput every few seconds:
RTL_DIAG=1 ./build/dec406_scan 406.0M 406.1MUseful SGB diagnostics:
DSSS_DIAG=1 ./build/dec406_scan 406.0M 406.1M
DUMP_FAIL=1 ./build/dec406_scan 406.0M 406.1M
make build/sgb_epl_diagDUMP_FAIL=1 writes failed SGB burst windows as burst_sgb_*.cf32 for
offline replay. build/sgb_epl_diag probes those dumps with EPL
correlators at wider residual-frequency ranges. ACQ_BANDPASS_HZ is kept as
a diagnostic acquisition-only filter; default 0 leaves it disabled.
systemd/scan1544.service is the dedicated downlink unit. It keeps the
backend explicit and switches the alert path to geographic filtering instead
of the 406 MHz distress-channel whitelist:
Environment=DEC406_BACKEND=hackrf
Environment=DEC406_ALERT_MODE=downlink
Environment=DEC406_ALERT_CENTER=43.0,1.5
Environment=DEC406_ALERT_RADIUS_KM=80If Pluto is used instead of HackRF on a given station, only
DEC406_BACKEND changes. In downlink mode, an email is sent only when the
decoded beacon position is valid and inside the configured radius.
[Unit]
Description=Scan406 Service
After=network.target
[Service]
Type=simple
SyslogIdentifier=scan406
WorkingDirectory=/home/firmin/balise_406MHz/dec406_scan/
ExecStart=/home/firmin/balise_406MHz/dec406_scan/build/dec406_scan 406.0M 406.1M
Restart=always
RestartSec=25
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetStderr is tagged with systemd journal priority prefixes:
journalctl -u scan406 -p info # clean trace (decoded frames only)
journalctl -u scan406 -p debug # full diagnostic streamIf a data/config_mail.txt is present, the scanner emails the decoded
frame whenever a beacon decodes on a T.012 Table H.2 distress channel
(B/C/D/F/G/J/K/N/O/R/S, ±2 kHz). The sendemail binary must be installed
— it is a Perl SMTP client, unrelated to the system sendmail.
A genuine alert also requires the frame to be a real distress message and the beacon to be heard twice. Test beacons, orbitography, bench tests and one-shot transmissions never raise one. To exercise the whole alert path anyway:
DEC406_ALERT_TEST=1 ./build/dec406_scan 406.0M 406.1M # all beacons
DEC406_ALERT_TEST=2 ./build/dec406_scan 406.0M 406.1M # SGB onlyWith value 1 every decoded beacon mails; value 2 restricts to SGB only. All
alerts raised this way carry a [TEST] subject prefix and a banner in the
body, so they cannot be mistaken for a real one. Leave the variable unset
in production.
At startup the banner prints the mail settings that matter for operation:
alerts : enabled
mail smtp : smtp.gmail.com:587
mail user : xxxx@gmail.com
mail to : a@b.fr,c@d.fr
To enable alerts, copy the template and fill in the four values:
cp data/config_mail.txt.example data/config_mail.txt
$EDITOR data/config_mail.txt # set your SMTP creds + recipients
chmod 600 data/config_mail.txt # contains an app passwordFormat (key=value, one per line):
smtp_serveur=smtp.gmail.com:587
utilisateur=user@example.org
password=app_password
destinataires=a@b.com,c@d.fr
Without that file the scanner runs normally; the banner prints
alerts : disabled and no email is sent.
Channel A (406.022 — orbitography/calibration) is excluded by design. Additional silencing filters layered on the channel whitelist:
- FGB beacons identified as Orbitography or with
ID-NOT-AVAIL(factory-fresh / bench tests) never trigger. - SGB test transmissions (T.018 §3 bit 43 = 1) never trigger.
- A second sighting of the same Hex ID within 3 min is required before mailing (filters one-shot bench-test bursts; a real distress beacon repeats every ~50 s and is confirmed at the second burst).
IQ @ 2.4576 MHz
→ DC blocker (IIR α=0.001)
→ boxcar decimation to chip rate (acquisition only)
→ freq_acq_fft_corr (chip-rate FFT-correlation, ±8 kHz then ±16 kHz fallback, ~1 Hz precision)
→ NCO wipeoff at sample rate
→ OQPSK delay (Q advanced by SPS/2)
→ multi-offset boxcar decimation (4 sub-chip offsets)
→ despread (preamble linear-fit freq/phase estimation,
per-bit BPSK Costas PLL over 256 chips)
→ 250 bits → bch_decode_250_202 (nerr) → decode_beacon
For each acquired burst the chain tries 4 boxcar offsets × 4 Costas phases (16 combos) against BCH, keeping the first that decodes cleanly. Preamble linear regression initialises carrier frequency/phase, replacing slow PLL convergence. A frame whose codeword BCH cannot correct is rejected.
IQ
→ multi-stage moving-average decimation to ~9.6 kHz
→ CW preamble carrier-frequency estimate (sample-level phase diffs)
→ frequency wipeoff
→ dual-grid CW end detection (two grids offset half/2)
→ multi-phase Costas search (4 phases × 13 offsets)
→ Manchester slicer (biphase-L ±1.1 rad)
→ BCH1 brute-force correction (t=3, bits 24..105)
→ 144 bits → BCH-1/BCH-2 validation + polarity fallback
No FM-demod, no audio detour, no biphase-L codec dependency.
src/ C sources (dsss_demod, despread, freq_acq, fgb_iq_demod,
dec406_v1g, dec406_v2g, main_iq, main_scan, scan_alert, ...)
include/ Headers
build/ Compiled binaries
tests/ SGB codec unit tests, BCH reject tests
utils/ Offline diagnostic tools
scripts/ Analysis / debug scripts
docs/ Specifications, deployment notes, SGB status notes
data/ Runtime data (config_mail.txt, etc.)
CHANGELOG.md— Version historyCLAUDE.md— Project conventions, debug methodologydocs/ARCHITECTURE_dec406.md— Detailed architecture (French)docs/TESTS_VALIDATION.md— Validation proceduresscripts/scan406.pl— Legacy Perl scanner (superseded by dec406_scan)
This project is licensed under the MIT License. See LICENSE.
- C/S T.001 — 1st Generation Beacon Specification (FGB)
- C/S T.012 — Beacon Type Approval (channel list, Table H.2)
- C/S T.018 — 2nd Generation Beacon Specification (SGB)
gr-cospas/gr-satellites— GNU Radio receiverssgb-codec(jbirby) — Python reference SGB codecsarsat_sgb(ADALM-PLUTO) — SGB modulator for PlutoSDRscan406.pl(F4EHY 2020) — Perl scanner ancestor, ported to C indec406_scan+scan_alert
