From 56e84d0bf35e27cf29dd67c9d72b00eb8bc7797e Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:34:54 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20current-state=20refactor=20=E2=80=94=20?= =?UTF-8?q?merge=20scheduled-MAC=20contracts,=20drop=20milestone=20framing?= =?UTF-8?q?,=20separate=20lab-rig=20facts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge the four "M0 contract" docs (dl-departure, beacon-grant, ack-txreport, ue-rx-attribution) into docs/scheduled-mac.md — one capability doc, four measured per-generation contracts; all measurements and verdicts kept verbatim. Repoint the test-script/API references. - Remove milestone/epic framing project-wide (M0..M6, "5G-NR RAN epic", "future work", "not yet"): forward-looking sections rephrased as current-state scope (8822e-quirks "Untested" -> "Coverage", adaptive-link-validation "Open questions" -> "What this validation does not cover", fhss/jammer-resilience honest-edges wording). - Separate lab-rig facts from general hardware behaviour: rig hostnames, BDF/IOMMU specifics and "on this rig/bench" phrasing generalized in CLAUDE.md, time-distribution, timing-accuracy, wfb-ng-tuning, bench-testing-near-field, tests/README (the local, untracked INVENTORY.md now carries the rig specifics). - Fill gaps vs recent PRs: CLAUDE.md gains the beacon/TSF/AP section (ReadTsf, StartBeacon/StopBeacon/UpdateBeaconPayload, PinBeaconTbtt, timesync/tdma demos) and the aggregation/ACK/TxReport section (SetAmpduMode, SetAckResponder, tx.report, GetRxQuality vs cell::UeRxAttribution), plus src/cell/ in the architecture map. - README: hardware table gets the missing 8812BU 5/10 MHz marker (and the 8821AU row loses a stray "AC"); binaries table gains tdma, timesync, doctor, pcieprobe; "Going deeper" restructured into themed groups covering every doc (previously 13 of 32 were unreachable). - Fix stale src/ paths in fused-fec.md (files live under src/jaguar1/); driver-primer now opens standalone. Verified: zero WIP/milestone/lab-phrasing sweep hits; every referenced path resolves; build + all 20 ctest selftests pass. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 46 +++++- README.md | 79 ++++++++-- docs/8822e-quirks.md | 21 +-- docs/ack-txreport.md | 64 -------- docs/adaptive-link-validation.md | 55 ++++--- docs/adaptive-link.md | 6 +- docs/aggregation.md | 5 +- docs/ap-mode.md | 10 +- docs/beacon-grant.md | 62 -------- docs/bench-testing-near-field.md | 2 +- docs/dl-departure.md | 65 -------- docs/driver-primer.md | 19 +-- docs/fhss.md | 12 +- docs/fused-fec.md | 4 +- docs/jammer-resilience.md | 11 +- docs/multi-ap-cellular.md | 7 +- docs/scheduled-mac.md | 255 +++++++++++++++++++++++++++++++ docs/time-distribution.md | 14 +- docs/timing-accuracy.md | 2 +- docs/ue-rx-attribution.md | 52 ------- docs/wfb-ng-tuning.md | 12 +- src/IRtlDevice.h | 2 +- tests/README.md | 2 +- tests/beacon_update_check.sh | 2 +- tests/dl_departure_matrix.sh | 2 +- 25 files changed, 462 insertions(+), 349 deletions(-) delete mode 100644 docs/ack-txreport.md delete mode 100644 docs/beacon-grant.md delete mode 100644 docs/dl-departure.md create mode 100644 docs/scheduled-mac.md delete mode 100644 docs/ue-rx-attribution.md diff --git a/CLAUDE.md b/CLAUDE.md index 97546247..3f432d78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,8 +78,8 @@ on rxdemo and txdemo (TX = the data/MGMT BD rings behind the unchanged bottom-up. Bind/restore: `tests/pcie_vfio_bind.sh` (driver_override, not new_id — the in-tree rtw88 auto-probe race). Validation: `sudo python3 -tests/pcie_rx_smoke.py` on the radxa-x4 (`ssh radxa-x4`, 8821CE at -0000:01:00.0, IOMMU group 12) — ambient beacons CRC-clean on ch 6 + 36. +tests/pcie_rx_smoke.py` against a vfio-bound 8821CE (rig specifics: local +`INVENTORY.md`) — ambient beacons CRC-clean on ch 6 + 36. ## Build @@ -153,7 +153,8 @@ traps it encodes: the in-tree rtw88 modules auto-probe (and fw-download into) every Realtek dongle at each enumeration — `modprobe -r` does NOT survive re-enumeration, temp-blacklist instead; and `authorized`-toggle "cold" leaves chip state (real VBUS cold via `REGRESS_VBUS_MAP` / -uhubctl — never on xhci root ports on this rig). +uhubctl — hub ports with per-port power switching only, not xhci root +ports). ## Logging @@ -334,6 +335,42 @@ per-bin energy + frame stats; `tests/sounding_sweep.sh` + `tests/sounding_map.py recover a coarse per-bin H(f) — down to 5 MHz bins on Jaguar3 (`docs/rx-spectrum-sensing.md`). +## Hardware time, beacons, AP mode + +`ReadTsf()` reads the 64-bit MAC TSF and every received frame carries the +MAC-latched `tsfl` RX timestamp — on all generations, µs-grade +(`docs/time-distribution.md`, measured vs NTP/PTP: `docs/timing-accuracy.md`). +`StartBeacon` loads a beacon into the MAC's reserved page and the chip +auto-transmits at each TBTT with the live TSF stamped at the TX instant — the +sub-µs downlink. The chip beacons **autonomously**: a session that ends +without a power-cycle must call `StopBeacon()` or the beacon keeps airing. +`UpdateBeaconPayload` swaps the airing content in place (frame-atomic on air, +TBTT-quantized latency); `PinBeaconTbtt` steers the TBTT to an absolute TSF +instant without corrupting the clock (Jaguar1: offset 0 only — its TBTT is +hardware-locked to the TSF grid); `AdjustBeaconTimingFine` is the µs-fine +manual lever. Demos: `timesync` (`DEVOURER_TSYNC_ROLE=master|slave|ue` + +`DEVOURER_TSYNC_*` knobs, incl. the PCIe master + I226-PTP discipline loop) +and `tdma` (TSF-slotted narrowband↔wide bursts on one channel). The beacon is +also the seed of AP mode — a real Linux station associates, open or WPA2-PSK +(`docs/ap-mode.md`); the multi-cell architecture it enables is +`docs/multi-ap-cellular.md`, and the measured scheduler contracts (submit→air +guard time, dynamic beacon grants, ACK/TxReport, per-UE RX attribution) are +`docs/scheduled-mac.md`. + +## Aggregation, hardware ACK, TX reports + +`SetAmpduMode` enables 802.11 A-MPDU on injected frames: ~+30% *goodput* at +the PHY ceiling by amortizing per-frame overhead — an occupancy metric can't +show it, count delivered payload. `SetAckResponder(mac)` arms the hardware +ACK/BlockAck responder; with a unicast TA on the soliciting frame this closes +a hardware-ARQ loop (autonomous MAC retransmission until ACK). Per-frame TX +outcomes surface as `tx.report` events (CCX via C2H) — the TX-side link +sensor; C2H rides the RX path, so J1/J2 TX-only sessions see none (run +`DEVOURER_TX_WITH_RX=thread`; J3's coex thread drains C2H regardless). +`GetRxQuality()` is the device-wide windowed RX sensor; +`cell::UeRxAttribution` is its per-transmitter (per-UE) counterpart. Docs: +`docs/aggregation.md`, measured per-generation matrix `docs/scheduled-mac.md`. + ## Architecture **The caller owns libusb.** `WiFiDriver::CreateRtlDevice` is intentionally @@ -367,6 +404,9 @@ Generation-agnostic core in `src/` (always compiled; depends on no HAL): - `PhyTableLoader` — runtime walker for Realtek's phydm-format register tables (`check_positive` + opcode state machine, without pulling in phydm itself). Shared by Jaguar1 + Jaguar2; Jaguar3 has its own `PhyTableLoaderJaguar3`. +- `cell/` — caller-side per-cell helpers built on the device API + (`UeRxAttribution`: per-transmitter windowed RX statistics keyed by 802.11 + TA); the device RX loops are untouched. Per-generation HALs (each self-contained): diff --git a/README.md b/README.md index 79d73c06..17d7189d 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,9 @@ Bandwidth cells are devourer's measured on-air TX throughput (Mbps, HT MCS7, | **RTL8812AU** | 2T2R | 56 | 52 | 52 | [CHANEVE CHW50L](https://www.aliexpress.com/item/4000762461362.html) (`0bda:8812`). 5/10 MHz capable | | **RTL8811AU** | 1T1R | — | — | — | 1T1R cut of 8812 silicon; rides the 8812 code path. Not benchmarked. 5/10 MHz capable | | **RTL8814AU** | 4T4R, 3-SS max | 65 | †(32) | †(32) | `0bda:8813`; tested on COMFAST CF-938AC and CF-960AC — antenna builds differ in realised [RX diversity](docs/measuring-spatial-diversity.md). 5/10 MHz capable | -| **RTL8821AU** | 1T1R AC + BT | 54 | 32 | 28 | TP-Link Archer T2U Plus (`2357:0120`) | +| **RTL8821AU** | 1T1R + BT | 54 | 32 | 28 | TP-Link Archer T2U Plus (`2357:0120`) | | **RTL8822BU** | 2T2R + BT | 52 | 50 | 49 | TP-Link Archer T3U (`2357:012d`). 5/10 MHz capable | -| **RTL8812BU** | 1T1R + BT | — | — | — | 1T1R cut of 8822B silicon; rides the 8822BU code path. Not benchmarked | +| **RTL8812BU** | 1T1R + BT | — | — | — | 1T1R cut of 8822B silicon; rides the 8822BU code path. Not benchmarked. 5/10 MHz capable | | **RTL8811CU** | 1T1R + BT | 36 | 29 | 28 | COMFAST CF-811AC (`0bda:c811`). 5/10 MHz capable | | **RTL8821CU** | 1T1R + BT | — | — | — | rides the 8811CU (8821C) code path. 5/10 MHz capable | | **RTL8812CU** | 2T2R | 65 | 60 | 60 | LB-LINK WDN1300H (`0bda:c812`). 5/10 MHz capable | @@ -137,7 +137,11 @@ the `env:` tags in [`src/DeviceConfig.h`](src/DeviceConfig.h). | `streamtx` / `duplex` | stdin-driven TX / full-duplex packet link | | `svctx` | per-video-layer rate ladders (unequal error protection) | | `txpower` | runtime TX-power API walkthrough | +| `tdma` | TSF-slotted burst TDMA (narrowband ↔ wide on one channel) | +| `timesync` | over-the-air clock distribution (master / slave / UE roles) | | `sense` | Wi-Fi motion sensing from beamforming reports | +| `doctor` | adapter-health triage → HEALTHY / SUSPECT / FAILING | +| `pcieprobe` | PCIe transport bring-up validation, layer by layer | | `precoder` | OFDM subcarrier shaping proof-of-concept | All chips compile in by default; per-chip CMake options (`DEVOURER_JAGUAR1`, @@ -183,30 +187,46 @@ one `IRtlDevice` interface covers all three generations. ## Going deeper +**Primers** — start here: + - [Visual RF primer](docs/rf-primer.md) — animated intro to the concepts behind everything below. - [Visual driver primer](docs/driver-primer.md) — animated intro to the chip and vendor-driver machinery: registers, efuse, firmware, MAC, PHY tables, calibration, coexistence. + +**Link engineering:** + - [Adaptive link](docs/adaptive-link.md) — the energy-minimizing video-link - controller design, and [its validation](docs/adaptive-link-validation.md). + controller design, [its validation](docs/adaptive-link-validation.md), and + the [building blocks](docs/adaptive-link-building-blocks.md): what each knob + (power, rate, bandwidth, hopping) measurably buys. - [Fused FEC](docs/fused-fec.md) — the cross-layer error-protection stack: per-layer PHY rates, corrupt-frame salvage, outer erasure code. +- [Aggregation & hardware ACK](docs/aggregation.md) — USB TX aggregation, + per-frame CCX TX-status reports, 802.11 A-MPDU (`SetAmpduMode`, +30% on-air + goodput), and the hardware ACK/BlockAck responder for reliable-unicast links. +- [wfb-ng tuning](docs/wfb-ng-tuning.md) — the most efficient wfb-ng + configuration, and the SDR-measured devourer-vs-wfb-ng TX comparison. + +**Spectrum agility:** + - [Frequency hopping](docs/frequency-hopping.md) — how per-packet hopping works and what it costs on each chip. +- [FHSS](docs/fhss.md) — the anti-jam design article: keyed SipHash hop + schedules, slot-locked lockstep RX, and + [jammer resilience](docs/jammer-resilience.md) — measured delivery against + parked and following jammers, and where a follower breaks. - [Narrowband](docs/narrowband.md) — 5/10 MHz channels across all three generations: the baseband re-clock, the per-chip register machinery, and the walls (RF re-latch edges, per-die clock coupling, the 5 MHz/5 GHz CFO limit). -- [Performance](docs/performance-tuning.md) — devourer vs. kernel driver on - startup time, on-air throughput, and host CPU (3–4× lower); the TX - submission modes and the tuning levers, with the methodology. -- [Adapter doctor](docs/adapter-doctor.md) — dying-dongle triage: EFUSE - read-stability, firmware-boot and RX-smoke probes with a - HEALTHY / SUSPECT / FAILING verdict. -- [Spectrum sensing](docs/rx-spectrum-sensing.md), - [spatial diversity](docs/measuring-spatial-diversity.md), - [bench testing near-field](docs/bench-testing-near-field.md) — measurement - guides for the built-in radio instrumentation. +- [Spectrum sensing](docs/rx-spectrum-sensing.md) — RX energy sweeps down to + 5 MHz bins: a coarse per-bin H(f) from the dongle itself. +- [Pseudo preamble puncturing](docs/pseudo-preamble-puncturing.md) — how close + per-tone RX masks/notches get to using a wide channel with a dirty slice. + +**Timing & coordination:** + - [Time distribution](docs/time-distribution.md) — LTE-eNB-style over-the-air clock distribution off the hardware beacon TSF: sub-µs downlink, TSF adoption, µs-fine TBTT steering and a converging closed-loop uplink timing advance. @@ -216,9 +236,36 @@ one `IRtlDevice` interface covers all three generations. - [AP mode](docs/ap-mode.md) — devourer as an infrastructure access point a real Linux station associates with: beacon → probe/auth/assoc → DHCP/ARP/ICMP → ping, open or WPA2-PSK (4-way handshake + software CCMP), validated against rtw88. -- [Aggregation & hardware ACK](docs/aggregation.md) — USB TX aggregation, - per-frame CCX TX-status reports, 802.11 A-MPDU (`SetAmpduMode`, +30% on-air - goodput), and the hardware ACK/BlockAck responder for reliable-unicast links. +- [Scheduled MAC](docs/scheduled-mac.md) — four measured contracts under a slot + scheduler: submit→air guard time, dynamic beacon grants, hardware ACK/TxReport, + per-UE RX attribution. +- [Multi-AP cellular](docs/multi-ap-cellular.md) — what the shared clock + enables: coordinated cells, make-before-break handover, roaming robot UEs. + +**Measurement & instrumentation:** + +- [LA-mode IQ capture](docs/la-capture.md) — raw complex baseband into the TX + packet buffer; per-tone H(k)/CSI offline, from the dongle alone. +- [Spatial diversity](docs/measuring-spatial-diversity.md), + [bench testing near-field](docs/bench-testing-near-field.md) — measurement + guides for the built-in radio instrumentation. +- [Beamforming self-sounding](docs/beamforming-self-sounding.md) — per-subcarrier + CSI from two adapters via the VHT sounding exchange; and its sibling + [victim sensing](docs/beamforming-victim-sensing.md) — motion sensing from + captured beamforming reports. +- [Adapter doctor](docs/adapter-doctor.md) — dying-dongle triage: EFUSE + read-stability, firmware-boot and RX-smoke probes with a + HEALTHY / SUSPECT / FAILING verdict. +- [Performance](docs/performance-tuning.md) — devourer vs. kernel driver on + startup time, on-air throughput, and host CPU (3–4× lower); the TX + submission modes and the tuning levers, with the methodology. + +**Chip specifics & internals:** + +- [8822E quirks](docs/8822e-quirks.md) — the RTL8812EU/8822EU definitive + quirks list: what the chip needs, what devourer does, the reproducers. +- [Logging](docs/logging.md) — the two-plane output schema: JSONL machine + events on stdout, human diagnostics on stderr. ## Testing diff --git a/docs/8822e-quirks.md b/docs/8822e-quirks.md index 7b41b32b..719c300f 100644 --- a/docs/8822e-quirks.md +++ b/docs/8822e-quirks.md @@ -7,8 +7,8 @@ The rtl8822e family (WiFi-only **RTL8812EU** `0bda:a81a`, BT-combo **RTL8822EU** `0bda:e822`, chip-id `0x17`, Jaguar3) as devourer drives it today. Every entry states what the chip needs, what devourer does about it, the residual cost if any, and the reproducer that proves it. Reference hardware: LB-LINK -BL-M8812EU2 (`0bda:a81a`, rfe-type 21); the BT-combo part and a second board -for variance remain untested (see **Untested**). +BL-M8812EU2 (`0bda:a81a`, rfe-type 21); the BT-combo part and other board +variants are outside the characterized set (see **Coverage**). The methodology that closed most of these: vendor-kernel ground truth on the same unit (`tests/eu_kernel_mcs_probe.sh`), full MAC+BB end-state diff @@ -35,8 +35,9 @@ family programs. Three of these must hold, or TX breaks in distinctive ways: ONE chain (2SS gets both). The old 1ss-on-both mapping (`0x33`) interacts with the DPDT fix — MCS0 TX stalls (bulk NAK wedge) and MCS7 delivery collapses. Path B is selected at 5 GHz on measured merit (MCS7 ~2.5× the - delivery of 1ss-A at ~3 dB better EVM on this module); the 2.4 GHz choice is - the kernel default, unvalidated (see **2.4 GHz TX**). + delivery of 1ss-A at ~3 dB better EVM on this module); the 2.4 GHz choice + follows the kernel default — on-air comparison is blocked by the 2.4 GHz TX + quirk (see **2.4 GHz TX**). - **`PAD_CTRL1 0x64[29:28]`**: halmac pre-init sets both; the bring-up's FW/coex steps clear bit 29 — re-asserted post-coex. @@ -160,10 +161,12 @@ duty×rate — energy, not decodable throughput.) `GetTxPowerCaps().step_measured` stays false; controllers should calibrate their own dB-per-step or use ground RSSI. -## Untested +## Coverage -- **RTL8822EU (`0bda:e822`, BT combo)**: entirely untested — including whether +Everything above is characterized on one board: the LB-LINK BL-M8812EU2 +(WiFi-only RTL8812EU, rfe 21). Outside that set: + +- **RTL8822EU (`0bda:e822`, BT combo)**: uncharacterized — including whether it needs more than the WiFi-only coex handling the CU-style combo parts get. -- **Board variance**: all of the above is one LB-LINK BL-M8812EU2 (rfe 21). - rfe 22–24 boards differ in RFE pin mapping and DPK policy; the 2.4 GHz TX - verdict especially needs a second board. +- **Other rfe types**: rfe 22–24 boards differ in RFE pin mapping and DPK + policy; the 2.4 GHz TX verdict in particular is single-board evidence. diff --git a/docs/ack-txreport.md b/docs/ack-txreport.md deleted file mode 100644 index 1ca6fc3a..00000000 --- a/docs/ack-txreport.md +++ /dev/null @@ -1,64 +0,0 @@ -# Unicast ACK + TxReport capability matrix (M0 contract 3) - -A scheduled MAC's reliability layer (per-UE delivery detection, HARQ-style -retransmission, link adaptation) rests on two capabilities per generation: -an **injected unicast descriptor solicits a hardware ACK** (with autonomous -MAC retransmission until it arrives), and **`TxReport` reports the per-frame -ACK / no-ACK outcome** to the host. This measures both, per generation, plus -the report delivery rate — `tests/ack_txreport_matrix.sh` / -`tests/ack_txreport_analyze.py` (`--selftest` covers the verdict logic). - -## Method - -Fixed hardware-ACK responder (`SetAckResponder` on a second adapter); per TX -generation three phases: **on** (responder armed with MAC1, unicast QoS-Data -to MAC1 → expect ~100% `tx.report ok`, retries ~0), **retarget** (responder -re-armed to a different MAC2, TX to MAC2 → proves RA and responder MAC are -arbitrary), **off** (no responder → expect 0% ok, retries pinned at the -descriptor limit: the no-ACK outcome must be *visible*, per frame). -`report_coverage` = reports / frames sent (`tx.stats.submitted`); HalMAC adds -SW_DEFINE tag-echo gap counting. - -TX sessions run `DEVOURER_TX_WITH_RX=thread`: CCX reports arrive on the C2H -RX path, so J1/J2 TX-only sessions never see them (measured: J2 TX-only = 0 -reports; only J3 drains C2H off its coex runtime without an RX loop). A -scheduled MAC runs TX+RX anyway, so this is the relevant session shape. - -## Measured (ch36, MCS3, unicast TA, 8814AU responder, ~8 s/phase) - -| TX generation | on: ACK rate / mean retries | retarget | off: retries pinned | report coverage | tag gaps | -|---|---|---|---|---|---| -| Jaguar1 8812AU | 1.00 / 0.34 | 1.00 / 0.25 | yes (12) | 1.00 | n/a (8812 fmt) | -| Jaguar2 8812BU | 0.91 / 2.1 (run-to-run 0.12–0.91) | 0.64 / 5.3 | yes (12) | 0.86 | 0 | -| Jaguar3 8822CU | 1.00 / 0.24 | 1.00 / 0.13 | yes (12) | 0.96 | 0 | - -Responder-side capability (same bench, J3 TX as the reference soliciting -station): **8814AU** closes the loop at retries ~0.1 (the bench responder of -choice); **8812AU** works but degraded (97% delivery at ~7 mean retries — -its SIFS ACKs only land intermittently); **8821AU never closed the loop** -(TX retries stayed pinned with it armed); the 8812BU responder was -previously proven (`tests/ack_responder_check.sh`). - -## The contract - -1. **Per-frame delivery detection is GO on all three generations**: the OFF - phase pins retries at the descriptor limit with `state=1` on every report — - a no-ACK outcome is unambiguously visible per frame, which is all M2's - software retransmission logic needs. Report coverage 86–100% with zero - HalMAC tag gaps (interior losses); the reliability layer must tolerate a - ~5–15% report-less frame tail (treat missing report as "unknown", not - "delivered"). -2. **Closed-loop hardware ACK + autonomous retry is GO on Jaguar1 and - Jaguar3** (100% delivery, retries ≈ 0.2–0.3) including retargeting an - arbitrary UE MAC mid-session (re-arm `SetAckResponder`, change the - descriptor RA — both fully dynamic). -3. **Jaguar2 as the soliciting TX is MARGINAL on this bench**: ACK closure - varied 12–91% across identical runs (mean retries 2–11) against both - 8814AU and 8812AU responders, and its TX pace in the TX+RX-thread shape is - ~24 ms/frame regardless of the requested gap (~37 fps vs J1/J3's ~150). - As a *responder* J2 is proven good. An M1 cell should prefer J1/J3 (or the - 8821CE) for the DU role until the J2 TX anomaly is root-caused; treat it - as open, not as silicon folklore. -4. Bench quirk recorded: the J3 report's `missed` field reads a constant 4× - the report count while tag continuity shows zero loss — the 8822C - `missed_rpt` offset likely decodes something else; trust `tag` gaps on J3. diff --git a/docs/adaptive-link-validation.md b/docs/adaptive-link-validation.md index 6efa0821..ce059b6d 100644 --- a/docs/adaptive-link-validation.md +++ b/docs/adaptive-link-validation.md @@ -99,36 +99,35 @@ than equivalent static noise** — the core prediction of the simulation: (0.82 → 0.09 at gain 52) — fades punch through the controller's fixed margin even when the average looks acceptable. -This is a clean **relative** confirmation. It is **not** an absolute SLA test: on -this bench the static baseline never reaches 0.99 (it tops out ≈ 0.89), because -the adapters sit inches apart with tens of dB of margin, so the interferer is -never in the "link holds at 0.99, then fades break it" regime. The residual loss +This is a clean **relative** confirmation. It is **not** an absolute SLA test: +on a near-field bench the static baseline never reaches 0.99 (it tops out +≈ 0.89), because the adapters sit inches apart with tens of dB of margin, so +the interferer is never in the "link holds at 0.99, then fades break it" regime. The residual loss is real end-to-end loss (on-air + processing), now measured honestly. -## Open questions (need hardware not currently available) - -1. **Absolute SLA-break, and the fade-margin go/no-go.** The relative fade - penalty is confirmed; the absolute "baseline holds 0.99 → fading breaks it" - test — and the decision to enable the fade margin by default — needs a channel - that can be parked at ~0.99 and stressed in a controlled way. The bench can't - reach that over the air. **An attenuator rig is the unblock** (see below). With - it, the test is: at a fixed setpoint where static holds ~0.99, sweep fading and - compare `fade_margin_k` off vs on. - -2. **Calibrate a real build.** linklab's RF-chain model and this repo's energy - model are nominal. `tests/calibrate_energy.py` (thermal + SDR power sweeps) and - `tests/calibrate_link.py` (interferer sweeps) fit them to a specific build; - `linklab`'s `calib/fit.py` + `export.py` then produce a devourer - `energy_calib.json`. This needs the sweeps run on hardware to anchor the - absolute energy numbers (relative savings hold without it). - -3. **On-air adaptive SVC.** The per-temporal-layer ladder flies at fixed MCS today - (`svctx`, `tests/svc_uep_onair.sh`); retuning the per-layer ladder and shed - set live from the ground controller is validated in simulation (linklab) but - not yet on air. - -4. **Real RC uplink.** The rendezvous/failsafe watchdog input is abstracted; a - real command-radio uplink wires into it directly. +## What this validation does not cover + +1. **The absolute SLA-break point, and the fade-margin go/no-go.** The relative + fade penalty is confirmed; the absolute "baseline holds 0.99 → fading breaks + it" regime needs a channel that can be parked at ~0.99 and stressed in a + controlled way — an over-the-air bench cannot hold that setpoint, so it + requires the conducted (attenuator) setup below. With that rig, the test is: + at a fixed setpoint where static holds ~0.99, sweep fading and compare + `fade_margin_k` off vs on. + +2. **Absolute energy numbers.** linklab's RF-chain model and this repo's energy + model are nominal. `tests/calibrate_energy.py` (thermal + SDR power sweeps) + and `tests/calibrate_link.py` (interferer sweeps) fit them to a specific + build; `linklab`'s `calib/fit.py` + `export.py` then produce a devourer + `energy_calib.json`. Until those sweeps anchor a given build, the energy + savings are relative figures (the relative shape holds regardless). + +3. **Live on-air ladder retuning.** The per-temporal-layer ladder flies at fixed + MCS on air (`svctx`, `tests/svc_uep_onair.sh`); retuning the ladder and shed + set live from the ground controller is validated in simulation (linklab). + +4. **A real RC uplink.** The rendezvous/failsafe watchdog input is abstracted; + a real command-radio uplink wires into it directly. ### What the attenuator rig looks like diff --git a/docs/adaptive-link.md b/docs/adaptive-link.md index c3daa3e1..7a8fe464 100644 --- a/docs/adaptive-link.md +++ b/docs/adaptive-link.md @@ -247,9 +247,9 @@ below the point where enhancement is gone. - **The command uplink is abstracted.** There is no real RC radio in the repo, so uplink loss is simulated to exercise the failsafe and rendezvous; a real uplink drops into the same watchdog input. -- **On-air scalable video is not yet adaptive.** Each temporal layer can already - fly at its own modulation on air; the closed loop that retunes the per-layer - ladder live is validated in software and is the next on-air integration step. +- **Live ladder retuning is validated in simulation, not on air.** Each temporal + layer already flies at its own modulation on air; the closed loop that retunes + the per-layer ladder live runs in the linklab simulation. ## References diff --git a/docs/aggregation.md b/docs/aggregation.md index 773d3c43..946a0202 100644 --- a/docs/aggregation.md +++ b/docs/aggregation.md @@ -65,7 +65,10 @@ best-effort decode). Jaguar3 TX-only sessions get reports via the coex thread's C2H drain; TX+RX sessions via the RX loop. This is the TX-side link sensor: retry counts feed adaptive rate/power the way -RSSI/EVM feed the RX side. +RSSI/EVM feed the RX side. The measured per-generation contract — which +generations close the hardware-ACK loop as soliciting TX, and the report +coverage a reliability layer can count on — is in +[scheduled-mac.md](scheduled-mac.md). ## A-MPDU (`SetAmpduMode`) diff --git a/docs/ap-mode.md b/docs/ap-mode.md index 0d284c38..b7943a69 100644 --- a/docs/ap-mode.md +++ b/docs/ap-mode.md @@ -1,9 +1,11 @@ # Infrastructure AP mode — devourer as a Wi-Fi access point -devourer's hardware-beacon path (`StartBeacon`, see `docs/time-distribution.md`) -is the foundation for acting as a real infrastructure **access point**: a Linux -station discovers devourer, authenticates, associates, gets an IP, and exchanges -IP traffic with it — open or WPA2-PSK encrypted. These are experimental example +devourer's hardware-beacon path (`StartBeacon`, see `docs/time-distribution.md`; +live content swaps via `UpdateBeaconPayload` are measured in +[scheduled-mac.md](scheduled-mac.md)) is the foundation for acting as a real +infrastructure **access point**: a Linux station discovers devourer, +authenticates, associates, gets an IP, and exchanges IP traffic with it — open +or WPA2-PSK encrypted. These are experimental example harnesses under `tests/`, not a library API — the pieces (beacon, RX callback, `send_packet`) are the same ones the demos use; the AP logic (probe/auth/assoc responses, DHCP/ARP/ICMP, the WPA2 4-way handshake, software CCMP) lives in the diff --git a/docs/beacon-grant.md b/docs/beacon-grant.md deleted file mode 100644 index f25b02e2..00000000 --- a/docs/beacon-grant.md +++ /dev/null @@ -1,62 +0,0 @@ -# Dynamic beacon-content delivery (M0 contract 2) - -A scheduled MAC (5G-NR RAN epic) carries its DCI-style grant map in the beacon -body, so the DU must be able to **change the airing beacon's content** without -missing, duplicating or tearing beacons. The primitive is -`IRtlDevice::UpdateBeaconPayload(beacon, len)` — an in-place content swap for -an active `StartBeacon` (same buffer contract; interval, TBTT phase and port -identity untouched) riding the same reserved-page re-download the TBTT steers -use. Its companion `StopBeacon()` silences the beacon function: the chip -beacons **autonomously**, so a beaconing session that ends without a device -power-cycle must call it — a killed process leaves the beacon airing -indefinitely (bench-bitten: a stale beacon with the same SA contaminated the -next test's witness). - -Per generation: Jaguar2 replaces the retained `_bcn_mpdu` and re-downloads via -the steer path (the J2 engine loses the bcn-valid latch on re-latch, so the -download is also the re-arm); Jaguar1 is a fresh BCNQ-boundary store bracket -(no re-ignite needed — the port stays configured); Jaguar3 is a fresh HalMAC -`download_beacon_page` (its latch is stable — no steer machinery involved). - -## Measured (ch36, 100 TU interval, 30 updates/gen every ~1 s, 8821AU witness) - -`tests/beacon_update_check.sh` — the probe beacons a versioned vendor IE -(u32 version + version-derived 32-byte pattern + CRC16, so one frame proves a -torn swap) and calls `UpdateBeaconPayload` every 10 intervals; the witness -records `(hw seq, tsfl, version, crc_ok)` per beacon and -`tests/beacon_update_analyze.py` reconstructs the TBTT grid from `tsfl`, -separating update-caused missing slots from background witness loss. - -| generation | updates aired | excess skips/update | torn | version regress | update→air p50 | p99 | -|---|---|---|---|---|---|---| -| Jaguar1 8812AU | 30/30 | 0.0 | 0 | 0 | 40 ms | 67 ms | -| Jaguar2 8812BU | 30/30 | 0.0 | 0 | 0 | 63 ms | 174 ms | -| Jaguar3 8822CU | 30/30 | 0.0 | 0 | 0 | 72 ms | 99 ms | - -## The contract - -1. **Dynamic beacon grants are GO on all three generations.** Every update - aired; the background-corrected skip cost was 0 in this bench (the ≤ 1 - skipped beacon per re-download that TBTT steers pay was not even resolvable - above witness loss at this cadence). No torn frames — the swap is - frame-atomic as observed on air (the CRC never caught a half-old, - half-new body). No old content re-airing after the new version's first - appearance. -2. **Update→air latency is TBTT-quantized**: content lands on the next (or - next-but-one) beacon — p50 ≈ half a period to a period, p99 ≤ ~2 periods at - 100 TU. A grant map published via the beacon is therefore *effective* one - to two beacon intervals after the scheduler decides it. M1's grant timing - must budget that pipeline (grants for slot epoch N+2, decided at epoch N). -3. **Not atomic versus TBTT by design**: a beacon airing during the download - may still carry the previous content, and the API guarantees only - whole-version frames (measured, via the CRC), not a bounded switchover - instant. The `effective_tbtt` discipline lives in the grant-map payload - (epoch field), not in the radio primitive. -4. The M0 fallback (static beacon grant + scheduled unicast deltas) is NOT - needed. Kept as the escape hatch if a future generation/transport shows a - real per-update skip cost. - -Tooling: `tests/beacon_update_probe.cpp` (TX + witness modes; build line in -header), `tests/beacon_update_analyze.py` (`--selftest` covers the -skip/dup/late/stale/torn classifier on synthetic streams), -`tests/beacon_update_check.sh` (per-generation orchestration). diff --git a/docs/bench-testing-near-field.md b/docs/bench-testing-near-field.md index 35cbb068..fd6b59ad 100644 --- a/docs/bench-testing-near-field.md +++ b/docs/bench-testing-near-field.md @@ -20,7 +20,7 @@ Two mechanisms, both power problems that masquerade as sensitivity problems: ## The tell: EVM, not SNR The counterintuitive part — and the reason people miss it — is that **SNR can -look perfectly healthy while the link is saturating.** Measured on this bench, +look perfectly healthy while the link is saturating.** Measured by sweeping an 8812AU's TX power from low to full while a second adapter reported per-frame metrics (`tests/saturation_knee_sweep.sh`): diff --git a/docs/dl-departure.md b/docs/dl-departure.md deleted file mode 100644 index e5164408..00000000 --- a/docs/dl-departure.md +++ /dev/null @@ -1,65 +0,0 @@ -# DL departure: the send_packet→air guard-time contract (M0 contract 1) - -A scheduled MAC (5G-NR RAN epic) promises collision-free downlink slots, which -requires knowing how long after the host calls `send_packet` the frame is -actually on the air — per transport, as a **distribution**, not an average. -This is that measurement and its per-transport contract. - -## Method - -One witness receiver captures, from the SAME frame, the transmitter's embedded -submit stamps and its own hardware RX timestamp (`RxAtrib.tsfl`, MAC-latched): - -- **TD v2 tag** (`examples/tdma/tdma.h`, version byte 2): `tx_tsf` = ReadTsf - near send (TX hardware clock) + `host_ns` = `steady_clock` immediately - before the `send_packet` call (the clock a host-side slot scheduler actually - controls). -- `tests/txegress_analyze.py` least-squares fits `rx_tsfl` against each stamp; - the line absorbs clock offset, crystal skew and propagation, so the residual - is per-frame submit→air jitter. Robust (3×MAD) rejection separates the - **floor** (frames that aired immediately = transport + MAC-pipeline jitter) - from the **tail** (channel deferral — which a scheduler must budget too). -- The contract number: `guard_us` = p99.9 of all host-clock residuals above - the median, emitted as a machine-checkable `txeg.verdict` JSONL line with - p50/p90/p99/p99.9/max and a `fine_dl_slots` go/no-go against `--slot-us`. - -Probes: `tests/dl_departure_tx.cpp` (any USB adapter), -`tests/pcie_txegress_tx.cpp` (8821CE over vfio). Orchestration: -`tests/dl_departure_matrix.sh` (fixed witness, TX swept over the transports; -the PCIe cell ships the probe to the remote rig, vfio-binds, runs, restores). - -## Measured (ch36, 6M legacy, ~2000 frames/cell, 8821AU witness) - -| transport | floor RMS | p90 | p99 | p99.9 (=guard) | max | -|---|---|---|---|---|---| -| Jaguar1 8812AU (async USB2) | 22 µs | 28 µs | 101 µs | 0.76 ms | 2.1 ms | -| Jaguar2 8812BU (sync USB3) | 14 µs | 64 µs | 1.7 ms | 3.1 ms | 3.3 ms | -| Jaguar3 8822CU (sync USB3) | 16 µs | 61 µs | 2.2 ms | 3.2 ms | 3.3 ms | -| 8821CE (PCIe, vfio) | 11 µs | 54 µs | 2.4 ms | 3.2 ms | 3.3 ms | - -Crystal ppm sane (−17 ppm, same reference witness) on every cell — the robust -fits locked. The tail is run-to-run ambient-dependent: the same J1 cell has -measured p99.9 between ~0.8 ms and ~3.2 ms across runs on the same channel. - -## The contract - -1. **The transport floor is NOT the bottleneck.** All four transports place - the bulk of frames within tens of µs of nominal (floor RMS 11–26 µs; p90 - ≤ 64 µs). PCIe is the tightest (11 µs) but the USB floors are the same - order — transport choice does not gate slot design at ≥ ms slot sizes. -2. **The tail is channel deferral, and it does not go away.** Even at 5 GHz on - a mostly-idle channel, ~1% of frames air 0.1–2.4 ms late (ambient beacons + - CSMA — `SetCcaMode` relaxes energy-CCA, not preamble deferral). p99.9 sits - at ~1–3 ms on every transport. -3. **Go/no-go: fine (sub-ms) DL slots are REFUTED on all transports** for a - p99.9-grade deadline on a real channel. The M1 design consequence is the - fallback the epic anticipated: a **submission-ahead scheduler** — submit a - slot's frame `guard_us` before the slot boundary and size slots ≥ ~2× the - measured guard (i.e. multi-ms slots), OR accept a bounded deadline-miss - ratio (~1% at a 1 ms guard, per the p99 row) and let HARQ absorb it. - Re-measure `guard_us` per deployment environment; the verdict line exists - so that check is one script run. - -The hardware-beacon path (MAC-timed TBTT, `PinBeaconTbtt`) is unaffected by -any of this — beacons depart on the TBTT grid below the CSMA/queueing layer, -which is why scheduled **UL** rides beacon-steered timing, not `send_packet`. diff --git a/docs/driver-primer.md b/docs/driver-primer.md index 92e09ce7..dcefb0e2 100644 --- a/docs/driver-primer.md +++ b/docs/driver-primer.md @@ -1,14 +1,15 @@ # A visual primer on the driver machinery -The [RF primer](rf-primer.md) shows what the *radio physics* looks like — -subcarriers, constellations, gain control. This page is its sibling for the -*silicon and software*: what is actually inside a Realtek Wi-Fi chip, and what the -vendor driver vocabulary means — firmware, efuse, FWDL, DMAC/CMAC, halbb/halrf, -LSSI, IQK, H2C, DIG. If you've ever opened a Realtek vendor tree and -bounced off ten thousand files of alphabet soup, this is the decoder ring. Like the -RF primer it's a picture book: thirteen short animations in the DEVOURER -live-monitor style, ordered from the silicon up. Read it top to bottom and both the -vendor trees and devourer's `src/` will read like prose. +This page is the decoder ring for the *silicon and software* inside a Realtek +Wi-Fi chip: what is actually in the package, and what the vendor-driver +vocabulary means — firmware, efuse, FWDL, DMAC/CMAC, halbb/halrf, LSSI, IQK, +H2C, DIG. If you've ever opened a Realtek vendor tree and bounced off ten +thousand files of alphabet soup, start here. Its sibling, the +[RF primer](rf-primer.md), shows what the *radio physics* looks like — +subcarriers, constellations, gain control. Like the RF primer this is a +picture book: thirteen short animations in the DEVOURER live-monitor style, +ordered from the silicon up. Read it top to bottom and both the vendor trees +and devourer's `src/` will read like prose. Everything here is grounded in the vendor reference drivers vendored under `reference/` (`rtl8812au` → `rtl88x2bu`/`rtl88x2cu`/`rtl88x2eu`), in Realtek's diff --git a/docs/fhss.md b/docs/fhss.md index 11940ec7..1dcd4c73 100644 --- a/docs/fhss.md +++ b/docs/fhss.md @@ -194,9 +194,9 @@ Two strategies, matched to the order it faces: - **predictive** (against sequential): jam the channel the public order says comes *next*, which cancels the follower's own latency. -Measured on this rig, the follower's reaction is dominated by the B210 transmit +Measured, the follower's reaction is dominated by the B210 transmit retune at **~3.5 ms** (wideband sensing itself costs ~0.3 ms). That latency is -the floor on how fast any reactive follower here can move, and it **favours the +the floor on how fast such a reactive follower can move, and it **favours the defender**. The two strategies' chase dynamics diverge sharply: over a 20-second run against a 50 ms-slot transmitter, the **reactive** follower issues ~2100 retunes (constant correction — always a step behind an unpredictable hop) while @@ -210,7 +210,7 @@ land on a keyed hopper, while a predictive jammer against a sequential hopper is limited only by its retune time and holds to much shorter dwells. The gap between those two thresholds is exactly what a keyed permutation buys. -## Where it stands, and what is next +## Where it stands What works today, on hardware: fast intra-band retune on all three chip generations; sequential and keyed slot-hopping driven purely by radiotap and @@ -223,6 +223,6 @@ the slow full set); per-hop TX power is not re-tuned, so a hopset spanning a wid 5 GHz range wants a periodic full set to refresh per-rate power; the follower experiment uses a 3-channel hopset because a single B210 needs ≥60 MS/s to span a 60 MHz hopset in one FFT and trips a UHD tuning assertion at the usual -61.44 MS/s. And the natural next step — **adaptive hopset exclusion**, dropping a -persistently-jammed channel from the set so the link stops visiting it at all — -is future work. +61.44 MS/s. And the schedule visits every configured channel — there is no +adaptive exclusion of a persistently-jammed one; the fused-FEC layer absorbs +those dwells as erasures. diff --git a/docs/fused-fec.md b/docs/fused-fec.md index fcd3d500..3448c32d 100644 --- a/docs/fused-fec.md +++ b/docs/fused-fec.md @@ -12,14 +12,14 @@ default, never cooperate: 1. **PHY-layer FEC** — the convolutional (BCC) or LDPC inner code baked into every 802.11 MCS. Its code rate is fixed by the MCS choice. devourer already selects it per packet from the radiotap header / `TxMode` - (`src/RadiotapBuilder.cpp`, `src/RtlJaguarDevice.cpp`), and the SVC ladder + (`src/RadiotapBuilder.cpp`, `src/jaguar1/RtlJaguarDevice.cpp`), and the SVC ladder (`examples/svctx/svc_tx.h`) already picks a robust MCS for important video layers. 2. **Application-layer FEC** — a packet-erasure code spread across radio frames (Reed-Solomon / RaptorQ / RLC). It recovers whole frames that are *lost*. 3. **Corrupted-frame retention** — `DEVOURER_RX_KEEP_CORRUPTED` keeps frames that fail the 802.11 FCS instead of dropping them - (`src/RadioManagementModule.cpp` sets RCR `ACRC32|AICV`; `examples/rx/main.cpp` + (`src/jaguar1/RadioManagementModule.cpp` sets RCR `ACRC32|AICV`; `examples/rx/main.cpp` surfaces the body with `crc_err`/`icv_err` + per-frame RSSI/EVM/SNR). **The gap:** a frame that fails the FCS is normally dropped wholesale, so the diff --git a/docs/jammer-resilience.md b/docs/jammer-resilience.md index cf6f35f2..05b413ad 100644 --- a/docs/jammer-resilience.md +++ b/docs/jammer-resilience.md @@ -1,4 +1,4 @@ -# Jammer resilience (M5) +# Jammer resilience How frequency hopping and the fused-FEC link hold up against a narrowband jammer, and where a *following* jammer stops being able to keep up. Two @@ -64,7 +64,7 @@ Two strategies, matched to the TX order: - **predictive** (vs a sequential TX): the public round-robin order lets the follower jam the channel it expects *next*, cancelling its own latency. -Measured on this rig: +Measured: - Full-duplex sense costs ~0.3 ms; the follower's reaction is dominated by the B210 TX retune (`set_tx_freq`) at **~3.5 ms**. That retune latency is the floor @@ -87,7 +87,7 @@ The gap between those two thresholds is what a keyed permutation buys. ## Notes / limitations -- The follower needs ≥60 MS/s to span a 60 MHz hopset in one FFT. On this B210, +- The follower needs ≥60 MS/s to span a 60 MHz hopset in one FFT. On a B210, 61.44/56/50 MS/s trip a UHD tuning assertion (`std::lcm` overflow); 60 MS/s is clean, so the follower experiment uses a 3-channel hopset (36/40/44, 40 MHz) that fits comfortably. @@ -95,5 +95,6 @@ The gap between those two thresholds is what a keyed permutation buys. streamers per cycle (streamer setup is ~hundreds of ms and rapid RX/TX switching corrupts the B200 control channel). Persistent streamers with serialized control (send burst → sense → retune, one thread) are stable. -- M6 (adaptive hopset exclusion — drop a persistently-jammed channel from the - set) is follow-up work. +- The hop schedule visits every configured channel; there is no adaptive + exclusion of a persistently-jammed one — the FEC absorbs those dwells (the + ~1/N loss in experiment 1). diff --git a/docs/multi-ap-cellular.md b/docs/multi-ap-cellular.md index 1805d16b..b55d4de4 100644 --- a/docs/multi-ap-cellular.md +++ b/docs/multi-ap-cellular.md @@ -26,8 +26,11 @@ LTE deployment, minus the licensed spectrum and the basestation price tag. This page is the conceptual map of what those ingredients build. Everything below is *architecture riding on proven primitives*: the per-link mechanics all -have bench numbers in the linked docs; the multi-cell coordination itself is -application-layer software — no new silicon tricks required. +have bench numbers in the linked docs — the scheduler-facing ones (submit→air +guard time, dynamic beacon grants, hardware ACK/TxReport, per-UE RX +attribution) are collected in [scheduled-mac.md](scheduled-mac.md); the +multi-cell coordination itself is application-layer software — no new silicon +tricks required. ## 1. Coordinated scheduling — ICIC over your existing Ethernet diff --git a/docs/scheduled-mac.md b/docs/scheduled-mac.md new file mode 100644 index 00000000..af954410 --- /dev/null +++ b/docs/scheduled-mac.md @@ -0,0 +1,255 @@ +# Scheduled-MAC building blocks — four measured contracts + +A scheduled, cellular-style MAC on devourer — collision-free TDMA downlink +slots, a grant map carried in the beacon body, per-frame delivery detection +with hardware retransmission, per-UE link adaptation — rests on four radio +capabilities. Each section states the capability, how it was measured on +hardware, and the resulting per-generation contract a scheduler can build on. +The shared-clock and TBTT machinery these lean on is +[time-distribution.md](time-distribution.md); the multi-cell coordination +story they add up to is [multi-ap-cellular.md](multi-ap-cellular.md). + +## TX departure: the send_packet→air guard time + +A scheduled MAC that promises collision-free downlink slots needs to know how +long after the host calls `send_packet` the frame is actually on the air — +per transport, as a **distribution**, not an average. This is that +measurement and its per-transport contract. + +### Method + +One witness receiver captures, from the SAME frame, the transmitter's embedded +submit stamps and its own hardware RX timestamp (`RxAtrib.tsfl`, MAC-latched): + +- **TD v2 tag** (`examples/tdma/tdma.h`, version byte 2): `tx_tsf` = ReadTsf + near send (TX hardware clock) + `host_ns` = `steady_clock` immediately + before the `send_packet` call (the clock a host-side slot scheduler actually + controls). +- `tests/txegress_analyze.py` least-squares fits `rx_tsfl` against each stamp; + the line absorbs clock offset, crystal skew and propagation, so the residual + is per-frame submit→air jitter. Robust (3×MAD) rejection separates the + **floor** (frames that aired immediately = transport + MAC-pipeline jitter) + from the **tail** (channel deferral — which a scheduler must budget too). +- The contract number: `guard_us` = p99.9 of all host-clock residuals above + the median, emitted as a machine-checkable `txeg.verdict` JSONL line with + p50/p90/p99/p99.9/max and a `fine_dl_slots` go/no-go against `--slot-us`. + +Probes: `tests/dl_departure_tx.cpp` (any USB adapter), +`tests/pcie_txegress_tx.cpp` (8821CE over vfio). Orchestration: +`tests/dl_departure_matrix.sh` (fixed witness, TX swept over the transports; +the PCIe cell ships the probe to the remote rig, vfio-binds, runs, restores). + +### Measured (ch36, 6M legacy, ~2000 frames/cell, 8821AU witness) + +| transport | floor RMS | p90 | p99 | p99.9 (=guard) | max | +|---|---|---|---|---|---| +| Jaguar1 8812AU (async USB2) | 22 µs | 28 µs | 101 µs | 0.76 ms | 2.1 ms | +| Jaguar2 8812BU (sync USB3) | 14 µs | 64 µs | 1.7 ms | 3.1 ms | 3.3 ms | +| Jaguar3 8822CU (sync USB3) | 16 µs | 61 µs | 2.2 ms | 3.2 ms | 3.3 ms | +| 8821CE (PCIe, vfio) | 11 µs | 54 µs | 2.4 ms | 3.2 ms | 3.3 ms | + +Crystal ppm sane (−17 ppm, same reference witness) on every cell — the robust +fits locked. The tail is run-to-run ambient-dependent: the same J1 cell has +measured p99.9 between ~0.8 ms and ~3.2 ms across runs on the same channel. + +### The contract + +1. **The transport floor is NOT the bottleneck.** All four transports place + the bulk of frames within tens of µs of nominal (floor RMS 11–26 µs; p90 + ≤ 64 µs). PCIe is the tightest (11 µs) but the USB floors are the same + order — transport choice does not gate slot design at ≥ ms slot sizes. +2. **The tail is channel deferral, and it does not go away.** Even at 5 GHz on + a mostly-idle channel, ~1% of frames air 0.1–2.4 ms late (ambient beacons + + CSMA — `SetCcaMode` relaxes energy-CCA, not preamble deferral). p99.9 sits + at ~1–3 ms on every transport. +3. **Go/no-go: fine (sub-ms) DL slots are REFUTED on all transports** for a + p99.9-grade deadline on a real channel. The design consequence is a + **submission-ahead scheduler** — submit a slot's frame `guard_us` before + the slot boundary and size slots ≥ ~2× the measured guard (i.e. multi-ms + slots), OR accept a bounded deadline-miss ratio (~1% at a 1 ms guard, per + the p99 row) and let HARQ absorb it. Re-measure `guard_us` per deployment + environment; the verdict line exists so that check is one script run. + +The hardware-beacon path (MAC-timed TBTT, `PinBeaconTbtt`) is unaffected by +any of this — beacons depart on the TBTT grid below the CSMA/queueing layer, +which is why scheduled **UL** rides beacon-steered timing, not `send_packet`. + +## Dynamic beacon-content delivery + +A scheduled MAC that carries its DCI-style grant map in the beacon body must +be able to **change the airing beacon's content** without missing, duplicating +or tearing beacons. The primitive is +`IRtlDevice::UpdateBeaconPayload(beacon, len)` — an in-place content swap for +an active `StartBeacon` (same buffer contract; interval, TBTT phase and port +identity untouched) riding the same reserved-page re-download the TBTT steers +use. Its companion `StopBeacon()` silences the beacon function: the chip +beacons **autonomously**, so a beaconing session that ends without a device +power-cycle must call it — a killed process leaves the beacon airing +indefinitely (bench-bitten: a stale beacon with the same SA contaminated the +next test's witness). + +Per generation: Jaguar2 replaces the retained `_bcn_mpdu` and re-downloads via +the steer path (the J2 engine loses the bcn-valid latch on re-latch, so the +download is also the re-arm); Jaguar1 is a fresh BCNQ-boundary store bracket +(no re-ignite needed — the port stays configured); Jaguar3 is a fresh HalMAC +`download_beacon_page` (its latch is stable — no steer machinery involved). + +### Measured (ch36, 100 TU interval, 30 updates/gen every ~1 s, 8821AU witness) + +`tests/beacon_update_check.sh` — the probe beacons a versioned vendor IE +(u32 version + version-derived 32-byte pattern + CRC16, so one frame proves a +torn swap) and calls `UpdateBeaconPayload` every 10 intervals; the witness +records `(hw seq, tsfl, version, crc_ok)` per beacon and +`tests/beacon_update_analyze.py` reconstructs the TBTT grid from `tsfl`, +separating update-caused missing slots from background witness loss. + +| generation | updates aired | excess skips/update | torn | version regress | update→air p50 | p99 | +|---|---|---|---|---|---|---| +| Jaguar1 8812AU | 30/30 | 0.0 | 0 | 0 | 40 ms | 67 ms | +| Jaguar2 8812BU | 30/30 | 0.0 | 0 | 0 | 63 ms | 174 ms | +| Jaguar3 8822CU | 30/30 | 0.0 | 0 | 0 | 72 ms | 99 ms | + +### The contract + +1. **Dynamic beacon grants are GO on all three generations.** Every update + aired; the background-corrected skip cost was 0 in this measurement (the + ≤ 1 skipped beacon per re-download that TBTT steers pay was not even + resolvable above witness loss at this cadence). No torn frames — the swap + is frame-atomic as observed on air (the CRC never caught a half-old, + half-new body). No old content re-airing after the new version's first + appearance. +2. **Update→air latency is TBTT-quantized**: content lands on the next (or + next-but-one) beacon — p50 ≈ half a period to a period, p99 ≤ ~2 periods at + 100 TU. A grant map published via the beacon is therefore *effective* one + to two beacon intervals after the scheduler decides it, and the scheduler's + grant timing must budget that pipeline (grants for slot epoch N+2, decided + at epoch N). +3. **Not atomic versus TBTT by design**: a beacon airing during the download + may still carry the previous content, and the API guarantees only + whole-version frames (measured, via the CRC), not a bounded switchover + instant. The `effective_tbtt` discipline lives in the grant-map payload + (epoch field), not in the radio primitive. +4. A static-beacon + scheduled-unicast-delta fallback is not needed on any of + the three generations. + +Tooling: `tests/beacon_update_probe.cpp` (TX + witness modes; build line in +header), `tests/beacon_update_analyze.py` (`--selftest` covers the +skip/dup/late/stale/torn classifier on synthetic streams), +`tests/beacon_update_check.sh` (per-generation orchestration). + +## Unicast ACK + TxReport capability matrix + +A scheduled MAC's reliability layer (per-UE delivery detection, HARQ-style +retransmission, link adaptation) rests on two capabilities per generation: +an **injected unicast descriptor solicits a hardware ACK** (with autonomous +MAC retransmission until it arrives), and **`TxReport` reports the per-frame +ACK / no-ACK outcome** to the host. This measures both, per generation, plus +the report delivery rate — `tests/ack_txreport_matrix.sh` / +`tests/ack_txreport_analyze.py` (`--selftest` covers the verdict logic). + +### Method + +Fixed hardware-ACK responder (`SetAckResponder` on a second adapter); per TX +generation three phases: **on** (responder armed with MAC1, unicast QoS-Data +to MAC1 → expect ~100% `tx.report ok`, retries ~0), **retarget** (responder +re-armed to a different MAC2, TX to MAC2 → proves RA and responder MAC are +arbitrary), **off** (no responder → expect 0% ok, retries pinned at the +descriptor limit: the no-ACK outcome must be *visible*, per frame). +`report_coverage` = reports / frames sent (`tx.stats.submitted`); HalMAC adds +SW_DEFINE tag-echo gap counting. + +TX sessions run `DEVOURER_TX_WITH_RX=thread`: CCX reports arrive on the C2H +RX path, so J1/J2 TX-only sessions never see them (measured: J2 TX-only = 0 +reports; only J3 drains C2H off its coex runtime without an RX loop). A +scheduled MAC runs TX+RX anyway, so this is the relevant session shape. + +### Measured (ch36, MCS3, unicast TA, 8814AU responder, ~8 s/phase) + +| TX generation | on: ACK rate / mean retries | retarget | off: retries pinned | report coverage | tag gaps | +|---|---|---|---|---|---| +| Jaguar1 8812AU | 1.00 / 0.34 | 1.00 / 0.25 | yes (12) | 1.00 | n/a (8812 fmt) | +| Jaguar2 8812BU | 0.91 / 2.1 (run-to-run 0.12–0.91) | 0.64 / 5.3 | yes (12) | 0.86 | 0 | +| Jaguar3 8822CU | 1.00 / 0.24 | 1.00 / 0.13 | yes (12) | 0.96 | 0 | + +Responder-side capability (same setup, J3 TX as the reference soliciting +station): **8814AU** closes the loop at retries ~0.1 (the bench responder of +choice); **8812AU** works but degraded (97% delivery at ~7 mean retries — +its SIFS ACKs only land intermittently); **8821AU never closed the loop** +(TX retries stayed pinned with it armed); the 8812BU responder was +separately proven (`tests/ack_responder_check.sh`). + +### The contract + +1. **Per-frame delivery detection is GO on all three generations**: the OFF + phase pins retries at the descriptor limit with `state=1` on every report — + a no-ACK outcome is unambiguously visible per frame, which is all a + software retransmission layer needs. Report coverage 86–100% with zero + HalMAC tag gaps (interior losses); the reliability layer must tolerate a + ~5–15% report-less frame tail (treat missing report as "unknown", not + "delivered"). +2. **Closed-loop hardware ACK + autonomous retry is GO on Jaguar1 and + Jaguar3** (100% delivery, retries ≈ 0.2–0.3) including retargeting an + arbitrary UE MAC mid-session (re-arm `SetAckResponder`, change the + descriptor RA — both fully dynamic). +3. **Jaguar2 as the soliciting TX is MARGINAL as measured**: ACK closure + varied 12–91% across identical runs (mean retries 2–11) against both + 8814AU and 8812AU responders, and its TX pace in the TX+RX-thread shape is + ~24 ms/frame regardless of the requested gap (~37 fps vs J1/J3's ~150). + As a *responder* J2 is proven good. Prefer J1/J3 (or the 8821CE) for the + soliciting role — the J2 anomaly is measured but unexplained; treat it as + open, not as silicon folklore. +4. Bench quirk recorded: the J3 report's `missed` field reads a constant 4× + the report count while tag continuity shows zero loss — the 8822C + `missed_rpt` offset likely decodes something else; trust `tag` gaps on J3. + +## Per-UE RX attribution + +`GetRxQuality()` is device-wide by design: one draining accumulator fed by +every decoded frame, whoever sent it. A cell scheduler adapting per-UE rate and +power needs the same windowed statistics **attributed to each transmitter** — +that is `devourer::cell::UeRxAttribution` (`src/cell/UeRxAttribution.h` — +`src/cell/` holds the caller-side per-cell helpers built on the device API). + +### The contract + +Pure caller-side logic — the device RX loops are untouched and `GetRxQuality` +stays the radio-wide diagnostic. Everything needed is already per-frame in the +`Packet` callback: + +- **key** — the transmitter address (802.11 addr2/TA), extracted by + `cell::extract_ta`: bytes [10..16) of the MPDU for every frame type except + the two control subtypes that end at addr1 (CTS, ACK). +- **values** — `rx_pkt_attrib`'s path-A RSSI/SNR/EVM plus the hardware RX + timestamp `tsfl`, folded with the exact `RxQualityAccumulator` conventions + (`rssi_raw <= 0` is not a sample; SNR/EVM folded only when present; passive + noise floor = `(rssi_raw − 110) − snr_raw/2`). + +`add()` (or `add_mpdu()`, which extracts the TA itself) per frame; +`snapshot()` drains the whole table into one `UeRxWindow` per TA (delta +semantics, like `GetRxQuality`) with converted units, window mean/extremes and +`last_tsfl` for staleness. The table is bounded (default 64 TAs per window); +overflow frames are counted in `evicted_frames`, never silently lost. + +### Measured + +`tests/ue_rx_attribution_check.sh`: two transmitters with distinct unicast SAs +(8812AU at a 2 ms inter-frame gap, 8822CU at 8 ms) against one `ue_rx_probe` +witness (8812BU), 12 s. The probe attributed the streams separately — +TX1 2955 frames at −51 dBm mean, TX2 741 frames at −44 dBm mean, a 4.0× +count ratio exactly matching the 4× cadence ratio — confirming per-UE frame +counts and per-UE signal statistics don't bleed between transmitters. + +### Tooling + +- `tests/ue_rx_probe.cpp` — on-air probe: feeds every decoded frame into a + `UeRxAttribution`, drains once a second, emits one `ue.rx` JSONL event per + UE per window (`ta`, `frames`, `rssi_dbm`, `rssi_max_dbm`, `snr_db`, + `snr_min_db`, `evm_db`, `nf_dbm`, `last_tsfl`) plus `ue.rx.evicted` when the + cap was hit. Build line in the header. +- `tests/ue_rx_attribution_selftest.cpp` — headless ctest guard + (`ue_rx_attribution_derive`): TA extraction over frame types, folding + conventions, drain semantics, eviction accounting. +- `tests/ue_rx_attribution_check.sh` — the two-TX on-air validation above. + +A cell scheduler wraps this into its UE registry (association state, timing +advance, the per-UE RX window as the link-adaptation input). diff --git a/docs/time-distribution.md b/docs/time-distribution.md index d60b7464..cd8b740e 100644 --- a/docs/time-distribution.md +++ b/docs/time-distribution.md @@ -70,9 +70,8 @@ unaffected (already transport-free). Tool: `tests/pcie_txegress_tx.cpp` + `tests/txegress_witness.cpp`. The `timesync` demo drives a PCIe master via `DEVOURER_PCIE_BDF` — but note the ~12 µs floor is a *robust-fit* transport number: on a busy channel the raw slave lock is deferral-limited regardless of -transport (radxa bench, crowded ch6: 420 µs RMS — the channel, not the bus), -and the radxa↔bench RF path only reaches on 2.4 GHz, so prefer the hardware -beacon below for PCIe masters. +transport (measured 420 µs RMS on a crowded ch6 — the channel, not the bus), +so prefer the hardware beacon below for PCIe masters. Run it with `tests/timesync_demo.sh` (one master + two slaves; joins the two `{"ev":"timesync.lock"}` streams by beacon seq into the inter-UE error). @@ -338,11 +337,11 @@ closes the loop, and the closed loop is a shipped tool: `tests/pcie_ptp_beacon.cpp` runs the hardware beacon on the 8821CE and holds its TBTT to the I226 PHC (read via `FD_TO_CLOCKID`, no system-clock detour) with a full-gain PI on the pin actuator. End-to-end bench, everything running -concurrently (radxa loop + a USB slave over the air): +concurrently (the PCIe discipline loop + a USB slave over the air): | link in the chain | measured | |-------------------|----------| -| radxa TBTT vs the I226 PTP reference | **1.24 µs RMS**, max 5.5 µs (156 pins, ~90 s) | +| 8821CE TBTT vs the I226 PTP reference | **1.24 µs RMS**, max 5.5 µs (156 pins, ~90 s) | | USB slave lock through the live discipline | 13.9 µs RMS (575 beacons, ~0 lost through 160+ pins) | The slave number carries a caveat: its all-history least-squares fit assumes a @@ -353,7 +352,10 @@ must be in monitor mode so the MAC/TSF is clocked). What this unlocks at the multi-AP level — coordinated scheduling between cells, make-before-break handover, robots as roaming UEs — is mapped out in -[`multi-ap-cellular.md`](multi-ap-cellular.md). +[`multi-ap-cellular.md`](multi-ap-cellular.md); the four measured +per-generation contracts a slot scheduler builds on (submit→air guard time, +dynamic beacon grants, hardware ACK/TxReport, per-UE RX attribution) are in +[`scheduled-mac.md`](scheduled-mac.md). ## Env knobs diff --git a/docs/timing-accuracy.md b/docs/timing-accuracy.md index a295a9a3..d09542b2 100644 --- a/docs/timing-accuracy.md +++ b/docs/timing-accuracy.md @@ -2,7 +2,7 @@ A common question: how does devourer's hardware-TSF time distribution (`docs/time-distribution.md`) compare with running NTP or PTP over the same -Wi-Fi? Below are **measured** numbers from a two-node bench (this rig), the +Wi-Fi? Below are **measured** numbers from a two-node bench, the methodology, and the honest caveats. Reproduce the transport half with the `reglat` microbench; the NTP half with `chronyd -Q` as described. diff --git a/docs/ue-rx-attribution.md b/docs/ue-rx-attribution.md deleted file mode 100644 index 1f13a5e8..00000000 --- a/docs/ue-rx-attribution.md +++ /dev/null @@ -1,52 +0,0 @@ -# Per-UE RX attribution (M0 contract 4) - -`GetRxQuality()` is device-wide by design: one draining accumulator fed by -every decoded frame, whoever sent it. A cell scheduler adapting per-UE rate and -power needs the same windowed statistics **attributed to each transmitter** — -that is `devourer::cell::UeRxAttribution` (`src/cell/UeRxAttribution.h`), the -seed of the scheduled-MAC `UeRegistry` (5G-NR RAN epic, `src/cell/` = the -per-cell DU library). - -## The contract - -Pure caller-side logic — the device RX loops are untouched and `GetRxQuality` -stays the radio-wide diagnostic. Everything needed is already per-frame in the -`Packet` callback: - -- **key** — the transmitter address (802.11 addr2/TA), extracted by - `cell::extract_ta`: bytes [10..16) of the MPDU for every frame type except - the two control subtypes that end at addr1 (CTS, ACK). -- **values** — `rx_pkt_attrib`'s path-A RSSI/SNR/EVM plus the hardware RX - timestamp `tsfl`, folded with the exact `RxQualityAccumulator` conventions - (`rssi_raw <= 0` is not a sample; SNR/EVM folded only when present; passive - noise floor = `(rssi_raw − 110) − snr_raw/2`). - -`add()` (or `add_mpdu()`, which extracts the TA itself) per frame; -`snapshot()` drains the whole table into one `UeRxWindow` per TA (delta -semantics, like `GetRxQuality`) with converted units, window mean/extremes and -`last_tsfl` for staleness. The table is bounded (default 64 TAs per window); -overflow frames are counted in `evicted_frames`, never silently lost. - -## Measured (bench) - -`tests/ue_rx_attribution_check.sh`: two transmitters with distinct unicast SAs -(8812AU at a 2 ms inter-frame gap, 8822CU at 8 ms) against one `ue_rx_probe` -witness (8812BU), 12 s. The probe attributed the streams separately — -TX1 2955 frames at −51 dBm mean, TX2 741 frames at −44 dBm mean, a 4.0× -count ratio exactly matching the 4× cadence ratio — confirming per-UE frame -counts and per-UE signal statistics don't bleed between transmitters. - -## Tooling - -- `tests/ue_rx_probe.cpp` — on-air probe: feeds every decoded frame into a - `UeRxAttribution`, drains once a second, emits one `ue.rx` JSONL event per - UE per window (`ta`, `frames`, `rssi_dbm`, `rssi_max_dbm`, `snr_db`, - `snr_min_db`, `evm_db`, `nf_dbm`, `last_tsfl`) plus `ue.rx.evicted` when the - cap was hit. Build line in the header. -- `tests/ue_rx_attribution_selftest.cpp` — headless ctest guard - (`ue_rx_attribution_derive`): TA extraction over frame types, folding - conventions, drain semantics, eviction accounting. -- `tests/ue_rx_attribution_check.sh` — the two-TX on-air validation above. - -M1 wraps this into `UeRegistry` (association state, timing advance, and the -per-UE RX window as the link-adaptation input). diff --git a/docs/wfb-ng-tuning.md b/docs/wfb-ng-tuning.md index 09e13dfd..a62bc2db 100644 --- a/docs/wfb-ng-tuning.md +++ b/docs/wfb-ng-tuning.md @@ -37,8 +37,7 @@ airtime / chip TX, not the USB transport. `sudo ./dkms-install.sh`). It is the wfb-ng injection-tuned driver. Set `rtw_tx_pwr_idx_override` 30–45 (≤63; higher needs active cooling). The in-tree rtw88 driver's monitor injection is much slower (~6 Mbps) — use svpcom - for wfb-ng. It builds on modern host kernels (6.18 here) as well as the pinned - 5.15. + for wfb-ng. It builds on modern host kernels as well as the pinned 5.15. - **Throughput levers** (`/etc/wifibroadcast.cfg`, or `wfb_tx -M/-B/-G/-S/-L`): - `mcs_index` — the primary lever (MCS1 ≈ 7 Mbps → MCS5–7 + 40 MHz ≈ 36–52 Mbps). - `bandwidth = 40` — ~doubles capacity. @@ -58,8 +57,8 @@ undercounts a fast transmitter. `tests/sdr_duty.py` measures the fraction of tim the (clean) channel's received power is above the idle noise floor = the transmitter's airtime occupancy (duty cycle), which has no such ceiling: `on_air_Mbps ≈ duty × PHY_rate(MCS, BW, GI)`. Calibrate the idle noise floor once -(`--noise-db`, ≈ −62 dB here) — a percentile auto-floor mis-reads once the channel -is ~saturated because the low tail becomes signal. +(`--noise-db`; ≈ −62 dB is a typical quiet-bench value) — a percentile auto-floor +mis-reads once the channel is ~saturated because the low tail becomes signal. ## Reproduce @@ -73,5 +72,6 @@ git clone https://github.com/svpcom/wfb-ng && cd wfb-ng && make sudo python3 tests/sdr_duty.py --freq 5745e6 --secs 4 --mcs 7 --bw 20 --noise-db -62 ``` -Hardware here: RTL8812AU `0bda:8812`, USRP B210 `2500:0020`, libvirt VM -`devourer-testrig` (kernel 5.15) for the passthrough comparison. +Measured with an RTL8812AU (`0bda:8812`) and a USRP B210; the bare-metal-vs-VM +comparison ran the same driver inside a libvirt VM (kernel 5.15) over qemu-xhci +USB passthrough. diff --git a/src/IRtlDevice.h b/src/IRtlDevice.h index c6e5c790..73d29887 100644 --- a/src/IRtlDevice.h +++ b/src/IRtlDevice.h @@ -300,7 +300,7 @@ class IRtlDevice { * so the cost bound is the steer's: at most one skipped beacon per update * while the valid latch re-arms, and the swap is NOT atomic versus TBTT (a * beacon airing during the download may still carry the previous content). - * Measured per-generation skip/latency numbers: docs/beacon-grant.md. */ + * Measured per-generation skip/latency numbers: docs/scheduled-mac.md. */ virtual bool UpdateBeaconPayload(const uint8_t *beacon, size_t len) { (void)beacon; (void)len; return false; diff --git a/tests/README.md b/tests/README.md index bc5026aa..432f5d7b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -160,7 +160,7 @@ per-cell stdout/stderr logs end up at `/tmp/devourer-regress-last/`. - `--tx-pid 0xNNNN` / `--rx-pid 0xNNNN` — pick specific DUTs (defaults to the first two auto-detected) - `--no-baseline-abort` — run all 4 cells even if kernel-kernel fails - (useful when one chipset has no working kernel driver on this rig) + (useful when one chipset has no working kernel driver on the host) - `--no-rf-reset` — skip the per-cell USB port-level authorize-cycle. Default behaviour is to deauthorize / reauthorize each DUT's USB port before every cell, forcing a true chip-power cycle. Required because diff --git a/tests/beacon_update_check.sh b/tests/beacon_update_check.sh index 28ac5785..105ee6a1 100644 --- a/tests/beacon_update_check.sh +++ b/tests/beacon_update_check.sh @@ -4,7 +4,7 @@ # grant-map beacon (beacon_update_probe), watch it with a witness on a second # adapter, swap the content N times, and run beacon_update_analyze.py — one # bcn.verdict JSONL row per generation (skips/update, update→air latency, -# torn/regression atomicity). Results table: docs/beacon-grant.md. +# torn/regression atomicity). Results table: docs/scheduled-mac.md. # # bash tests/beacon_update_check.sh # CELLS="j2-8812bu:0x2357:0x012d" N_UPDATES=50 bash tests/beacon_update_check.sh diff --git a/tests/dl_departure_matrix.sh b/tests/dl_departure_matrix.sh index 014c3b15..7faf3efd 100644 --- a/tests/dl_departure_matrix.sh +++ b/tests/dl_departure_matrix.sh @@ -6,7 +6,7 @@ # tx_tsf (ReadTsf) + host_ns (steady_clock immediately before send_packet); the # witness records them with its hardware rx_tsfl, and txegress_analyze.py's # host-clock fit turns the residual tail into the per-transport guard contract -# (one txeg.verdict JSONL line per cell). Results table: docs/dl-departure.md. +# (one txeg.verdict JSONL line per cell). Results table: docs/scheduled-mac.md. # # sudo bash tests/dl_departure_matrix.sh # SECS=60 SLOT_US=1000 sudo bash tests/dl_departure_matrix.sh