This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Userspace re-implementation of Realtek's RTL88xxAU Wi-Fi driver — speaks to the chip
directly via libusb instead of a kernel module. Static library WiFiDriver + two
demo binaries (WiFiDriverDemo for RX, WiFiDriverTxDemo for TX). Used by the
OpenIPC project for long-range video links.
Targets the Realtek "Jaguar" 1st-gen 802.11ac family: RTL8812AU (2T2R,
reference), RTL8811AU (1T1R cut of 8812 silicon — rides the 8812 code
path with RFType=RF_TYPE_1T1R selected via REG_SYS_CFG bit 27),
RTL8814AU (4T4R RF / 3-SS baseband; host-pushed TX requires the
on-chip 3081 MCU, which devourer boots during firmware download —
a failed FW-boot poll means dead TX while RX still works), and
RTL8821AU (1T1R AC + BT combo).
NOT 8821AU's family confusion: it IS Jaguar wave 1 (CHIP_8821 = 7 in Realtek's HalVerDef, shares the enum with CHIP_8812), not Jaguar2 as sometimes claimed. The Jaguar2 (8812BU, 8822BU/BE, ...) and Kestrel (11ax) families are out of scope — they share the "AU"/"BU" branding but the baseband and HAL differ enough to need their own driver.
cmake -S . -B build
cmake --build build -jlibusb-1.0 is required: pkg-config on Linux/macOS, vcpkg on Windows
(VCPKG_ROOT must be set so the toolchain file resolves). CI matrix builds
across GCC/Clang/MSVC on Ubuntu/macOS/Windows, plus a separate build-mingw
job (mingw-w64 via MSYS2, libusb from pkg-config) covering the Windows-GCC
toolchain the MSVC matrix cell doesn't
(.github/workflows/cmake-multi-platform.yml). ctest runs in every CI job;
the one registered test (stream_stdin_binary) round-trips the stream demos'
binary-stdin framing (txdemo/stream_stdin.h) headlessly, so a Windows
text-mode regression fails CI instead of only surfacing on a radio. Hardware
regression testing happens out-of-band via tests/regress.py.
tests/regress.py runs a 2x2 TX/RX matrix (devourer vs. kernel driver) using
two USB Wi-Fi adapters plugged into the host. Run after building devourer.
# Local mode — kernel cells use whatever driver is bound on the host
sudo python3 tests/regress.py
# VM mode (recommended for RTL8814AU and other chips whose kernel driver
# doesn't build on bleeding-edge kernels) — kernel cells run inside a
# pinned-kernel (Ubuntu 22.04 / 5.15) libvirt VM with aircrack-ng/rtl8812au
# preloaded. Provision once with tests/setup_vm.sh, then:
sudo python3 tests/regress.py \
--vm-name devourer-testrig --vm-ssh <user>@<VM-IP>Default channel is 6 (2.4GHz). Pass --channel 36 / --channel 100
to exercise 5GHz; do not assume a single-band matrix is comprehensive.
Matrix-interpretation caveat: with the 8814 as TX, the kernel-TX cells
read 0 at every channel — aircrack-ng/88XXau host-push beacon
injection (what inject_beacon.py does) doesn't emit on that driver,
even though its probe-request injection does. Judge 8814 TX by the
devourer-TX cells.
Three specialised modes layered on top of the default 4-cell matrix:
--full-matrix: iterates every ordered (TX, RX) pair of plugged DUTs across all four driver-side combinations (N×(N−1)×4 cells; ~16 min for N=3 in VM mode). Use for cross-chipset interop regressions.--encoding-matrix --tx-pid 0xNNNN --rx-pid 0xNNNN: for one ordered pair, iterates (driver-mode × radiotap encoding combo) — 4 HT combos (BCC / LDPC / STBC=1 / LDPC+STBC) + 2 VHT combos (BCC / LDPC), 24 cells total.--sniffer-iface IFACE: when set, captures on a 3rd monitor-mode adapter alongside every cell and reports decoded radiotap encoding distribution. Intended for AR9271 — vanilla radiotap, no driver-side filtering. NB:aircrack-ng/88XXauon the kernel-TX side strips the radiotap LDPC + STBC bits before air, so thek/kandk/drows of--encoding-matrixare NOT authoritative for LDPC/STBC asymmetries (MCS index + HT/VHT distinction do survive).
Init/startup-time benchmarking lives in tests/bench_init.py: per-DUT
cold-init timing of devourer RX (exec → first RX frame), devourer TX
(exec → first bulk-OUT), and — with --vm-name/--vm-ssh — the kernel
driver (virsh attach → monitor up → first frame). Per-stage numbers come
from the init-timing: <scope>.<stage> = N ms lines the library emits
(src/InitTimer.h); A/B variants isolate libusb log level, USB reset, and
the TX-power loop.
DUTs are routed between host and VM per cell via virsh attach-device.
Re-run with --keep-logs to inspect per-cell logs (and per-cell pcaps
when --sniffer-iface is active) at /tmp/devourer-regress-last/. See
tests/README.md for the full matrix semantics and prerequisites.
Both WiFiDriverDemo and WiFiDriverTxDemo honour:
DEVOURER_PID=0xNNNN— restrict the open loop to a single PID (e.g.0x8813for RTL8814AU); without it, the demo iterates every Realtek PID.DEVOURER_VID=0xNNNN— override VID (default0x0bda); needed for OEM-rebadged dongles like the TP-Link Archer T2U Plus (2357:0120).DEVOURER_CHANNEL=N— override monitor channel.DEVOURER_SKIP_RESET=1— skiplibusb_reset_devicebefore claim; useful when picking up a chip whose firmware is already running.DEVOURER_SKIP_TXPWR=1— skip the per-rate TX-power loop during channel switch (runs by default on every chip; escape hatch for RX-only experiments).DEVOURER_USB_DEBUG=1— raise libusb log level from the default WARNING to DEBUG (produces ~7 MB per 15 s — has filled/tmpmid-capture and adds 0.5-0.8 s to init even with stderr discarded).DEVOURER_USB_QUIETis accepted as a no-op for backwards compatibility.DEVOURER_THERMAL_POLL_MS=N— emit periodic<devourer-thermal>lines from the chip thermal meter (RF[A][0x42][15:10]) paired with the EFUSE baseline:raw(0..63 thermal units, ~1.5-2 °C each, not absolute °C),baseline,delta = raw − baseline, and a coarsestatusbucket (cool/warm/hot/critical, keyed off delta — the meter has no calibrated °C, so this is deliberately bucketed rather than a fake temperature). Works on every Jaguar chip; read-only (does not alter TX-power tracking). 0/unset = disabled. InWiFiDriverDemo(RX) this spawns a background poller at the given cadence; inWiFiDriverTxDemoit is read inline on the TX thread (no extra USB contention) everyN/2frames. Jaguar-1 has no hard thermal TX shutdown — a risingdeltais the early warning that the PA is heating and TX power is being backed off. NB: on the 8814 the EFUSE baseline is read at the 8812 offset, so the absolutedeltamay be off there; the raw trend is still valid.DEVOURER_THERMAL_WARN_DELTA=N— thermal-units-above-baseline threshold at which a one-shotwarnfires (default15); re-arms once the chip cools back below it.
WiFiDriverTxDemo additionally honours radiotap-encoding knobs that
patch the beacon's MCS info field (or, with _VHT=1, replace it with a
VHT info field) before the bulk-OUT loop:
DEVOURER_TX_MCS=N— HT MCS index (0..31). Default 1.DEVOURER_TX_LDPC=1— set FEC type LDPC (vs default BCC).DEVOURER_TX_STBC=N— STBC stream count (0..3). Default 0.DEVOURER_TX_BW=20|40|80|160— HT honours 20/40; VHT honours 20-160.DEVOURER_TX_VHT=1— switch from HT MCS field (13-byte radiotap) to VHT info field (22-byte radiotap). Exposes:DEVOURER_TX_VHT_MCS=N— VHT MCS index (0..9 typical).DEVOURER_TX_VHT_NSS=N— VHT spatial streams.
_LDPC / _STBC / _BW apply to whichever (HT/VHT) mode is active.
WiFiDriverTxDemo also honours a TX-gain ramp + duty knob for thermal /
TX-power characterisation (drives RtlJaguarDevice::SetTxPowerOverride +
ApplyTxPower):
DEVOURER_TX_PWR_START=N— force an absolute per-rate TXAGC index (0..63), bypassing the EFUSE per-rate table. Unset = normal EFUSE-driven power.DEVOURER_TX_PWR_STOP=N/DEVOURER_TX_PWR_STEP=N/DEVOURER_TX_PWR_STEP_MS=N— step the override from START up to STOP by STEP every STEP_MS, in one uninterrupted TX session, emitting a<devourer-txpwr>index=Nmarker per step. The override only moves on-air power for OFDM/HT/VHT rates — drive HT withDEVOURER_TX_HT_MCS=1(CCK at the 1M default tracks the index in-register but the SDR-measured swing is dominated by the CCK path).DEVOURER_TX_GAP_US=N— inter-frame gap in microseconds (default 2000, ~500 fps).0= back-to-back for maximum TX duty (heating experiments).DEVOURER_TX_PWR_READBACK=1— after each override apply, print<devourer-txpwr-rb>with the read-back TXAGC registers (0xc20 CCK 1M / 0xc24 OFDM 6M) to confirm the write landed.
The reusable experiment harness lives in tests/thermal_gain_sweep.py
(orchestrator), tests/sdr_power_probe.py (USRP receive-power ground truth),
and tests/run_thermal_gain_sweep.sh (build + uv venv + sudo run).
The caller owns libusb. WiFiDriver::CreateRtlDevice 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. demo/main.cpp is the canonical boilerplate.
Chip identity is resolved at construction from SYS_CFG bits + USB PID;
RtlJaguarDevice (the orchestrator) then drives bring-up, RX, and TX
through chip-family branches. The class name was previously Rtl8812aDevice
— a deprecated alias still exists for one release cycle.
Module layout in src/:
RtlJaguarDevice— top-level orchestrator (Init / InitWrite / send_packet).HalModule— chip bring-up, power sequencing, BB/AGC/RF table application, USB EP priority, FIFO/page-boundary init. Most of the chip-family-specific divergence lives here (_8812A,_8814A,_8821Asuffixed methods).RadioManagementModule— channel, bandwidth, TX power; supports up to 4 RF paths for 8814.EepromManager— EFUSE / EEPROM read + autoload state; supplies thecut_version/rfe_typeused byPhyTableLoader::CheckPositive.FirmwareManager— chip-specific FW download (FirmwareDownload_8814Auses the 3081-DDMA path; 8812/8821 use the page-write path).PhyTableLoader— runtime walker for Realtek's phydm-format register tables (opcode-encoded conditional blocks). Replicates upstream phydm'scheck_positive+ state machine without pulling in phydm itself.RtlUsbAdapter— libusb wrapper (vendor control + bulk transfers).FrameParser— RX parsing and TX descriptor layout (SET_TX_DESC_*_8812macros are shared across the Jaguar family).Radiotap.c— radiotap header iterator. TX buffers passed tosend_packetmust begin with a radiotap header; rate / MCS / VHT / STBC / LDPC / SGI / bandwidth are read from it.
hal/ holds vendor headers and tables ported from Realtek's tree. The
8814-specific BB/AGC/RF tables under hal/phydm/rtl8814a/Hal8814_PhyTables.{c,h}
are generated from the upstream aircrack-ng/rtl8814au source by
tools/extract_8814a_phy_tables.py — edit the generator, not the output.
The runtime parser for these tables is src/PhyTableLoader, not the upstream
phydm parser.
- ZeroCD trap: some Realtek dongles enumerate first as a USB
mass-storage device (
0bda:1a2bis the canonical offender) exposing the Windows installer, then re-enumerate as the NIC after a mode switch. Iflibusb_open_device_with_vid_pidreturns NULL, checklsusb— may needusb_modeswitchfirst. - rmmod/sysfs-unbind actively de-inits the chip (RF off, MAC DMA off).
After detaching a kernel driver, expect to re-init from cold, not warm.
DEVOURER_SKIP_RESET=1only helps when firmware state is still intact.
auto logger = std::make_shared<Logger>();
WiFiDriver driver(logger);
auto dev = driver.CreateRtlDevice(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.11The canonical test beacon (txdemo/main.cpp) uses SA 57:42:75:05:d6:00 —
the same constant is hardcoded into demo/main.cpp as the <devourer-tx-hit>
matcher and into tests/regress.py (CANONICAL_SA). Change all three
together if it ever moves.