iridium: decode real off-air wideband captures (noise seed + DDC + ITL)#127
Conversation
The full-band hunter detected real bursts but decoded 0 frames from a
6 MS/s off-air capture. Three isolated fixes, each unit-tested:
- Channelize each burst with xng_dsp::Ddc (two-stage windowed-sinc anti-
alias) instead of a boxcar-of-decim averager. The boxcar's sinc
sidelobes fold ~8 dB of wideband noise into the 250 kHz channel
(measured peak/noise 8.5 vs 16.6 dB on the same burst). Passband 50 kHz
one-sided so the demod's ±30 kHz tone-CFO search can recover off-center
detections.
- Seed the demod noise floor from the channel (decisive fix). The
asymmetric noise EMA starts at 1.0 and needs ~1400 quiet samples to
converge; an isolated extracted burst has only ~1000 pre-roll samples,
so the floor froze ~18 dB high and the acquisition gate (noise*8) sat
above the signal -> 0 energetic windows, UW fit never attempted. The
front end now estimates noise (20th-percentile power) and seeds the
demod. This alone took the capture 0 -> decoding (UW costs 0.003-0.04).
- Stop over-rejecting in the ECC/classify gates: ecc_blocks trusts a
weight-1 BCH correction even when the separate even-parity bit is also
flipped (unambiguous on this d=5 code; the parity flip is a harmless
second error), and classify accepts BCH-correctable RA headers, not
only zero-syndrome ones.
- Add the ITL ("TL", Time-Location) frame type, classified before IRA.
Its 96-bit `11`+0... header is a valid degenerate all-zero BCH
codeword, so without an explicit ITL check it fell through to the IRA
classifier and mis-decoded as an all-zero ring alert. The real captures
are ITL-dominated; they now report kind:"itl" (full PRS sat/plane
decode is TODO).
Result: the real capture decodes 2 ITL frames (was 0); all 283 workspace
tests pass, including new guards for ITL classification, IRA-still-works,
and the ecc_blocks parity tolerance. Adds XNG_IRIDIUM_DEBUG-gated
acquisition/detection diagnostics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughThis PR enhances the Iridium wideband (wave 2) decoder to handle real off-air bursts reliably. It introduces demodulator debug diagnostics and noise-floor seeding, adds a new ITL (Time-Location) frame type with refined classification heuristics, and replaces boxcar decimation with DDC-based downmix and robust noise estimation. ChangesWideband Demodulation and Frame Classification
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8280d2c035
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let diff = (data[0] == 0) as u32 | ||
| + (data[1] == 0) as u32 | ||
| + data[2..96].iter().map(|&b| b as u32).sum::<u32>(); | ||
| if diff <= 3 { | ||
| return FrameKind::Itl; |
There was a problem hiding this comment.
Keep ITL matching from preempting broadcasts
When the first 96 post-access bits are all zero, this early ITL check returns before the Bc branch below. That pattern is not just noise: the existing Bc predicate accepts a type-0 000000 HDR_POLY header followed by zero BCH blocks, so decode_bits now emits kind:"itl" for those broadcast frames instead of broadcast. Since the new guard only needs to stop near-zero headers from reaching the IRA classifier, run it after Bc/LCW or require the 11 prefix more strictly.
Useful? React with 👍 / 👎.
| let k = powers.len() / 5; | ||
| powers.select_nth_unstable_by(k, |a, b| a.total_cmp(b)); | ||
| powers[k] |
There was a problem hiding this comment.
Seed noise from the quiet margins, not the whole burst
For long bursts, the extracted segment is only PRE_S + POST_S (16 ms) of quiet around as much as MAX_BURST_S (92 ms) of signal, so the quiet samples can be less than 20% of chan. In that case powers.len() / 5 selects a burst-power sample rather than the noise floor; seed_noise() then sets the acquisition gate to about 8× the signal power and the demodulator can find zero energetic UW windows for the longest frames. Estimate from the known pre/post regions or use a lower percentile tied to the quiet fraction.
Useful? React with 👍 / 👎.
What
The wideband Iridium hunter detected real bursts in a 6 MS/s off-air capture (SAWbird+IR / Maxtena PN100) but decoded 0 frames. This makes it decode them, with three isolated, unit-tested fixes plus a missing frame type.
Root causes & fixes
Channelization — boxcar → DDC. Each detected burst was downmixed and decimated with a boxcar-of-decim averager, a poor anti-alias filter: its sinc sidelobes fold ~8 dB of wideband noise into the 250 kHz channel (measured peak/noise 8.5 dB vs 16.6 dB through a real FIR on the same burst). Now uses
xng_dsp::Ddc(the two-stage windowed-sinc the single-channel path already trusts), 50 kHz one-sided passband so the demod's ±30 kHz tone-CFO search can recover off-center detections.Seed the demod noise floor — the decisive fix. The demod's asymmetric noise EMA starts at 1.0 and needs ~1400 quiet samples to converge. A continuous stream gives it that; an isolated wideband-extracted burst has only ~1000 channel samples of pre-roll, so the floor froze ~18 dB too high when the burst arrived and the acquisition gate (
noise·8) sat above the signal → 0 energetic windows, UW fit never attempted. The front end now estimates channel noise (20th-percentile power) andseed_noise()s the demod. This alone took the capture 0 → decoding (clean UW costs 0.003–0.04).Stop over-rejecting in ECC/classify.
ecc_blocksnow trusts a weight-1 BCH correction even when the separate even-parity bit is also flipped (unambiguous on this d=5 code; the parity flip is a harmless second error), andclassifyaccepts BCH-correctable RA headers, not only zero-syndrome ones.Add the ITL ("TL", Time-Location) frame type, classified before IRA. Its 96-bit
11+0… header is a valid degenerate all-zero BCH codeword, so without an explicit ITL check it fell through to the IRA classifier and mis-decoded as a ring alert with an all-zero satellite/position. The real captures are ITL-dominated; they now reportkind:"itl"(full satellite/plane PRS decode via the toolkit'sitl.pytables is TODO).Result
itl_header_classifies_as_itl_not_ra,ira_header_still_classifies_as_ra,ecc_blocks_tolerates_flipped_parity_bit.XNG_IRIDIUM_DEBUG-gated acquisition/detection diagnostics for future field work.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Documentation