Traffic stack: TPEG/TEC, RDS-TMC, DAB packet-mode and announcement FIGs - #49
Supermagnum wants to merge 15 commits into
Conversation
Shared decode + GeoJSON output (bearer dab-tpeg / fm-rds-tmc) so dabradio and fmradio stay focused on bearer FIGs/RDS while traffic events share one shape. TPEG transport follows ISO/TS 21219-5; TEC maps to features; RDS-TMC ALERT-C includes CA/encryption-admin detection without decrypting LTN=0. Co-authored-by: Cursor <cursoragent@cursor.com>
Add FIG 0/3 (packet components), FIG 0/2 tmid==3, FIG 0/8, and FIG 0/13 (UAtype) parsing plus MSC packet reassembly with --dump-fic, --dump-packets, and --traffic. Validated on the NRK RIKS 12D capture: packet CRC pass rate 91.5% (681/744) on the SPI/EPG component; FIG 0/13 shows UAtype 0x007 (EPG) only — no 0x004 TPEG — matching welle.io on the same IQ. Co-authored-by: Cursor <cursoragent@cursor.com>
Parse announcement support (0/18) and switching (0/19), emit AnnouncementEvent JSON (bearer dab-announcement) via --announcements — separate from TPEG GeoJSON. Cross-checked against welle.io on the Innland 13E capture (EID 0xF501): both agree FIG 0/18 and 0/19 are absent in-FIC. A ~40-minute live welle watch on 13E also saw ext18=0 throughout, so the empty result is missing on-air signalling, not a parse miss. Co-authored-by: Cursor <cursoragent@cursor.com>
Decode TMC AID (CD46/CD47) from group 3A, ALERT-C from the allocated group (typically 8A), and surface CA/encryption-admin / LTN=0 without attempting decryption. Adds --traffic and --location-tables; unit-tested ALERT-C paths (no Norwegian FM capture available at the receiver site). Co-authored-by: Cursor <cursoragent@cursor.com>
tools/fig018_live_watch.py tails welle-cli dump.fic for extension 18. Does not rewrite dump.fic under welle's open FD (that race inflated fibs_scanned ~48x while ext counters stayed real). IQ-files/ is gitignored — multi-GB captures stay local reference data, not repo history. Co-authored-by: Cursor <cursoragent@cursor.com>
The dabradio readme was accidentally replaced with the core desperado readme while restoring announcement sources from a shared backup path. Co-authored-by: Cursor <cursoragent@cursor.com>
Also place the FIG 0/18/0/19 readme section with the other traffic docs and list --announcements in the CLI reference. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
DAB does not send out traffic information any more here in Norway, that was closed "to save money " . |
xoolive
left a comment
There was a problem hiding this comment.
Thank you for your contribution! I will have to read more in depth, but here are already a few comments I have
…Riks IQ fixture, positive RDS-TMC docs, drop IQ-files gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
xoolive
left a comment
There was a problem hiding this comment.
Thanks for the substantial addition. I found several correctness issues that should be addressed before merge:
-
TPEG malformed-input panic:
crates/traffic/src/tpeg/transport.rs:131-172can accept an overflowing IntUnLoMB value and then evaluate uncheckedpos + len. A CRC-valid malformed payload can therefore panic (the release profile enables overflow checks). Please reject unrepresentable values before shifting and usechecked_addfor slice bounds. -
Unbounded FM memory use: decoded TMC features are always queued in
RdsParser, butmain.rsdrains them only with--traffic. A normal long-running receive of a TMC station will retain repeated features indefinitely. Always drain the queue; print only when the flag is enabled. -
Partial 3A ODA registration corrupts state: the new 3A path uses/persists blocks C and D even when they failed CRC. A partial group can overwrite a valid ODA registration or cause
handle_system_group(0)to mark TMC as encrypted. Require valid C and D blocks before updating ODA/TMC state, with a regression test. -
Unsupported traffic mode combinations:
--mono --traffictakes the mono path without RDS processing;--raw-out --trafficcontinues before RDS output. Either support these combinations or reject them explicitly. -
DG flag handling: a packet component whose FIG 0/3 has
DG flag=1(no_data_groups) still unconditionally goes throughapplication_payload()indabradio/src/main.rs. That strips a data-group header/CRC from raw application data. Use the raw group bytes for no-data-group components. -
Late packet FIGs are missed:
traffic_initializedis set once after generic ensemble completeness. Since this does not require FIG 0/3, 0/8, or 0/13, later packet/TPEG metadata is never picked up; file traffic mode may also exit early. Re-evaluate targets as packet FIG information arrives without resetting existing MSC handlers. -
Duplicate packet targets: FIG 0/2 deduplication includes the later-resolved mutable
subchannel_id, allowing the same(TMId=3, SCId)component to be inserted twice. Deduplicate/update by stable component identity.
Validation: cargo test -p traffic -p fmradio -p dabradio passes (184 passed, 5 ignored). FM replay decoded normal RDS successfully but contained no TMC ODA. A 300-frame RTL-SDR DAB replay decoded Métropolitain 2 (3,588 FIBs) but contains no packet/TPEG/announcement signalling, so it validates the normal DAB path only.
|
Agent found a few issues, but I think the biggest one is that I don't have any way to validate the code is actually meaningful on actual data. With my local recordings (in few European countries), none of them found data going through your implementation... |
…ses. Reject overflowing TPEG IntUnLoMB lengths, keep FM TMC queues drained, require full CRC on RDS 3A ODA updates, honor DG=1 payloads, re-arm late packet/TPEG FIGs without resetting MSC handlers, dedupe packet components by SCId, and reject unsupported --traffic flag combos—with regression tests for each. Co-authored-by: Cursor <cursoragent@cursor.com>
|
All 7 issues addressed, each with a regression test:
|
Re: validating against actual data — found something since the last update, and it partially closes this. Real off-air confirmation of genuine TPEG signaling. A raw IQ sample from a Belgian RTBF DAB ensemble (channel 12B), originally shared by another user on dab-cmdline issue #27 back in 2017 (hosted on Mega.nz, still downloading), lists a service named TPEG_PACKET. Running the full FIG 0/13 sweep against it — same procedure used for the RIKS/Innland "absent" findings, cross-checked against welle.io's dump.fic on the same file — confirms it: UAtype 0x004, SId 0xE0606361, resolved via FIG 0/3 to SubCh 14 / packet address 1 / 16 kbps. welle-cli and this implementation agree on all 12 services/labels in the ensemble. This is the first genuine UAtype 0x004 this project has seen on real broadcast data, as opposed to every capture so far (RIKS, Innland, your Métropolitain 2 replay) which validated the FIC/FIG pipeline correctly but happened to carry no TPEG. MSC/packet decode on this file is weak but real, and the reason is worth stating plainly. Initial packet CRC pass rate was 2.7% (later 3.4% after a couple of fixes below) — I want to be precise that this is not chance-level: chance for a 16-bit CRC is ~0.0015%, so ~2.7–3.4% is roughly 20–40x above noise. I initially suspected a DC-offset issue specific to this file's cu8/RTL-SDR origin (vs. the cf32/USRP-class hardware used for RIKS/Innland) and fixed a real centering bug in the cu8 path along the way (/128 → (b−127.5)/127.5), plus added a real NCO frequency-mix and switched MSC to viterbi_decode_state0. But direct spectrum measurement ruled out DC as the actual cause here (dc_excess_db ≈ 0.02, no spike). The real explanation: this specific file's own FIC quality peaks around ~80% CRC, versus ~99.9% on the clean captures — it's a decade-old RTL-SDR clip with whatever reception conditions existed at capture time, and that's a ceiling on the recording itself, not something the decoder can fix. MSC tracking below that (3–18% depending on component) is consistent with FIC's much stronger protection absorbing more of the same underlying degradation than MSC's thinner margin can. Why I think the partial result is itself meaningful, not just "not done yet": a broken or brittle decoder would show chance-level output regardless of input quality — that's exactly what the original gr-dab attempt looked like (~1 pass out of ~97,800, indistinguishable from noise). Getting a real, measurably-above-chance partial decode out of a genuinely degraded, ~9-year-old, low-SNR capture is evidence the implementation degrades gracefully with signal quality rather than failing outright — which is closer to what a robust real-world decoder needs to do than only succeeding on clean, modern captures. Combined with the ~91.5% (RIKS) and ~84% (Innland) pass rates already validated on clean signals, I'd frame this as: works well on good signal, still extracts real signal on bad signal, rather than reading the Belgian result in isolation as a failure. Test suite: cargo test -p traffic -p fmradio -p dabradio — 185 passed, 4 ignored. Full status trail for the Belgian capture is in IQ-files/be_12b_validation/be_12b_validation_summary.json plus the note in crates/dabradio/readme.md, alongside the RIKS/Innland trails from before. I know this doesn't fully close the "validate against real TPEG content end-to-end" question — the TPEG transport/TEC layer still hasn't been exercised against a cleanly-decoded real packet stream, since this file's ceiling doesn't get there. If you have a European recording of your own from a currently-active TPEG deployment with better SNR than this one, that would be the fastest way to finish that last piece — happy to run it through the same validation procedure. |
…oarse search. MSC EEP now traces from state 0 like FIC/welle; cu8 uses (b-127.5)/127.5; OFDM initial coarse accepts ±36 kHz so large RTL offsets lock without relying only on a digital mix. Adaptive DC for cu8/shift; document Belgian 12B TPEG FIC-ok / MSC-partial findings. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
A reproducible positive-signal capture is now available: GQRX wideband Belgian DAB capture. Format: zstd-compressed GQRX
SHA-256: The file is intentionally shared outside Git because it is about 695 MiB compressed. It can be replayed directly after decompression, for example: zstd -dc capture.raw.zst | dabradio - --channel 12A --format cf32 \
--sample-rate 16000000 --center-freq 220936000 --no-audio --dump-fic --dump-packets --traffic |
OK, I can run that trough my code if you want that done ? |
|
Ok actually if you rebase on main you can read directly the .zst file (no need to pipe it) If you want to try? |
I will look at it tomorrow, but it depends on how much tokens I have left this most month. |
|
No rush, I am not in a hurry to merge, at least not until there's evidence some new information appears |
|
GQRX Belgian capture is validated. The missing piece was packet-mode FEC (addr 1022 / RS(204,188)). Results Code zstd -dc gqrx_….raw.zst | dabradio - --channel 12B --format cf32 |
GQRX 12B EPG groups now reassemble; 12A TPEG FIC/announcements stay positive while that clip's TPEG subchannel is padding-only. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
crates/trafficas a shared application layer for TPEG/TEC and RDS-TMC GeoJSON (dab-tpeg/fm-rds-tmc), including ALERT-C with CA/encryption-admin detection without decrypting LTN=0.--dump-fic,--dump-packets,--traffic). Validated on NRK RIKS 12D: packet CRC pass rate 91.5% (681/744) on SPI/EPG; FIG 0/13 shows UAtype 0x007 (EPG) only, no 0x004 TPEG — matching welle.io on the same IQ.--announcements,AnnouncementEventJSON). Cross-checked against welle.io on Innland 13E (EID 0xF501): both agree 0/18 and 0/19 are absent in-FIC; a ~40-minute live watch also saw ext18=0.--traffic,--location-tables).tools/fig018_live_watch.pyfor live FIG 0/18 monitoring against welle-cli dump.fic, and gitignoreIQ-files/so large captures stay local.Test plan
cargo test -p trafficcargo test -p fmradio(ALERT-C unit paths)cargo check -p dabradio -p fmradio -p traffictools/fig018_live_watch.pytails dump.fic without rewriting under welle's open FDMade with Cursor