tdma example: burst-level bandwidth TDMA (narrowband <-> wide)#224
Merged
Conversation
A worked demonstration of what the cheap FastSetBandwidth switch (#223) unlocks: a transmitter alternates BURSTS between a robust narrowband width (critical frames — the ~6 dB link-budget win) and a wide width (bulk frames), flipping bandwidth per-burst. It is burst-level, not per-frame, because narrowband is an ADC-clock-domain state (not a radiotap field) and a receiver decodes one width at a time; rate-UEP is layered on top (critical at a robust rate, bulk at a fast MCS, via the svctx radiotap mechanism). One binary, one adapter, one role (examples/tdma/, tdma.h is header-only): - role=tx run the schedule, inject class-tagged frames, emit a marker at each narrowband-burst start. - role=rx-sync switch bandwidth in lockstep. DEVOURER_TDMA_SYNC=wallclock (shared system_clock) or =marker (self-clock off the TX marker — no shared clock; anchor + coast on steady_clock + re-anchor). Switches guard_ms early to absorb switch latency. - role=rx-camp camp permanently on one width. Two of these (narrowband + wide) are the user's "1 TX + 2 RX" mode: an independent, always-listening robust narrowband link + a wide bulk link. There is no hardware time primitive to exploit (no host TSF read, no PPS/GPS hook), so lockstep rides the OS wall-clock or the TX marker — both shipped so they can be compared. Bench-validated (tests/tdma_demo.sh: TX 8812AU, RX 8822CU/8822BU, ch36, 10 MHz NB). Each receiver reports a per-band x per-class decode table; every scenario shows critical+marker under the narrowband band and bulk under the wide band with near-zero off-diagonal (only frames caught mid-switch): wallclock lockstep, marker lockstep (self-clocked), and the dual-RX split (each RX hears only its band). No library changes — additive example + one doc section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A newcomer-facing visual intro to the tdma example: an animated time-frequency view where the occupied width "breathes" between a thin narrowband burst (critical, robust rate) and the full 20 MHz block (bulk, fast rate), with a live readout of the current phase/width/payload. Same DEVOURER live-monitor style as the other rf-primer GIFs, generated by tools/tdma_schedule_gif.py (Pillow, seamless 3-period loop). Bumps the primer to nine animations and points "where to go next" at narrowband.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ck + switch cost The first cut showed only the breathing schedule; a newcomer couldn't see who the stations are, what keeps them aligned, or how fast the flip is. Redraw it as the full picture: a TX and an RX station (antennas, current width, sending vs decoding), a GPS shared 1-PPS clock that ticks both at once (dashed sync lines + a pulse that travels to both stations on each flip), a frame in flight coloured by class, the repeating schedule strip with a playhead, and a SWITCH COST gauge contrasting FastSetBandwidth's 0.2 ms against a 90 ms full retune (~450x) — the speed that makes the alternation practical. Smaller too (308 KB). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Windows/cl CI job failed: examples/tdma/main.cpp fell through to the <unistd.h> include branch, which MSVC lacks (and sleep/usleep with it). Replace the platform include block with a _MSC_VER-aware one (libusb.h on MSVC, as the other examples do) and swap sleep()/usleep() for portable std::this_thread::sleep_for. No behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A worked, runnable example of the burst-level bandwidth TDMA idea that
FastSetBandwidth(#223) unlocks: a transmitter alternates bursts between a robust narrowband width (5/10 MHz — the ~6 dB link-budget win, for critical frames) and a wide width (20/40 MHz — throughput, for bulk frames), flipping bandwidth per-burst in well under a millisecond to a few ms.It is burst-level, not per-frame, for two reasons that are physics, not laziness: narrowband is an ADC-clock-domain state (not a radiotap field), and a receiver decodes exactly one width at a time. So frames of one protection class are grouped into a burst, both ends switch, then the next burst. Per-frame rate-UEP is layered on top (critical also rides a robust rate, bulk a fast MCS — reusing the
svctxradiotap mechanism).The interesting hard part: sync
devourer exposes no hardware time primitive (no host TSF read, no PPS/GPS hook — only a per-frame read-only
tsfl). The example ships both receiver sync strategies so experimenters can compare:wallclock— both ends compute the phase fromsystem_clock(shared Unix-epoch anchor). Trivially aligned same-machine; cross-machine needs GPS/PPS/NTP-disciplined clocks (the caller's to provide).marker— self-clocking: the TX emits a marker frame at each narrowband-burst start; the RX camps narrowband until it decodes one, anchors its schedule to it, coasts onsteady_clock, and re-anchors on each subsequent marker. No external clock at all.A lockstep receiver switches
guard_msearly to absorb the switch-latency dead-window (the guard must exceed the chip's switch latency; bursts ≫ it).Roles (one binary, one adapter, one role)
DEVOURER_TDMA_ROLE=tx— run the schedule, inject class-tagged frames + markers.=rx-sync(DEVOURER_TDMA_SYNC=wallclock|marker) — switch in lockstep (the user's 1 TX + 1 RX mode).=rx-camp(DEVOURER_TDMA_CAMP=5|10|20) — camp on one width. Atx+ tworx-camp(narrowband + wide) is the user's 1 TX + 2 RX mode: an independent, always-listening robust narrowband link + a wide bulk link.Schedule knobs:
DEVOURER_TDMA_NB/_WIDE,_NB_MS/_WIDE_MS,_EPOCH_MS,_GUARD_MS,_CRIT_RATE/_BULK_RATE,_GAP_US. Metrics are JSONL on stdout ({"ev":"tdma.*"}); each RX prints a per-band × per-class decode table.Bench validation (
tests/tdma_demo.sh)TX 8812AU, RX 8822CU/8822BU, ch 36, 10 MHz narrowband. A correct run shows critical+marker under the narrowband band and bulk under the wide band, near-zero off-diagonal (only frames caught mid-switch):
Both sync modes lock and track; the dual-RX split is clean (each receiver hears only its band's bursts).
ctestgreen; no library changes — additiveexamples/tdma/+tests/tdma_demo.sh+ onedocs/narrowband.mdsection, so it can't regress the driver.🤖 Generated with Claude Code