Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 25 additions & 19 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ construction from the `SYS_CFG2` chip-id (Kestrel: PID-first):
20/40 MHz on 2.4/5 GHz, plus long-preamble CCK on 2.4 GHz at 20 MHz, plus
10 MHz narrowband (5 MHz refused — `src/rtl8733b/CLAUDE.md`).
Everything the backend has not ported (TSF/beacons, A-MPDU, CCX/`tx.report`,
the flat-index and per-rate TX-power knobs) falls through to `IRtlDevice`'s
the flat-index and per-rate TX-power knobs) falls through to `IRadio`/`IRtlRadio`'s
not-ported defaults rather than being faked, so read the base class before
assuming a cross-generation feature below applies here. `FastRetune` IS
ported (intra-band, TSSI kept live — `src/rtl8733b/CLAUDE.md`). SGI, LDPC, STBC, VHT
Expand All @@ -79,12 +79,12 @@ PCIe sibling of the 8821CU — rides the same Jaguar2 HAL through a vfio-pci
transport (`src/PcieTransport.{h,cpp}`: BAR2 MMIO registers over the same
0x0000..0xFFFF space the USB vendor-control path addresses, 88xx
buffer-descriptor DMA rings for TX/RX). USB and PCIe are independent
transports behind `devourer::IRtlTransport` (`src/RtlTransport.h`); the
transports behind `devourer::ITransport` (`src/Transport.h`); the
bus-neutral `RtlAdapter` the HALs hold forwards to whichever it was built
with. The few genuinely bus-specific bring-up steps gate on `is_usb()` (PCIe
power-seq rows, PQ map, no USB RX-agg, no DLFW 512-pad) or ride `hci_setup()`
(pre-power TRX ring programming, no-op on USB). Factory:
`WiFiDriver::CreateRtlDevicePcie(PcieTransport::Open(bdf, logger))` — the
`WiFiDriver::CreateRadioPcie(PcieTransport::Open(bdf, logger))` — the
caller owns vfio like it owns libusb. Demos: `DEVOURER_PCIE_BDF=0000:01:00.0`
on rxdemo and txdemo; `pcieprobe <bdf>` validates the layers bottom-up.
Bind/restore: `tests/pcie_vfio_bind.sh` — driver_override, **not** new_id,
Expand Down Expand Up @@ -173,7 +173,8 @@ second back-to-back `sdr_duty` read can fail to reacquire and report ~0).

