From ecca1d7c194ae143d4f8aebbf7348d5c8fbfcd4f Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Sat, 27 Jun 2026 10:07:18 +0300 Subject: [PATCH] tx: programmatic runtime TX-mode API; drop DEVOURER_TX_HT_MCS gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit send_packet only honoured a radiotap HT-MCS index when DEVOURER_TX_HT_MCS was set in the environment, so a valid HT radiotap silently fell back to the MGN_1M (1M CCK / 6M-legacy-at-5G) default. A library deciding TX behaviour from a CLI env var, and an ignored-by-default radiotap, were both wrong. - Radiotap is now authoritative per-packet: the HT-MCS index is honoured unconditionally (gate removed). - New devourer::TxMode struct (src/TxMode.h) + RtlJaguarDevice::SetTxMode / ClearTxMode: a runtime default applied when a frame's radiotap carries no rate, replacing the hardcoded MGN_1M. Mirrors the SetTxPowerOverride pattern. - Consolidate the per-knob DEVOURER_TX_MCS/_VHT/_VHT_MCS/_VHT_NSS/_LDPC/_STBC/_BW env vars into one DEVOURER_TX_RATE string ([/][/SGI][/LDPC][/STBC]), parsed by RadiotapBuilder::parse_tx_mode_env into a TxMode. StreamRateCfg -> TxMode; the txdemo sends a rate-less beacon and drives the mode via SetTxMode. - Migrate regress.py / bench_onair.py / thermal_gain_sweep.py and the docs to DEVOURER_TX_RATE; correct the precoder note about send_packet ignoring HT MCS. Verified: build + ctest green; on air (8812 -> 8814 monitor, ch6) MCS7 decodes as 11n 65 Mb/s MCS 7 and an unset rate as 6 Mb/s 11g — both with no DEVOURER_TX_HT_MCS. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 34 +++--- CMakeLists.txt | 2 + README.md | 18 ++- docs/wfb-ng-tuning.md | 2 +- src/RadiotapBuilder.cpp | 167 ++++++++++++++------------- src/RadiotapBuilder.h | 82 ++++--------- src/RtlJaguarDevice.cpp | 43 +++++-- src/RtlJaguarDevice.h | 14 +++ src/TxMode.cpp | 46 ++++++++ src/TxMode.h | 62 ++++++++++ tests/README.md | 11 +- tests/bench_onair.py | 5 +- tests/regress.py | 34 +++--- tests/thermal_gain_sweep.py | 7 +- tools/precoder/README.md | 16 ++- tools/precoder/encode_subcarriers.py | 11 +- txdemo/main.cpp | 120 +++---------------- txdemo/stream_duplex_demo/main.cpp | 2 +- txdemo/stream_tx_demo/main.cpp | 2 +- 19 files changed, 345 insertions(+), 333 deletions(-) create mode 100644 src/TxMode.cpp create mode 100644 src/TxMode.h diff --git a/CLAUDE.md b/CLAUDE.md index efe305b..c16646b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,20 +131,22 @@ Both `WiFiDriverDemo` and `WiFiDriverTxDemo` honour: which a one-shot `warn` fires (default `15`); 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` selects the on-air TX mode with a single env var that it +parses into a `devourer::TxMode` and hands to `RtlJaguarDevice::SetTxMode` +(the canonical runtime API; the demo sends a rate-less beacon so this mode +applies). The library itself is radiotap-driven — a frame that carries its own +rate radiotap overrides the mode per-packet — so there is **no** `DEVOURER_TX_HT_MCS` +gate any more (an HT-MCS radiotap is honoured unconditionally): + +- `DEVOURER_TX_RATE=[/][/SGI][/LDPC][/STBC]` (case-insensitive). Unset + = `6M` legacy. Examples: `MCS7`, `MCS7/40/SGI`, `VHT2SS_MCS3/80/LDPC`, `54M`. + - ``: `6M`/`9M`/`12M`/`18M`/`24M`/`36M`/`48M`/`54M` (legacy OFDM), + `MCS0`..`MCS31` (HT), or `VHT1SS_MCS0`..`VHT4SS_MCS9` (VHT). + - ``: `20`|`40`|`80`|`160` (default 20; legacy is always 20). + - `SGI` / `LDPC` / `STBC`: optional modifiers. + +(Programmatic equivalent: `dev->SetTxMode(devourer::TxMode{...})`; +`dev->ClearTxMode()` reverts to the built-in default.) `WiFiDriverTxDemo` also honours a TX-gain ramp + duty knob for thermal / TX-power characterisation (drives `RtlJaguarDevice::SetTxPowerOverride` + @@ -156,8 +158,8 @@ TX-power characterisation (drives `RtlJaguarDevice::SetTxPowerOverride` + — step the override from START up to STOP by STEP every STEP_MS, in one uninterrupted TX session, emitting a `index=N` marker per step. The override only moves on-air power for OFDM/HT/VHT rates — drive HT with - `DEVOURER_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_RATE=MCS1` (the CCK path 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 5eb9f39..c668d0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,8 @@ add_library(WiFiDriver src/RtlUsbAdapter.cpp src/RtlUsbAdapter.h src/SelectedChannel.h + src/TxMode.cpp + src/TxMode.h src/WiFiDriver.cpp src/WiFiDriver.h src/registry_priv.h diff --git a/README.md b/README.md index 6edc14e..713a3ac 100644 --- a/README.md +++ b/README.md @@ -132,17 +132,13 @@ Common to both demos: to DEBUG (produces ~7 MB per 15 s, can fill `/tmp` mid-capture, and slows init measurably). `DEVOURER_USB_QUIET` is accepted as a no-op. -`WiFiDriverTxDemo`-only knobs patch the canonical beacon's radiotap -header before the TX loop: - -- `DEVOURER_TX_MCS=N` — HT MCS index. Default 1. -- `DEVOURER_TX_LDPC=1` — FEC type LDPC (vs default BCC). -- `DEVOURER_TX_STBC=N` — STBC stream count (0..3). Default 0. -- `DEVOURER_TX_BW=20|40|80|160` — bandwidth. -- `DEVOURER_TX_VHT=1` — switch from HT MCS field (radiotap bit 19) to - VHT info field (bit 21). Exposes `DEVOURER_TX_VHT_MCS=N` (VHT MCS - index, 0..9 typical) and `DEVOURER_TX_VHT_NSS=N` (spatial streams). - `_LDPC` / `_STBC` / `_BW` apply to whichever (HT/VHT) mode is active. +`WiFiDriverTxDemo`-only knobs: + +- `DEVOURER_TX_RATE=[/][/SGI][/LDPC][/STBC]` — the on-air TX mode, + parsed into a `devourer::TxMode` and applied via `RtlJaguarDevice::SetTxMode`. + `` = `6M`..`54M` (legacy OFDM) | `MCS0`..`MCS31` (HT) | + `VHT1SS_MCS0`..`VHT4SS_MCS9` (VHT); `` = `20|40|80|160`. Unset = `6M`. + Examples: `MCS7`, `MCS7/40/SGI`, `VHT2SS_MCS3/80/LDPC`. - `DEVOURER_TX_PAYLOAD_BYTES=N` — pad the 802.11 PSDU up to `N` bytes (on-wire `N + 40`). For throughput testing — `N=3993` is wfb-ng's max frame payload. diff --git a/docs/wfb-ng-tuning.md b/docs/wfb-ng-tuning.md index f9203fb..1d6403d 100644 --- a/docs/wfb-ng-tuning.md +++ b/docs/wfb-ng-tuning.md @@ -68,7 +68,7 @@ is ~saturated because the low tail becomes signal. git clone https://github.com/svpcom/rtl8812au && cd rtl8812au && make && \ sudo insmod 88XXau_wfb.ko rtw_tx_pwr_idx_override=30 git clone https://github.com/svpcom/wfb-ng && cd wfb-ng && make -# devourer side: build/WiFiDriverTxDemo with DEVOURER_TX_MCS/_BW/_PAYLOAD_BYTES/_GAP_US=0 +# devourer side: build/WiFiDriverTxDemo with DEVOURER_TX_RATE=MCS7/20 + DEVOURER_TX_PAYLOAD_BYTES + DEVOURER_TX_GAP_US=0 # measure (ceiling-free) while either side floods a clean 5 GHz channel: sudo python3 tests/sdr_duty.py --freq 5745e6 --secs 4 --mcs 7 --bw 20 --noise-db -62 ``` diff --git a/src/RadiotapBuilder.cpp b/src/RadiotapBuilder.cpp index f30adfd..a2a0dbc 100644 --- a/src/RadiotapBuilder.cpp +++ b/src/RadiotapBuilder.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace devourer { @@ -31,11 +32,9 @@ void emit_u32_le(std::vector& v, uint32_t x) { v.push_back(static_cast((x >> 24) & 0xFF)); } -std::vector build_legacy(const StreamRateCfg& cfg) { - /* 13-byte legacy-OFDM radiotap. Bit-identical to the historic - * kRadiotapLegacy6M[13] constant except byte 8 (RATE), which the caller - * controls. Length stays 13 so send_packet's vht-detection heuristic - * keeps this on the legacy path. */ +std::vector build_legacy(const TxMode& cfg) { + /* 13-byte legacy-OFDM radiotap. Byte 8 (RATE) carried from cfg; length stays + * 13 so send_packet's vht-detection heuristic keeps this on the legacy path. */ std::vector r; r.reserve(13); emit_u8(r, kRadiotapVersion); @@ -49,17 +48,11 @@ std::vector build_legacy(const StreamRateCfg& cfg) { return r; } -std::vector build_ht(const StreamRateCfg& cfg) { - /* 13-byte HT radiotap: presence = TX_FLAGS | MCS, no RATE field. Total - * header length must remain 13 so send_packet stays on rate_id=8 (HT, - * not VHT). Verified against txdemo/main.cpp's beacon_frame[] HT layout - * at line 300 — same it_present, same MCS field positions. +std::vector build_ht(const TxMode& cfg) { + /* 13-byte HT radiotap: presence = TX_FLAGS | MCS, no RATE field. Length stays + * 13 so send_packet keeps rate_id=8 (HT, not VHT). * - * MCS field layout (3 bytes after TX_FLAGS): - * byte 0: known mask - * byte 1: flags (BW / SGI / FEC / STBC) - * byte 2: MCS index 0..31 - */ + * MCS field layout (3 bytes after TX_FLAGS): known mask, flags, MCS index. */ constexpr uint8_t kKnownBw = 1u << 0; constexpr uint8_t kKnownMcs = 1u << 1; constexpr uint8_t kKnownGi = 1u << 2; @@ -68,7 +61,7 @@ std::vector build_ht(const StreamRateCfg& cfg) { uint8_t known = kKnownMcs | kKnownGi | kKnownFec | kKnownBw | kKnownStbc; uint8_t flags = 0; - /* BW: 0=20, 1=40. (20L/20U upper/lower not exposed.) */ + /* BW: 0=20, 1=40. */ if (cfg.bw_mhz >= 40) flags |= 0x01; if (cfg.sgi) flags |= 0x04; if (cfg.ldpc) flags |= 0x10; @@ -87,11 +80,9 @@ std::vector build_ht(const StreamRateCfg& cfg) { return r; } -std::vector build_vht(const StreamRateCfg& cfg) { - /* 22-byte VHT radiotap. Identical layout to txdemo/main.cpp's - * DEVOURER_TX_VHT=1 path (lines 374-409) — header(8) + TX_FLAGS(2) + - * VHT info(12). Length > 13 triggers send_packet's vht=true branch - * (rate_id=9), which is required for the VHT info field to be honoured. */ +std::vector build_vht(const TxMode& cfg) { + /* 22-byte VHT radiotap: header(8) + TX_FLAGS(2) + VHT info(12). Length > 13 + * triggers send_packet's vht=true branch (rate_id=9). */ uint8_t bw_code; switch (cfg.bw_mhz) { case 40: bw_code = 1; break; @@ -99,8 +90,7 @@ std::vector build_vht(const StreamRateCfg& cfg) { case 160: bw_code = 11; break; default: bw_code = 0; break; } - /* known mask: STBC(0) | GI(2) | BW(6) — matches send_packet's VHT - * decoder at RtlJaguarDevice.cpp:110-138. */ + /* known mask: STBC(0) | GI(2) | BW(6) — matches send_packet's VHT decoder. */ const uint16_t known = (1u << 0) | (1u << 2) | (1u << 6); uint8_t vht_flags = 0; if (cfg.stbc) vht_flags |= 0x01; @@ -152,62 +142,25 @@ bool parse_uint(const std::string& s, size_t pos, unsigned* out) { return true; } -} // namespace - -std::vector build_stream_radiotap(const StreamRateCfg& cfg) { - switch (cfg.mode) { - case StreamRateCfg::Mode::HT: return build_ht(cfg); - case StreamRateCfg::Mode::VHT: return build_vht(cfg); - case StreamRateCfg::Mode::Legacy: - default: return build_legacy(cfg); - } -} - -StreamRateCfg parse_stream_rate_env() { - StreamRateCfg cfg; - - /* Bandwidth (cross-cuts modes). */ - if (const char* bw = std::getenv("DEVOURER_STREAM_BW")) { - int v = std::atoi(bw); - if (v == 20 || v == 40 || v == 80 || v == 160) { - cfg.bw_mhz = static_cast(v); - } - } - cfg.sgi = std::getenv("DEVOURER_STREAM_SGI") != nullptr; - cfg.ldpc = std::getenv("DEVOURER_STREAM_LDPC") != nullptr; - cfg.stbc = std::getenv("DEVOURER_STREAM_STBC") != nullptr; - - const char* raw = std::getenv("DEVOURER_STREAM_RATE"); - if (raw == nullptr || *raw == '\0') { - return cfg; /* defaults: legacy 6M */ - } - const std::string s = to_upper_stripped(raw); - - /* Legacy mnemonics. */ - if (s == "6M") { cfg.legacy_rate_500kbps = 12; return cfg; } - if (s == "9M") { cfg.legacy_rate_500kbps = 18; return cfg; } - if (s == "12M") { cfg.legacy_rate_500kbps = 24; return cfg; } - if (s == "18M") { cfg.legacy_rate_500kbps = 36; return cfg; } - if (s == "24M") { cfg.legacy_rate_500kbps = 48; return cfg; } - if (s == "36M") { cfg.legacy_rate_500kbps = 72; return cfg; } - if (s == "48M") { cfg.legacy_rate_500kbps = 96; return cfg; } - if (s == "54M") { cfg.legacy_rate_500kbps = 108; return cfg; } +/* Parse the rate mnemonic (first '/'-token) into cfg. Returns false if the + * token is not a recognised rate. */ +bool parse_rate_token(const std::string& s, TxMode* cfg) { + if (s == "6M") { cfg->legacy_rate_500kbps = 12; return true; } + if (s == "9M") { cfg->legacy_rate_500kbps = 18; return true; } + if (s == "12M") { cfg->legacy_rate_500kbps = 24; return true; } + if (s == "18M") { cfg->legacy_rate_500kbps = 36; return true; } + if (s == "24M") { cfg->legacy_rate_500kbps = 48; return true; } + if (s == "36M") { cfg->legacy_rate_500kbps = 72; return true; } + if (s == "48M") { cfg->legacy_rate_500kbps = 96; return true; } + if (s == "54M") { cfg->legacy_rate_500kbps = 108; return true; } /* HT: MCS, 0..31. */ if (s.rfind("MCS", 0) == 0) { unsigned mcs; if (parse_uint(s, 3, &mcs) && mcs <= 31) { - cfg.mode = StreamRateCfg::Mode::HT; - cfg.ht_mcs = static_cast(mcs); - if (std::getenv("DEVOURER_TX_HT_MCS") == nullptr) { - std::fprintf( - stderr, - "warning: DEVOURER_STREAM_RATE=MCS%u requires " - "DEVOURER_TX_HT_MCS=1 to actually fly at the requested rate " - "(otherwise send_packet falls back to 1M CCK)\n", - mcs); - } - return cfg; + cfg->mode = TxMode::Mode::HT; + cfg->ht_mcs = static_cast(mcs); + return true; } } @@ -222,19 +175,69 @@ StreamRateCfg parse_stream_rate_env() { if (s.compare(after_nss, tail.size(), tail) == 0) { unsigned mcs; if (parse_uint(s, after_nss + tail.size(), &mcs) && mcs <= 9) { - cfg.mode = StreamRateCfg::Mode::VHT; - cfg.vht_nss = static_cast(nss); - cfg.vht_mcs = static_cast(mcs); - return cfg; + cfg->mode = TxMode::Mode::VHT; + cfg->vht_nss = static_cast(nss); + cfg->vht_mcs = static_cast(mcs); + return true; } } } } + return false; +} + +} // namespace + +std::vector build_stream_radiotap(const TxMode& cfg) { + switch (cfg.mode) { + case TxMode::Mode::HT: return build_ht(cfg); + case TxMode::Mode::VHT: return build_vht(cfg); + case TxMode::Mode::Legacy: + default: return build_legacy(cfg); + } +} + +TxMode parse_tx_mode_env() { + TxMode cfg; /* defaults: legacy 6M, 20 MHz, no SGI/LDPC/STBC */ + + const char* raw = std::getenv("DEVOURER_TX_RATE"); + if (raw == nullptr || *raw == '\0') { + return cfg; + } + const std::string s = to_upper_stripped(raw); + + /* Split on '/': first token = rate, rest = bandwidth (numeric) or modifier + * flags (SGI / LDPC / STBC). */ + std::vector tokens; + size_t start = 0; + while (start <= s.size()) { + size_t slash = s.find('/', start); + if (slash == std::string::npos) { + tokens.push_back(s.substr(start)); + break; + } + tokens.push_back(s.substr(start, slash - start)); + start = slash + 1; + } + if (tokens.empty() || tokens[0].empty() || !parse_rate_token(tokens[0], &cfg)) { + std::fprintf(stderr, + "warning: unrecognised DEVOURER_TX_RATE=%s, " + "falling back to 6M legacy\n", raw); + return TxMode{}; + } - /* Unrecognised — fall back to default 6M legacy. */ - std::fprintf(stderr, - "warning: unrecognised DEVOURER_STREAM_RATE=%s," - " falling back to 6M legacy\n", raw); + for (size_t i = 1; i < tokens.size(); ++i) { + const std::string& t = tokens[i]; + if (t == "SGI") cfg.sgi = true; + else if (t == "LDPC") cfg.ldpc = true; + else if (t == "STBC") cfg.stbc = true; + else if (t == "20" || t == "40" || t == "80" || t == "160") + cfg.bw_mhz = static_cast(std::atoi(t.c_str())); + else if (!t.empty()) + std::fprintf(stderr, + "warning: ignoring unrecognised DEVOURER_TX_RATE " + "token '%s'\n", t.c_str()); + } return cfg; } diff --git a/src/RadiotapBuilder.h b/src/RadiotapBuilder.h index c28fefa..9b44911 100644 --- a/src/RadiotapBuilder.h +++ b/src/RadiotapBuilder.h @@ -1,75 +1,37 @@ -/* Stream-carrier radiotap helper shared between the stream TX binaries. +/* Radiotap-header builder + env parser shared by the TX binaries. * - * The stream demos historically each shipped a private kRadiotapLegacy6M[13] - * constant. This helper switches between three carrier modes — Legacy OFDM, - * HT-MCS, and VHT — under one env var (DEVOURER_STREAM_RATE), so the - * carrier rate becomes a robustness-vs-throughput knob for the precoder - * stream link's FEC layers (PR #86 / #87). + * build_stream_radiotap() turns a devourer::TxMode into a complete, well-formed + * radiotap header (Legacy OFDM / HT-MCS / VHT) that send_packet parses to set + * the on-air rate. parse_tx_mode_env() reads the DEVOURER_TX_RATE env string + * into a TxMode for the demos' command-line interface. * - * Three wire-format facts the chip-side send_packet relies on: - * - * - Length 13 (0x0d) → legacy path. radiotap_length != 0x0d sets vht=true - * in RtlJaguarDevice::send_packet, switching rate_id to 9. - * - The radiotap iterator parses *all* fields regardless of vht — so a - * 13-byte HT-MCS radiotap (it_present=MCS|TX_FLAGS, no RATE) is - * correctly parsed via the IEEE80211_RADIOTAP_MCS case and stays on - * rate_id=8. That's what we want for HT-MCS carriers. - * - VHT needs a 22-byte radiotap (it_present=VHT|TX_FLAGS). Length>13 - * puts us on rate_id=9 — that's what the VHT branch requires. - * - * For HT-MCS to actually set fixed_rate (vs falling back to MGN_1M CCK at - * 1 Mbps), the F1 gate DEVOURER_TX_HT_MCS=1 must also be set on the same - * process. parse_stream_rate_env() logs a warning to stderr if an HT - * rate is parsed without that gate; VHT has no such gate (send_packet's - * VHT branch always sets fixed_rate from the VHT info field). + * Two wire-format facts send_packet relies on: + * - A 13-byte (0x0d) radiotap keeps the legacy/HT path (rate_id=8). A length + * other than 13 flips send_packet's vht heuristic (rate_id=9) — VHT needs a + * 22-byte radiotap. + * - The radiotap iterator parses all present fields; a 13-byte HT-MCS radiotap + * (it_present=MCS|TX_FLAGS) is honoured directly — no env gate required. */ - #ifndef RADIOTAP_BUILDER_H #define RADIOTAP_BUILDER_H #include #include -namespace devourer { - -struct StreamRateCfg { - enum class Mode { Legacy, HT, VHT }; - Mode mode = Mode::Legacy; - - /* Legacy: in 500 kbps units. 12=6M, 18=9M, 24=12M, 36=18M, 48=24M, - * 72=36M, 96=48M, 108=54M. Default 6M. */ - uint8_t legacy_rate_500kbps = 12; +#include "TxMode.h" - /* HT: 0..31 (NSS is implicit — MCS0-7 = 1ss, 8-15 = 2ss, ...). */ - uint8_t ht_mcs = 0; - - /* VHT: per IEEE 802.11ac. mcs 0..9, nss 1..4. */ - uint8_t vht_mcs = 0; - uint8_t vht_nss = 1; - - /* Bandwidth in MHz. Legacy mode ignores this (always 20 MHz). HT - * recognises 20 or 40; VHT recognises 20/40/80/160. */ - uint8_t bw_mhz = 20; - - bool sgi = false; - bool ldpc = false; - bool stbc = false; -}; +namespace devourer { -/* Build a radiotap header according to cfg.mode. Output is always a - * complete, well-formed radiotap header — no 802.11 frame body. */ -std::vector build_stream_radiotap(const StreamRateCfg& cfg); +/* Build a radiotap header according to mode.mode. Output is always a complete, + * well-formed radiotap header — no 802.11 frame body. */ +std::vector build_stream_radiotap(const TxMode& mode); -/* Parse DEVOURER_STREAM_RATE / DEVOURER_STREAM_BW / DEVOURER_STREAM_SGI / - * DEVOURER_STREAM_LDPC / DEVOURER_STREAM_STBC. Unrecognised values fall - * back to default 6M legacy. - * - * DEVOURER_STREAM_RATE grammar (case-insensitive): - * - Legacy: 6M / 9M / 12M / 18M / 24M / 36M / 48M / 54M - * - HT: MCS0 .. MCS31 - * - VHT: VHT1SS_MCS0 .. VHT4SS_MCS9 - */ -StreamRateCfg parse_stream_rate_env(); +/* Parse DEVOURER_TX_RATE into a TxMode. Single slash-separated string: + * [/][/SGI][/LDPC][/STBC] (case-insensitive) + * : 6M|9M|12M|18M|24M|36M|48M|54M | MCS0..MCS31 | VHT1SS_MCS0..VHT4SS_MCS9 + * : 20|40|80|160 (default 20) + * Unset or unrecognised falls back to 6M legacy. */ +TxMode parse_tx_mode_env(); } // namespace devourer diff --git a/src/RtlJaguarDevice.cpp b/src/RtlJaguarDevice.cpp index e1c0d48..9915a56 100644 --- a/src/RtlJaguarDevice.cpp +++ b/src/RtlJaguarDevice.cpp @@ -41,6 +41,9 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { u8 fixed_rate = MGN_1M, sgi = 0, bwidth = 0, ldpc = 0, stbc = 0; u16 txflags = 0; int rate_id = 0; + /* True once the radiotap carries a rate (RATE / HT-MCS-index / VHT). When it + * stays false the device TX-mode default (SetTxMode) — else MGN_1M — applies. */ + bool rate_from_radiotap = false; if (length < sizeof(struct ieee80211_radiotap_header)) { return false; } @@ -75,6 +78,7 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { case IEEE80211_RADIOTAP_RATE: fixed_rate = *iterator.this_arg; + rate_from_radiotap = true; break; case IEEE80211_RADIOTAP_TX_FLAGS: @@ -113,20 +117,16 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { ldpc = 1; } - /* DEVOURER_TX_HT_MCS=1: honour the HT MCS index from radiotap byte 2 - * and set fixed_rate accordingly. Without this knob the historic - * behaviour kicks in — fixed_rate stays at the MGN_1M default and the - * chip transmits 1 Mbps CCK regardless of the HT-MCS field (see PR - * #80's README: "send_packet only wires fixed_rate from the radiotap - * RATE/VHT fields — never the HT MCS index"). Gated because flipping - * this unconditionally would silently change the regression matrix's - * rate sweeps and the precoder PoC's locked-in 6 Mbps OFDM carrier. */ - static const bool ht_mcs_enabled = - std::getenv("DEVOURER_TX_HT_MCS") != nullptr; - if (ht_mcs_enabled && (mcs_known & IEEE80211_RADIOTAP_MCS_HAVE_MCS)) { + /* The radiotap is authoritative for per-packet rate: honour the HT MCS + * index from byte 2 unconditionally. (Previously gated behind the + * DEVOURER_TX_HT_MCS env var, so a valid HT radiotap silently fell back + * to 1M CCK — now replaced by the programmatic SetTxMode default, applied + * after this loop only when no rate is present in the radiotap.) */ + if (mcs_known & IEEE80211_RADIOTAP_MCS_HAVE_MCS) { uint8_t mcs_index = iterator.this_arg[2]; if (mcs_index <= 31) { fixed_rate = MGN_MCS0 + mcs_index; + rate_from_radiotap = true; } } } break; @@ -164,6 +164,7 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { if (mcs > 9) mcs = 9; fixed_rate = MGN_VHT1SS_MCS0 + ((nss - 1) * 10 + mcs); + rate_from_radiotap = true; } } break; @@ -172,6 +173,20 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { } } + /* The radiotap carried no rate → apply the runtime TX-mode default set via + * SetTxMode (modulation / MCS / BW / GI / FEC / STBC). With no default set, + * the MGN_1M fallback from above stands. Per-packet radiotap always wins. */ + if (!rate_from_radiotap && _tx_mode_default.has_value()) { + const devourer::TxParams tp = + devourer::tx_mode_to_params(*_tx_mode_default); + fixed_rate = tp.fixed_rate; + vht = tp.vht; + sgi = tp.sgi; + ldpc = tp.ldpc; + stbc = tp.stbc; + bwidth = static_cast(tp.bwidth); + } + /* CCK rates (1/2/5.5/11M) do not exist at 5GHz. The RTL8814AU silently * drops a CCK-rated frame on a 5GHz channel — the bulk-OUT completes but * nothing goes on-air (verified on hardware: default MGN_1M beacon = 0 @@ -402,6 +417,12 @@ void RtlJaguarDevice::SetTxPowerOverride(int idx) { void RtlJaguarDevice::ApplyTxPower() { _radioManagement->ApplyTxPower(); } +void RtlJaguarDevice::SetTxMode(const devourer::TxMode& mode) { + _tx_mode_default = mode; +} + +void RtlJaguarDevice::ClearTxMode() { _tx_mode_default.reset(); } + uint32_t RtlJaguarDevice::ReadBBReg(uint16_t addr, uint32_t mask) { return _radioManagement->phy_query_bb_reg_public(addr, mask); } diff --git a/src/RtlJaguarDevice.h b/src/RtlJaguarDevice.h index 9b49409..70b400c 100644 --- a/src/RtlJaguarDevice.h +++ b/src/RtlJaguarDevice.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "logger.h" @@ -16,6 +17,7 @@ #include "EepromManager.h" #include "RadioManagementModule.h" #include "FrameParser.h" +#include "TxMode.h" extern "C" { @@ -41,6 +43,9 @@ class RtlJaguarDevice { Logger_t _logger; uint8_t debug; Action_ParsedRadioPacket _packetProcessor = nullptr; + /* Runtime TX-mode default (SetTxMode/ClearTxMode); applied in send_packet + * only when the frame's radiotap carries no rate. */ + std::optional _tx_mode_default; public: RtlJaguarDevice(RtlUsbAdapter device, Logger_t logger); @@ -60,6 +65,15 @@ class RtlJaguarDevice { /* Read a baseband register (debug/diagnostic). Thin passthrough to the * radio manager's BB read — handy for confirming a TXAGC write landed. */ uint32_t ReadBBReg(uint16_t addr, uint32_t mask); + + /* Runtime TX-mode default. send_packet honours a frame's own radiotap rate + * fields per-packet; when a frame's radiotap carries no rate, this mode + * supplies the modulation / MCS / BW / GI / FEC / STBC instead of the + * built-in 1M-CCK fallback. Pure state — applied on the next send_packet, + * no USB I/O. ClearTxMode() reverts to the built-in default. */ + void SetTxMode(const devourer::TxMode& mode); + void ClearTxMode(); + bool send_packet(const uint8_t* packet, size_t length); SelectedChannel GetSelectedChannel(); bool should_stop = false; diff --git a/src/TxMode.cpp b/src/TxMode.cpp new file mode 100644 index 0000000..02d1b28 --- /dev/null +++ b/src/TxMode.cpp @@ -0,0 +1,46 @@ +#include "TxMode.h" + +#include "RadioManagementModule.h" // enum MGN_RATE + +namespace devourer { + +TxParams tx_mode_to_params(const TxMode& m) { + TxParams p{}; + p.sgi = m.sgi ? 1 : 0; + p.ldpc = m.ldpc ? 1 : 0; + p.stbc = m.stbc ? 1 : 0; + + switch (m.bw_mhz) { + case 40: p.bwidth = CHANNEL_WIDTH_40; break; + case 80: p.bwidth = CHANNEL_WIDTH_80; break; + /* The TX descriptor caps at 80 MHz (BWSettingOfDesc 0/1/2); map 160 down. */ + case 160: p.bwidth = CHANNEL_WIDTH_80; break; + default: p.bwidth = CHANNEL_WIDTH_20; break; + } + + switch (m.mode) { + case TxMode::Mode::HT: { + const uint8_t mcs = m.ht_mcs <= 31 ? m.ht_mcs : 0; + p.fixed_rate = static_cast(MGN_MCS0 + mcs); + p.vht = false; + break; + } + case TxMode::Mode::VHT: { + uint8_t nss = m.vht_nss < 1 ? 1 : (m.vht_nss > 4 ? 4 : m.vht_nss); + uint8_t mcs = m.vht_mcs > 9 ? 9 : m.vht_mcs; + p.fixed_rate = + static_cast(MGN_VHT1SS_MCS0 + (nss - 1) * 10 + mcs); + p.vht = true; + break; + } + case TxMode::Mode::Legacy: + default: + /* legacy_rate_500kbps already equals the MGN_* OFDM byte value. */ + p.fixed_rate = m.legacy_rate_500kbps; + p.vht = false; + break; + } + return p; +} + +} // namespace devourer diff --git a/src/TxMode.h b/src/TxMode.h new file mode 100644 index 0000000..97aea79 --- /dev/null +++ b/src/TxMode.h @@ -0,0 +1,62 @@ +/* Programmatic TX-mode descriptor for the Jaguar TX path. + * + * A TxMode names the modulation class (legacy OFDM / HT / VHT), the rate + * (MCS / NSS / legacy rate), bandwidth, and the GI / FEC / STBC modifiers — + * everything that decides a frame's on-air PHY rate. It is the type accepted + * by RtlJaguarDevice::SetTxMode (the runtime default applied when a frame's + * radiotap carries no rate) and produced by RadiotapBuilder's env parser. + * + * Replaces the former per-knob DEVOURER_TX_* environment variables and the + * DEVOURER_TX_HT_MCS library gate: send_packet now honours the radiotap rate + * fields unconditionally, and a TxMode supplies the default when there is none. + */ +#ifndef DEVOURER_TX_MODE_H +#define DEVOURER_TX_MODE_H + +#include + +#include "SelectedChannel.h" // ChannelWidth_t + +namespace devourer { + +struct TxMode { + enum class Mode { Legacy, HT, VHT }; + Mode mode = Mode::Legacy; + + /* Legacy OFDM rate in 500 kbps units (the radiotap RATE convention, which is + * identical to the MGN_* OFDM enum value): 12=6M, 18=9M, 24=12M, 36=18M, + * 48=24M, 72=36M, 96=48M, 108=54M. Default 6M. */ + uint8_t legacy_rate_500kbps = 12; + + /* HT MCS index 0..31 (NSS implicit: MCS0-7=1ss, 8-15=2ss, ...). */ + uint8_t ht_mcs = 0; + + /* VHT per 802.11ac: mcs 0..9, nss 1..4. */ + uint8_t vht_mcs = 0; + uint8_t vht_nss = 1; + + /* Bandwidth in MHz. Legacy is always 20; HT honours 20/40; VHT 20/40/80. */ + uint8_t bw_mhz = 20; + + bool sgi = false; + bool ldpc = false; + bool stbc = false; +}; + +/* The descriptor inputs send_packet writes, derived from a TxMode. `fixed_rate` + * is an MGN_* value (see RadioManagementModule.h); `vht` drives the TX-desc + * rate_id (8 for legacy/HT, 9 for VHT). */ +struct TxParams { + uint8_t fixed_rate; + bool vht; + uint8_t sgi; + uint8_t ldpc; + uint8_t stbc; + ChannelWidth_t bwidth; +}; + +TxParams tx_mode_to_params(const TxMode& mode); + +} // namespace devourer + +#endif // DEVOURER_TX_MODE_H diff --git a/tests/README.md b/tests/README.md index 95a6347..faa2e52 100644 --- a/tests/README.md +++ b/tests/README.md @@ -288,12 +288,11 @@ reportedly fails at RX). Add more in `ENCODING_COMBOS` at the top of The underlying knobs are also usable standalone for one-off targeted TX: -- **Devourer TX:** `DEVOURER_TX_MCS=N`, `DEVOURER_TX_LDPC=1`, - `DEVOURER_TX_STBC=N`, `DEVOURER_TX_BW=20|40|80|160` env vars read by - `WiFiDriverTxDemo`. Default mode is HT; `DEVOURER_TX_VHT=1` switches to - a VHT radiotap header (22 bytes) and exposes `DEVOURER_TX_VHT_MCS=N` + - `DEVOURER_TX_VHT_NSS=N`. `_LDPC` / `_STBC` / `_BW` apply to whichever - mode is active. +- **Devourer TX:** `DEVOURER_TX_RATE=[/][/SGI][/LDPC][/STBC]` read by + `WiFiDriverTxDemo` (parsed into a `devourer::TxMode`, applied via + `RtlJaguarDevice::SetTxMode`). `` = `6M`..`54M` (legacy) | `MCS0`..`MCS31` + (HT) | `VHT1SS_MCS0`..`VHT4SS_MCS9` (VHT); `` = `20|40|80|160`. Examples: + `MCS7/40/SGI`, `VHT2SS_MCS3/80/LDPC`. - **Kernel-side scapy TX:** `--mcs N` / `--ldpc` / `--stbc N` / `--bandwidth 20|40|80|160` on `tests/inject_beacon.py`, plus `--vht` / `--vht-mcs N` / `--vht-nss N` for VHT mode. diff --git a/tests/bench_onair.py b/tests/bench_onair.py index 2b43d56..bb26d3f 100755 --- a/tests/bench_onair.py +++ b/tests/bench_onair.py @@ -61,9 +61,8 @@ def sdr_duty(freq: float, mcs: int, bw: int, noise_db: float | None, def devourer_flood(vid, pid, ch, mcs, bw, size): env = dict(__import__("os").environ, DEVOURER_VID=vid, DEVOURER_PID=pid, DEVOURER_CHANNEL=str(ch), - DEVOURER_TX_HT_MCS="1", DEVOURER_TX_MCS=str(mcs), - DEVOURER_TX_BW=str(bw), DEVOURER_TX_PAYLOAD_BYTES=str(size), - DEVOURER_TX_GAP_US="0") + DEVOURER_TX_RATE=f"MCS{mcs}/{bw}", + DEVOURER_TX_PAYLOAD_BYTES=str(size), DEVOURER_TX_GAP_US="0") return regress._register_local_proc(subprocess.Popen( [str(ROOT / "build" / "WiFiDriverTxDemo")], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, diff --git a/tests/regress.py b/tests/regress.py index 353e3aa..2f90843 100755 --- a/tests/regress.py +++ b/tests/regress.py @@ -637,23 +637,27 @@ def _devourer_env(dut: Dut, channel: int, env["DEVOURER_PID"] = f"0x{dut.pid}" env["DEVOURER_CHANNEL"] = str(channel) if tx_encoding: - # DEVOURER_TX_* knobs are read by txdemo/main.cpp to patch the - # radiotap MCS info bytes before the TX loop. Only meaningful when - # spawning WiFiDriverTxDemo; harmless on the RX side. - if tx_encoding.get("mcs") is not None: - env["DEVOURER_TX_MCS"] = str(tx_encoding["mcs"]) + # The TX rate/mode is a single DEVOURER_TX_RATE string read by + # WiFiDriverTxDemo (-> RtlJaguarDevice::SetTxMode): + # "[/][/SGI][/LDPC][/STBC]". + # Only meaningful when spawning WiFiDriverTxDemo; harmless on the RX side. + if tx_encoding.get("vht"): + rate = (f"VHT{tx_encoding.get('nss', 1)}SS_MCS" + f"{tx_encoding.get('vht_mcs', 0)}") + elif tx_encoding.get("mcs") is not None: + rate = f"MCS{tx_encoding['mcs']}" + else: + rate = "6M" + parts = [rate] + if tx_encoding.get("bandwidth") is not None: + parts.append(str(tx_encoding["bandwidth"])) + if tx_encoding.get("sgi"): + parts.append("SGI") if tx_encoding.get("ldpc"): - env["DEVOURER_TX_LDPC"] = "1" + parts.append("LDPC") if tx_encoding.get("stbc"): - env["DEVOURER_TX_STBC"] = str(tx_encoding["stbc"]) - if tx_encoding.get("bandwidth") is not None: - env["DEVOURER_TX_BW"] = str(tx_encoding["bandwidth"]) - if tx_encoding.get("vht"): - env["DEVOURER_TX_VHT"] = "1" - if tx_encoding.get("vht_mcs") is not None: - env["DEVOURER_TX_VHT_MCS"] = str(tx_encoding["vht_mcs"]) - if tx_encoding.get("nss") is not None: - env["DEVOURER_TX_VHT_NSS"] = str(tx_encoding["nss"]) + parts.append("STBC") + env["DEVOURER_TX_RATE"] = "/".join(parts) return env diff --git a/tests/thermal_gain_sweep.py b/tests/thermal_gain_sweep.py index cbbb2df..ba48235 100755 --- a/tests/thermal_gain_sweep.py +++ b/tests/thermal_gain_sweep.py @@ -79,10 +79,9 @@ def _devourer_env(dut: regress.Dut, channel: int, ramp: dict) -> dict: env["DEVOURER_THERMAL_POLL_MS"] = str(ramp["thermal_ms"]) env["DEVOURER_TX_GAP_US"] = str(ramp["gap_us"]) if ramp["ht"]: - # Transmit HT/OFDM instead of the 1M-CCK default — CCK output power is - # decoupled from the per-rate TXAGC base, so the gain knob only shows - # up on-air for OFDM/HT/VHT rates. - env["DEVOURER_TX_HT_MCS"] = "1" + # Transmit HT (MCS1) instead of the 6M-legacy default — the gain knob + # only moves on-air power for OFDM/HT/VHT rates, not the CCK path. + env["DEVOURER_TX_RATE"] = "MCS1" # Keep a high warn threshold so the demo's own back-off message doesn't # spam; we judge from the data, and abort on `critical` ourselves. env["DEVOURER_THERMAL_WARN_DELTA"] = "100" diff --git a/tools/precoder/README.md b/tools/precoder/README.md index 2931ff0..8b0cf5b 100644 --- a/tools/precoder/README.md +++ b/tools/precoder/README.md @@ -212,15 +212,13 @@ observer; the off-chip counterpart to Tier 4. ## Things the plan got slightly wrong (and how this resolves them) -* **HT MCS 0 doesn't reach the air (decisive).** The plan selected the rate via - `DEVOURER_TX_MCS=0`, but `RtlJaguarDevice::send_packet` only sets the TX rate - from the radiotap RATE field (legacy) or the VHT field — it **never reads the - HT MCS index**. An HT-MCS frame with no RATE field therefore transmits at the - `MGN_1M` default = **1 Mbps CCK** (DSSS, no OFDM subcarriers). So this PoC uses - **legacy 6 Mbps OFDM** (BPSK r=½), which `send_packet` honours and which the - chip really modulates as OFDM. (Fixing the HT path is a one-liner in - `send_packet`, but it changes shared core TX behaviour the regression matrix - depends on — deliberately out of scope here.) +* **HT MCS 0 is a different numerology.** The plan selected the rate via the HT + MCS field, but HT MCS 0 is a *different* numerology (52 SD, 26 info bits, + 13×4 interleaver) than the plan's legacy-OFDM prose. So this PoC uses + **legacy 6 Mbps OFDM** (BPSK r=½) via the radiotap RATE field, which the chip + modulates as OFDM. (`send_packet` now honours the HT MCS index directly — the + former `DEVOURER_TX_HT_MCS` gate is gone — so an HT carrier is available via + `DEVOURER_TX_RATE=MCS` if a future variant wants the HT numerology.) * **Numerology.** Consequently the on-air rate is legacy 802.11a/g 6 Mbps: **48 data subcarriers, 24 info bits, a 16×3 interleaver** — exactly the plan's diff --git a/tools/precoder/encode_subcarriers.py b/tools/precoder/encode_subcarriers.py index 4cf9923..c0bc1bc 100644 --- a/tools/precoder/encode_subcarriers.py +++ b/tools/precoder/encode_subcarriers.py @@ -24,13 +24,10 @@ ------------------------------------ The originating plan's prose ("48 data + 4 pilot", "24 input bits") describes *legacy* 802.11a OFDM (6 Mbps BPSK). It also said to select the rate via the HT -radiotap MCS field (`DEVOURER_TX_MCS=0`) — but HT MCS 0 is a DIFFERENT -numerology (52 SD, 26 info bits, 13x4 interleaver) AND, decisively, -`RtlJaguarDevice::send_packet` never wires the HT MCS *index* into the TX rate: -an HT-MCS frame with no RATE field transmits at the MGN_1M default = 1 Mbps -CCK, which is DSSS — no OFDM subcarriers at all. So the working OFDM-BPSK path -is legacy 6 Mbps (honoured via the radiotap RATE field), which is what -`PrecoderDemo` transmits. Both numerologies are implemented: +radiotap MCS field — but HT MCS 0 is a DIFFERENT numerology (52 SD, 26 info +bits, 13x4 interleaver), so the working OFDM-BPSK path is legacy 6 Mbps +(honoured via the radiotap RATE field), which is what `PrecoderDemo` +transmits. Both numerologies are implemented: --phy legacy (default) N_SD=48 N_CBPS=48 N_DBPS=24 interleaver 16x3 --phy ht N_SD=52 N_CBPS=52 N_DBPS=26 interleaver 13x4 diff --git a/txdemo/main.cpp b/txdemo/main.cpp index b38066f..7f9789d 100644 --- a/txdemo/main.cpp +++ b/txdemo/main.cpp @@ -29,6 +29,7 @@ #include "FrameParser.h" #include "RtlUsbAdapter.h" #include "WiFiDriver.h" +#include "RadiotapBuilder.h" #include "logger.h" #define USB_VENDOR_ID 0x0bda @@ -310,8 +311,8 @@ int main(int argc, char **argv) { std::thread usb_thread(usb_event_loop, logger, context); uint8_t beacon_frame[] = { - 0x00, 0x00, 0x0d, 0x00, 0x00, 0x80, 0x08, 0x00, 0x08, 0x00, 0x37, - 0x00, 0x01, // radiotap header + 0x00, 0x00, 0x0a, 0x00, 0x00, 0x80, 0x00, 0x00, 0x08, + 0x00, // radiotap: TX_FLAGS only (rate-less) — rate comes from SetTxMode /* Mgmt / probe-request frame (FC=0x40 0x00). Was DATA / ToDS=1 * (FC=0x08 0x01) which requires an AP context the chip doesn't * have in monitor mode — the chip silently NAKed every bulk OUT. */ @@ -326,109 +327,16 @@ int main(int argc, char **argv) { 0xcd, 0xce, 0x4e, 0x35, 0xd9, 0x85, 0x9a, 0xcf, 0x4d, 0x48, 0x4c, 0x8f, 0x28, 0x6f, 0x10, 0xb0, 0xa9, 0x5d, 0xbf, 0xcb, 0x6f}; - /* Radiotap MCS info lives at beacon_frame[10..12]: known mask, flags, idx. - * Defaults encode HT MCS 1 / 20 MHz / long GI / BCC / no STBC. Env knobs - * let tests/regress.py --encoding-matrix exercise LDPC and STBC paths — - * needed to surface chip-specific asymmetries like the RTL8821AU - * LDPC-RX-no limitation. DEVOURER_TX_VHT=1 switches to a VHT (802.11ac) - * radiotap header instead (radiotap bit 21, 22-byte length) — required - * for chips whose LDPC RX limitation only appears on the VHT path. */ - bool tx_vht = std::getenv("DEVOURER_TX_VHT") != nullptr; - if (const char *m = std::getenv("DEVOURER_TX_MCS")) { - beacon_frame[12] = static_cast(std::strtoul(m, nullptr, 0) & 0x7F); - logger->info("DEVOURER_TX_MCS — HT MCS index set to {}", beacon_frame[12]); - } - uint8_t mcs_flags = beacon_frame[11]; - bool tx_ldpc = std::getenv("DEVOURER_TX_LDPC") != nullptr; - if (tx_ldpc && !tx_vht) { - mcs_flags |= 0x10; /* HT MCS flags bit 4 = FEC type LDPC */ - logger->info("DEVOURER_TX_LDPC — FEC=LDPC (HT)"); - } - int tx_stbc = 0; - if (const char *s = std::getenv("DEVOURER_TX_STBC")) { - tx_stbc = std::atoi(s) & 0x3; - if (!tx_vht) { - mcs_flags = static_cast((mcs_flags & ~0x60) | (tx_stbc << 5)); - } - logger->info("DEVOURER_TX_STBC — {} STBC stream(s)", tx_stbc); - } - int tx_bw = 20; - if (const char *bw = std::getenv("DEVOURER_TX_BW")) { - tx_bw = std::atoi(bw); - if (!tx_vht) { - uint8_t code = (tx_bw == 40) ? 0x01 : 0x00; - mcs_flags = static_cast((mcs_flags & ~0x03) | code); - } - logger->info("DEVOURER_TX_BW — {} MHz", tx_bw); - } - beacon_frame[11] = mcs_flags; - - /* Build the final TX buffer. Default: send beacon_frame[] verbatim (the - * existing HT path, with the in-place patches above already applied). VHT - * mode: swap the first 13 bytes (HT radiotap) for a 22-byte VHT radiotap, - * keep the 802.11 frame body unchanged. */ - std::vector tx_buf; - if (tx_vht) { - int vht_mcs = 0; - int vht_nss = 1; - if (const char *vm = std::getenv("DEVOURER_TX_VHT_MCS")) { - vht_mcs = std::atoi(vm) & 0xF; - } - if (const char *vn = std::getenv("DEVOURER_TX_VHT_NSS")) { - vht_nss = std::atoi(vn) & 0xF; - } - uint8_t bw_code = 0; - switch (tx_bw) { - case 40: bw_code = 1; break; - case 80: bw_code = 4; break; - case 160: bw_code = 11; break; - default: bw_code = 0; break; - } - /* VHT radiotap layout (22 bytes): header(8) + TX Flags(2) + VHT info(12). - * VHT info: u16 known, u8 flags, u8 bw, u8[4] mcs_nss, u8 coding, - * u8 group_id, u16 partial_aid. Mirrors tests/inject_beacon.py's - * _build_radiotap_vht. */ - const uint16_t known = (1u << 0) | (1u << 2) | (1u << 6); /* STBC|GI|BW */ - const uint8_t vht_info_flags = tx_stbc ? 0x01 : 0x00; - const uint8_t mcs_nss_user0 = - static_cast(((vht_mcs & 0xF) << 4) | (vht_nss & 0xF)); - const uint8_t coding = tx_ldpc ? 0x01 : 0x00; /* user-0 nibble */ - /* it_present = (1<<15) TX Flags | (1<<21) VHT */ - const uint32_t it_present = (1u << 15) | (1u << 21); - const uint16_t it_len = 22; - const uint16_t tx_flags = 0x0008; - tx_buf.reserve(22 + sizeof(beacon_frame) - 13); - /* radiotap header */ - tx_buf.push_back(0); /* version */ - tx_buf.push_back(0); /* pad */ - tx_buf.push_back(static_cast(it_len & 0xFF)); - tx_buf.push_back(static_cast((it_len >> 8) & 0xFF)); - tx_buf.push_back(static_cast(it_present & 0xFF)); - tx_buf.push_back(static_cast((it_present >> 8) & 0xFF)); - tx_buf.push_back(static_cast((it_present >> 16) & 0xFF)); - tx_buf.push_back(static_cast((it_present >> 24) & 0xFF)); - /* TX Flags */ - tx_buf.push_back(static_cast(tx_flags & 0xFF)); - tx_buf.push_back(static_cast((tx_flags >> 8) & 0xFF)); - /* VHT info */ - tx_buf.push_back(static_cast(known & 0xFF)); - tx_buf.push_back(static_cast((known >> 8) & 0xFF)); - tx_buf.push_back(vht_info_flags); - tx_buf.push_back(bw_code); - tx_buf.push_back(mcs_nss_user0); - tx_buf.push_back(0); tx_buf.push_back(0); tx_buf.push_back(0); /* users 1-3 */ - tx_buf.push_back(coding); - tx_buf.push_back(0); /* group_id */ - tx_buf.push_back(0); tx_buf.push_back(0); /* partial_aid LE */ - /* 802.11 frame body (skip the original 13-byte HT radiotap). */ - tx_buf.insert(tx_buf.end(), - beacon_frame + 13, beacon_frame + sizeof(beacon_frame)); - logger->info( - "DEVOURER_TX_VHT — VHT radiotap: mcs={} nss={} ldpc={} stbc={} bw={}MHz", - vht_mcs, vht_nss, tx_ldpc ? 1 : 0, tx_stbc, tx_bw); - } else { - tx_buf.assign(beacon_frame, beacon_frame + sizeof(beacon_frame)); - } + /* On-air rate/modulation comes from the device TX-mode default. + * parse_tx_mode_env() reads DEVOURER_TX_RATE — a single string + * "[/][/SGI][/LDPC][/STBC]" (e.g. "MCS7/40/SGI", "VHT2SS_MCS3/80", + * "6M") — into a TxMode. The beacon's rate-less radiotap (above) lets that + * default apply; a frame embedding its own rate radiotap overrides it per + * packet. Default (no env) = 6 M legacy. Replaces the former per-knob + * DEVOURER_TX_MCS/_VHT/_LDPC/_STBC/_BW env vars + the DEVOURER_TX_HT_MCS gate. */ + rtlDevice->SetTxMode(devourer::parse_tx_mode_env()); + + std::vector tx_buf(beacon_frame, beacon_frame + sizeof(beacon_frame)); /* Frame-size knob for throughput benchmarking. DEVOURER_TX_PAYLOAD_BYTES=N * pads the 802.11 body so the on-air PSDU is exactly N bytes — send_packet @@ -438,7 +346,7 @@ int main(int argc, char **argv) { * is N + TXDESC_SIZE bytes. Default unset = the small probe-request beacon. */ if (const char *e = std::getenv("DEVOURER_TX_PAYLOAD_BYTES")) { long want = std::strtol(e, nullptr, 0); - size_t radiotap_len = tx_vht ? 22 : 13; + size_t radiotap_len = 10; /* rate-less TX_FLAGS-only radiotap */ size_t body_len = tx_buf.size() - radiotap_len; if (want > 0 && static_cast(want) > body_len) { tx_buf.insert(tx_buf.end(), static_cast(want) - body_len, 0x00); diff --git a/txdemo/stream_duplex_demo/main.cpp b/txdemo/stream_duplex_demo/main.cpp index c29b5cb..e442321 100644 --- a/txdemo/stream_duplex_demo/main.cpp +++ b/txdemo/stream_duplex_demo/main.cpp @@ -79,7 +79,7 @@ static constexpr uint16_t kRealtekProductIds[] = { // processor below is identical to demo/main.cpp's, so any tooling that // already grep'd lines keeps working unchanged. static const std::vector kStreamRadiotap = - devourer::build_stream_radiotap(devourer::parse_stream_rate_env()); + devourer::build_stream_radiotap(devourer::parse_tx_mode_env()); static const uint8_t kCanonicalSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00}; static std::vector build_dot11_probe_req() { diff --git a/txdemo/stream_tx_demo/main.cpp b/txdemo/stream_tx_demo/main.cpp index 70e7c11..ca1e5b0 100644 --- a/txdemo/stream_tx_demo/main.cpp +++ b/txdemo/stream_tx_demo/main.cpp @@ -83,7 +83,7 @@ static constexpr uint16_t kRealtekProductIds[] = { // kRadiotapLegacy6M constant. Same canonical SA, same matcher in // demo/main.cpp's RX path — keep these three in lockstep, see CLAUDE.md. static const std::vector kStreamRadiotap = - devourer::build_stream_radiotap(devourer::parse_stream_rate_env()); + devourer::build_stream_radiotap(devourer::parse_tx_mode_env()); static const uint8_t kCanonicalSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00}; static std::vector build_dot11_probe_req() {