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
34 changes: 18 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<rate>[/<bw>][/SGI][/LDPC][/STBC]` (case-insensitive). Unset
= `6M` legacy. Examples: `MCS7`, `MCS7/40/SGI`, `VHT2SS_MCS3/80/LDPC`, `54M`.
- `<rate>`: `6M`/`9M`/`12M`/`18M`/`24M`/`36M`/`48M`/`54M` (legacy OFDM),
`MCS0`..`MCS31` (HT), or `VHT1SS_MCS0`..`VHT4SS_MCS9` (VHT).
- `<bw>`: `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` +
Expand All @@ -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 `<devourer-txpwr>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
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<rate>[/<bw>][/SGI][/LDPC][/STBC]` — the on-air TX mode,
parsed into a `devourer::TxMode` and applied via `RtlJaguarDevice::SetTxMode`.
`<rate>` = `6M`..`54M` (legacy OFDM) | `MCS0`..`MCS31` (HT) |
`VHT1SS_MCS0`..`VHT4SS_MCS9` (VHT); `<bw>` = `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.

Expand Down
2 changes: 1 addition & 1 deletion docs/wfb-ng-tuning.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
167 changes: 85 additions & 82 deletions src/RadiotapBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>

namespace devourer {

Expand Down Expand Up @@ -31,11 +32,9 @@ void emit_u32_le(std::vector<uint8_t>& v, uint32_t x) {
v.push_back(static_cast<uint8_t>((x >> 24) & 0xFF));
}

std::vector<uint8_t> 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<uint8_t> 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<uint8_t> r;
r.reserve(13);
emit_u8(r, kRadiotapVersion);
Expand All @@ -49,17 +48,11 @@ std::vector<uint8_t> build_legacy(const StreamRateCfg& cfg) {
return r;
}

std::vector<uint8_t> 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<uint8_t> 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;
Expand All @@ -68,7 +61,7 @@ std::vector<uint8_t> 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;
Expand All @@ -87,20 +80,17 @@ std::vector<uint8_t> build_ht(const StreamRateCfg& cfg) {
return r;
}

std::vector<uint8_t> 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<uint8_t> 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;
case 80: bw_code = 4; break;
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;
Expand Down Expand Up @@ -152,62 +142,25 @@ bool parse_uint(const std::string& s, size_t pos, unsigned* out) {
return true;
}

} // namespace

std::vector<uint8_t> 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<uint8_t>(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<N>, 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<uint8_t>(mcs);
if (std::getenv("DEVOURER_TX_HT_MCS") == nullptr) {
std::fprintf(
stderr,
"<stream-radiotap>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<uint8_t>(mcs);
return true;
}
}

Expand All @@ -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<uint8_t>(nss);
cfg.vht_mcs = static_cast<uint8_t>(mcs);
return cfg;
cfg->mode = TxMode::Mode::VHT;
cfg->vht_nss = static_cast<uint8_t>(nss);
cfg->vht_mcs = static_cast<uint8_t>(mcs);
return true;
}
}
}
}
return false;
}

} // namespace

std::vector<uint8_t> 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<std::string> 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,
"<tx-mode>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,
"<stream-radiotap>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<uint8_t>(std::atoi(t.c_str()));
else if (!t.empty())
std::fprintf(stderr,
"<tx-mode>warning: ignoring unrecognised DEVOURER_TX_RATE "
"token '%s'\n", t.c_str());
}
return cfg;
}

Expand Down
Loading
Loading