Suspect a DUT itself (deaf with a green init, chronic FW-boot fails):
`build/doctor` grades adapter health — EFUSE read-stability ×N, fw-boot,
RX smoke → HEALTHY/SUSPECT/FAILING in the exit code;
RX smoke → HEALTHY/SUSPECT/FAILING in the exit code (EFUSE stability is
`IRtlRadio`-only; the other legs are `IRadio`);
`tests/adapter_doctor_cold.sh` wraps it in per-rep VBUS cold + a vouched
flood for a definitive verdict (`docs/adapter-doctor.md`). Two cold-init
traps it encodes: the in-tree rtw88 modules auto-probe (and fw-download
Expand Down Expand Up @@ -207,11 +208,11 @@ dumps (kernel cross-validation format).
**The library reads no environment.** Construction-time knobs live in
`devourer::DeviceConfig` (`src/DeviceConfig.h` — rx / tx / bf / tuning / debug /
usb sections, every field doc-tagged with its env-var spelling and value
grammar), passed as `CreateRtlDevice`'s defaulted fourth argument. Mid-session
knobs are runtime setters on `IRtlDevice` (`SetTxMode`, `SetTxPowerOffsetQdb`,
grammar), passed as `CreateRadio`'s defaulted fourth argument. Mid-session
knobs are runtime setters on `IRadio` (`SetTxMode`, `SetTxPowerOffsetQdb`,
`SetTxPowerIndexOverride`, `SetRxPathMask`, `SetCcaMode`, `FastRetune`, ...).

**Adapter capabilities**: `IRtlDevice::GetAdapterCaps()` (`src/AdapterCaps.h`)
**Adapter capabilities**: `IRadio::GetAdapterCaps()` (`src/AdapterCaps.h`)
aggregates chip identity, chain counts, the composed `GetTxCaps` +
`GetTxPowerCaps`, channel widths, per-band tunable + characterized frequency
spans, and feature flags — resolved at construction, thread-safe, callable
Expand Down Expand Up @@ -270,7 +271,7 @@ those are the ones listed below.
- `DEVOURER_USB_DEBUG=1` — libusb DEBUG log level (~7 MB / 15 s, has filled
`/tmp` mid-capture; adds 0.5–0.8 s to init).
- `DEVOURER_THERMAL_POLL_MS=N` — emit `thermal` events from the RF 0x42 meter,
on every generation (the poller rides `IRtlDevice::GetThermalStatus`).
on every generation (the poller rides `IRadio::GetThermalStatus`).
`raw` is 0..63 thermal units (~1.5–2 °C each, **not** absolute °C); `delta`
= raw − EFUSE baseline. **Telemetry only**: the poller emits and warns
(`DEVOURER_THERMAL_WARN_DELTA`, default 15) and never stops RX; no HAL gates
Expand Down Expand Up @@ -388,7 +389,7 @@ temporal layer and injects each at its ladder's rate

## Frequency hopping

`IRtlDevice::FastRetune(channel)` — lean intra-band, same-bandwidth retune on
`IRadio::FastRetune(channel)` — lean intra-band, same-bandwidth retune on
all five generations (RF channel switch only, write-only from a
compose cache); falls back to full `SetMonitorChannel` on a band change.
FHSS-grade on the Jaguar/Kestrel dies: ~0.5–2.5 ms per hop depending on chip.
Expand Down Expand Up @@ -418,7 +419,7 @@ reference, policy thresholds, measured sensing constants and the on-air
harnesses: `src/hopset/CLAUDE.md`. Article + results: `docs/fhss.md`,
`docs/jammer-resilience.md`.

`IRtlDevice::FastSetBandwidth(bw)` is the bandwidth analogue — a lean
`IRadio::FastSetBandwidth(bw)` is the bandwidth analogue — a lean
same-channel toggle between 20 MHz and 5/10 MHz narrowband (baseband re-clock
only; ~0.18 ms on the 8812AU vs ~90 ms for the full `SetMonitorChannel`);
falls back to the full path for a 40/80 MHz endpoint. Validation:
Expand Down Expand Up @@ -498,16 +499,16 @@ sensor; C2H rides the RX path, so J1/J2 TX-only sessions see none (run

## Architecture

**The caller owns libusb.** `WiFiDriver::CreateRtlDevice` is intentionally
**The caller owns libusb.** `WiFiDriver::CreateRadio` is intentionally
thin — `libusb_init`, device open, kernel-driver detach, and
`libusb_claim_interface(handle, 0)` must happen **before** handing the handle
to the factory. `examples/rx/main.cpp` is the canonical boilerplate;
`devourer::claim_interface_then_reset` (src/UsbOpen.h) is the recommended
open path (advisory per-adapter lock before reset).

Owning libusb means owning the **teardown order**: destroy the `IRtlDevice`
Owning libusb means owning the **teardown order**: destroy the `IRadio`
first, then release the interface, close the handle, and only then
`libusb_exit`. The device is what quiesces TX (`IRtlDevice::Stop`, and the
`libusb_exit`. The device is what quiesces TX (`IRadio::Stop`, and the
destructor as a backstop: Jaguar1's async bulk-OUT URBs must be cancelled and
reaped while the context still exists), so tearing libusb down first is a
crash, not a leak — and only under enough TX load to keep URBs outstanding at
Expand All @@ -516,23 +517,28 @@ that order; the transport logs a diagnostic naming this if it is destroyed
with TX still in flight.

**Chip identity is resolved at construction** from the `SYS_CFG2` chip-id +
USB PID. `CreateRtlDevice` returns an `IRtlDevice` (`Init` = bring-up + RX
USB PID. `CreateRadio` returns an `IRadio` (`Init` = bring-up + RX
loop; `InitWrite` = TX bring-up; `StartRxLoop` = blocking RX worker on an
already-up chip, enabling TX+RX on one handle; `send_packet`) and constructs
`RtlJaguarDevice` / `RtlJaguar2Device` / `RtlJaguar3Device` / `RtlKestrelDevice`
/ `Rtl8733bDevice` per backend. `Rtl8812aDevice` is a deprecated alias of
`RtlJaguarDevice`. Optional device methods are **virtual with not-ported
defaults**, not pure virtual — a backend that hasn't ported a feature inherits
`RtlJaguarDevice`. The five Realtek backends derive from `IRtlRadio`
(`src/IRtlRadio.h`), the Realtek-only extension of `IRadio` — the header
carries the member list and the downcast contract. Optional device methods
are **virtual with not-ported defaults**, not pure virtual — a backend that
hasn't ported a feature inherits
`false`/`0`/a full-path fallback rather than a fake. Check the override list in
the backend's header before believing a cross-generation claim.

Generation-agnostic core in `src/` (always compiled; depends on no HAL):

- `WiFiDriver` — the factory (`CreateRtlDevice`).
- `IRadio` (`src/IRadio.h`) — the vendor-neutral radio contract every backend
implements; `WiFiDriver::CreateRadio` returns one.
- `WiFiDriver` — the factory (`CreateRadio`).
- `DeviceConfig.h` — construction-time configuration struct; every component
copies the sub-struct it consumes at construction.
- `RtlAdapter` — the bus-neutral register/frame accessor; a copyable value
type shared by every component, forwarding to the `IRtlTransport` it was
type shared by every component, forwarding to the `ITransport` it was
built with (`UsbTransport` = libusb vendor control + bulk; `PcieTransport` =
BAR2 MMIO + DMA rings). `RtlUsbAdapter` is a deprecated alias.
- `Radiotap.c` — radiotap iterator. TX buffers passed to `send_packet` **must**
Expand Down Expand Up @@ -616,7 +622,7 @@ byte-for-byte; prefer that shape when adding one.
```cpp
auto logger = std::make_shared<Logger>();
WiFiDriver driver(logger);
auto dev = driver.CreateRtlDevice(handle); // handle is already claimed
auto dev = driver.CreateRadio(handle); // handle is already claimed
dev->InitWrite(SelectedChannel{ .Channel = 36, .ChannelOffset = 0,
.ChannelWidth = CHANNEL_WIDTH_20 });
dev->send_packet(buffer, len); // buffer[0..] = radiotap header, then 802.11
Expand Down
22 changes: 17 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ add_library(devourer
src/Radiotap.c
src/RadiotapBuilder.cpp
src/RadiotapBuilder.h
src/RtlTransport.h
src/Transport.h
src/RtlAdapter.cpp
src/RtlAdapter.h
src/RtlUsbAdapter.h # compat alias shim (RtlUsbAdapter = RtlAdapter)
Expand Down Expand Up @@ -224,7 +224,8 @@ add_library(devourer
src/hopset/HopsetSense.h
src/hopset/HopsetFusion.h
src/hopset/HopsetEvents.h
src/IRtlDevice.h
src/IRadio.h
src/IRtlRadio.h
src/SignalStop.cpp
src/SignalStop.h
src/WiFiDriver.cpp
Expand Down Expand Up @@ -567,7 +568,7 @@ endif()
# rtl8733bprobe — staged USB bring-up prober for the RTL8733B (HALMAC 87xx),
# the pcieprobe/kestrelprobe sibling: identity -> power-on + EFUSE -> firmware
# -> MAC/PHY -> TSSI audit stages, used alongside normal factory dispatch. It
# never substitutes for the production IRtlDevice path.
# never substitutes for the production IRadio path.
if(DEVOURER_8733B)
add_executable(rtl8733bprobe
examples/rtl8733bprobe/main.cpp
Expand Down Expand Up @@ -676,7 +677,7 @@ target_link_libraries(duplex PUBLIC devourer PRIVATE PkgConfig::libusb)
target_include_directories(duplex PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common)

# txpower — reference consumer of the runtime TX-power API
# (IRtlDevice::SetTxPowerOffsetQdb / SetTxPowerIndexOverride / GetTxPowerState /
# (IRadio::SetTxPowerOffsetQdb / SetTxPowerIndexOverride / GetTxPowerState /
# GetThermalStatus): opens one adapter, steps the knobs from CLI args (no env
# vars), and prints machine-readable state markers. The register-level
# validation driver for tests/txpwr_offset_regcheck.sh and the shape of an
Expand Down Expand Up @@ -775,7 +776,18 @@ target_link_libraries(ToneMaskSelftest PRIVATE devourer)

add_test(NAME tone_mask_math COMMAND ToneMaskSelftest)

# Headless guard for the TX quiesce seam (IRtlTransport::quiesce_tx via
# Headless guard for the radio contract: a radio implementing only IRadio's
# pure-virtual core must compile without any Realtek type, its not-ported
# defaults must hold, and it must not be an IRtlRadio. A Realtek-typed pure
# virtual added to IRadio stops this compiling, which is the point.
add_executable(RadioIfaceSelftest
tests/radio_iface_selftest.cpp
)
target_link_libraries(RadioIfaceSelftest PRIVATE devourer)

add_test(NAME radio_iface COMMAND RadioIfaceSelftest)

# Headless guard for the TX quiesce seam (ITransport::quiesce_tx via
# RtlAdapter): the explicit "stop TX and wait it out" call every device makes
# before anything is released. UsbTransport's cancel/drain is validated on
# hardware under ASan — the test header says what it does and does not cover.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ the full boilerplate; the minimal RX path is:
```cpp
auto logger = std::make_shared<Logger>();
WiFiDriver driver(logger);
auto dev = driver.CreateRtlDevice(handle); // handle is already claimed
auto dev = driver.CreateRadio(handle); // handle is already claimed
dev->Init(packetProcessor, SelectedChannel{
.Channel = 36,
.ChannelOffset = 0,
Expand All @@ -229,13 +229,13 @@ Construction-time options travel in a `devourer::DeviceConfig`
```cpp
devourer::DeviceConfig cfg;
cfg.rx.keep_corrupted = true; // deliver CRC-failed frames too
auto dev = driver.CreateRtlDevice(handle, ctx, lock, cfg);
auto dev = driver.CreateRadio(handle, ctx, lock, cfg);
```

Anything that changes mid-session is a runtime setter on the device:
`SetTxMode`, `SetTxPowerOffsetQdb`, `SetRxPathMask`, `FastRetune`, ...
The device class is chosen automatically from the chip behind the handle;
one `IRtlDevice` interface covers all five hardware backends.
one `IRadio` interface covers all five hardware backends.

## Going deeper

Expand Down
7 changes: 4 additions & 3 deletions docs/adapter-doctor.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ build/doctor --bus 3 --port 2.3.3 # topology select (two same-PID adapters)

1. **Bring-up** — `InitWrite`; an abort is an immediate FAILING.
2. **EFUSE stability** — N fresh *physical* map reads
(`IRtlDevice::ProbeEfuseStability`), cross-compared byte-for-byte +
(`IRtlRadio::ProbeEfuseStability`), cross-compared byte-for-byte +
EEPROM-ID (0x8129) validated. Any read-to-read mismatch is
conclusive by itself. Not probed on the 8822E — its OTP is not
reliably readable after bring-up by design, so probing would flag
healthy units.
healthy units. Realtek-only (`IRtlRadio`): on another radio doctor
skips this step and grades the remaining legs.
3. **FW boot** — checksum + MCU-ready outcome of the bring-up's
download (`IRtlDevice::GetFwBootStatus`).
download (`IRadio::GetFwBootStatus`).
4. **RX smoke** — FCS-clean frame count over `--listen-secs`. Ambient
traffic counts. Hearing *nothing* is only SUSPECT unless
`--expect-traffic` vouches for a source on the channel — an
Expand Down
6 changes: 3 additions & 3 deletions docs/aggregation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ and A-MPDU unicast into reliable (hardware-ARQ) links.

## USB TX aggregation (`send_packets`)

`IRtlDevice::send_packets(TxPacketView*, n)` + `DeviceConfig tx.usb_agg_max`
`IRadio::send_packets(TxPacketView*, n)` + `DeviceConfig tx.usb_agg_max`
(env `DEVOURER_TX_USB_AGG`, default 0 = off → per-frame loop, byte-identical
descriptors). Packing rules live in `src/TxAggPlan.h` (pure math, ctest'd):
blocks 8-byte aligned, the FIRST descriptor carries the block count
Expand Down Expand Up @@ -90,7 +90,7 @@ coverage a reliability layer can count on — is in

## A-MPDU (`SetAmpduMode`)

`IRtlDevice::SetAmpduMode(AmpduMode)` / `ClearAmpduMode()` / `GetAmpduMode()`
`IRadio::SetAmpduMode(AmpduMode)` / `ClearAmpduMode()` / `GetAmpduMode()`
(env `DEVOURER_TX_AMPDU_MODE="tid/maxnum[/density[/noack[/maxtime_hex]]]"`,
`src/AmpduMode.h`, all generations) configure A-MPDU TX in one call: it marks
every data frame aggregatable (data QSEL + AGG_EN + MAX_AGG_NUM +
Expand Down Expand Up @@ -187,7 +187,7 @@ numbers above came from.

## Hardware ACK/BlockAck responder — reliable unicast

`IRtlDevice::SetAckResponder(mac)` / `ClearAckResponder()` (env
`IRadio::SetAckResponder(mac)` / `ClearAckResponder()` (env
`DEVOURER_ACK_RESPONDER=<unicast mac>`, all generations; `src/AckResponder.h`)
arms the MAC's autonomous ACK engine while monitor RX/injection continue
unchanged: port identity (MACID/BSSID 0x610/0x618 = `mac`) + net_type (0x102
Expand Down
2 changes: 1 addition & 1 deletion docs/beamforming-self-sounding.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ DEVOURER_PID=0x8813 DEVOURER_CHANNEL=100 DEVOURER_BF_DETECT_REPORT=4 \
# single-radio beamformer: the report is addressed TO the sounder, so one
# adapter can sound and capture its own reports — DEVOURER_TX_WITH_RX=thread
# runs the RX worker loop on a thread next to the TX loop (one bring-up, one
# claimed handle; see StartRxLoop in IRtlDevice). Hardware-validated on the
# claimed handle; see StartRxLoop in IRadio). Hardware-validated on the
# 8814AU (Jaguar-1), the 8822BU (Jaguar-2) and both Jaguar-3 variants
# (8822CU / 8822EU) — 50k+ self-captured reports per 20 s at full sounding
# rate. On Jaguar-2/3, DEVOURER_BF_ARM_SOUNDER takes the sounder MAC
Expand Down
5 changes: 3 additions & 2 deletions docs/driver-primer.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ access is a vendor **control transfer**: the host submits a **URB** (USB request
block) to the kernel's USB stack, the request crosses the wire with the register
address packed into its setup fields, and the chip answers with the data. That's
the whole trick — `lsusb`-level plumbing, no kernel driver magic. devourer's
`RtlAdapter` (`src/RtlTransport.h`) is nothing but this, and the vendor equivalent
`RtlAdapter` (`src/RtlAdapter.h`, over `devourer::UsbTransport` in
`src/UsbTransport.h`) is nothing but this, and the vendor equivalent
is `usb_ops_linux.c` in each tree.

The address space is a map you'll internalize fast: the MAC's registers occupy the
Expand Down Expand Up @@ -442,7 +443,7 @@ find each in the vendor trees and in devourer.
| PA / LNA | power amp (TX) / low-noise amp (RX) front end | 1 | board-level; selected by RFE type |
| WCPU | the MAC's embedded CPU running firmware | 1,5 | booted by FWDL |
| A-die / D-die | analog / digital silicon dies in one package | 1,9 | reached via DAV / DDV |
| URB | USB request block — one queued USB transfer | 2 | `usb_ops_linux.c` / `src/RtlTransport.h` |
| URB | USB request block — one queued USB transfer | 2 | `usb_ops_linux.c` / `src/UsbTransport.h` |
| chip id | which die design; drives HAL dispatch | 3 | `SYS_CFG` / `WiFiDriver` factory |
| cut | silicon revision (A/B/C…) | 3 | gates tables + firmware images |
| RFE (type) | board's RF front-end wiring variant, from efuse | 3 | gates PHY table blocks |
Expand Down
2 changes: 1 addition & 1 deletion docs/fhss.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ without stalling the data stream is the engineering problem.

## The hop engine: a lean retune

`IRtlDevice::FastRetune(channel, cache_rf)` is the generation-agnostic entry
`IRadio::FastRetune(channel, cache_rf)` is the generation-agnostic entry
point. The default implementation is the full `SetMonitorChannel`; every chip
family overrides it with a stripped-down path. The vendor's channel-set routine
does a great deal that does not change between two channels of the same band and
Expand Down
4 changes: 2 additions & 2 deletions docs/frequency-hopping.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ constants once); every subsequent same-band hop is ~1.5 ms.

## The ports: all three generations

`IRtlDevice::FastRetune(channel, cache_rf)` is the generation-agnostic entry
`IRadio::FastRetune(channel, cache_rf)` is the generation-agnostic entry
point (default = the full `SetMonitorChannel` at the current width/offset), and
every generation overrides it with a lean path built from the tricks above:

Expand Down Expand Up @@ -481,7 +481,7 @@ implies.

## The bandwidth analogue: FastSetBandwidth

The same method applies to a *bandwidth* switch. `IRtlDevice::FastSetBandwidth(bw)`
The same method applies to a *bandwidth* switch. `IRadio::FastSetBandwidth(bw)`
is a lean same-channel toggle between 20 MHz and 5/10 MHz narrowband — and it
collapses even harder than a hop, because narrowband keeps the RF in 20 MHz mode
(so the RF bandwidth register, MAC BW, sub-channel, TX power, and IQK are all
Expand Down
4 changes: 2 additions & 2 deletions docs/he-trigger-ul.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ responses and data frames:
`TriggerConfig`: frame-control `0x24`, duration, RA/TA, the HE Common Info
field, and one User Info field per granted user (AID, RU allocation, MCS,
spatial streams, target RSSI).
2. `IRtlDevice::SendTrigger()` prepends a radiotap header (legacy OFDM, so any
2. `IRadio::SendTrigger()` prepends a radiotap header (legacy OFDM, so any
monitor decodes it) and calls the normal `send_packet()` transmit path. The
firmware's trigger scheduler is bypassed entirely.

Expand Down Expand Up @@ -91,7 +91,7 @@ client firmware does not air. In short:

## Device API

The surface is on `IRtlDevice`; each call returns `false` on pre-AX generations.
The surface is on `IRadio`; each call returns `false` on pre-AX generations.

- `SendTrigger(const TriggerConfig&)` — air one Basic Trigger. Default path is
host-injection; set `DeviceConfig.debug.kestrel_trigger_f2p` (env
Expand Down
Loading
Loading