diff --git a/CMakeLists.txt b/CMakeLists.txt index 6eb73b4..50291f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,8 @@ add_library(WiFiDriver src/RadioManagementModule.cpp src/RadioManagementModule.h src/Radiotap.c + src/RadiotapBuilder.cpp + src/RadiotapBuilder.h src/RtlJaguarDevice.cpp src/RtlJaguarDevice.h src/RtlUsbAdapter.cpp diff --git a/src/RadiotapBuilder.cpp b/src/RadiotapBuilder.cpp new file mode 100644 index 0000000..f30adfd --- /dev/null +++ b/src/RadiotapBuilder.cpp @@ -0,0 +1,241 @@ +#include "RadiotapBuilder.h" + +#include +#include +#include +#include + +namespace devourer { + +namespace { + +constexpr uint8_t kRadiotapVersion = 0; + +/* it_present bit positions per radiotap spec. */ +constexpr uint32_t kPresentRate = 1u << 2; +constexpr uint32_t kPresentTxFlags = 1u << 15; +constexpr uint32_t kPresentMcs = 1u << 19; +constexpr uint32_t kPresentVht = 1u << 21; + +constexpr uint16_t kTxFlagsNoAck = 0x0008; + +void emit_u8(std::vector& v, uint8_t x) { v.push_back(x); } +void emit_u16_le(std::vector& v, uint16_t x) { + v.push_back(static_cast(x & 0xFF)); + v.push_back(static_cast((x >> 8) & 0xFF)); +} +void emit_u32_le(std::vector& v, uint32_t x) { + v.push_back(static_cast(x & 0xFF)); + v.push_back(static_cast((x >> 8) & 0xFF)); + v.push_back(static_cast((x >> 16) & 0xFF)); + 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 r; + r.reserve(13); + emit_u8(r, kRadiotapVersion); + emit_u8(r, 0); /* pad */ + emit_u16_le(r, 13); /* it_len */ + emit_u32_le(r, kPresentRate | kPresentTxFlags); /* it_present */ + emit_u8(r, cfg.legacy_rate_500kbps); /* RATE */ + emit_u8(r, 0); /* pad (TX_FLAGS u16 align) */ + emit_u16_le(r, kTxFlagsNoAck); /* TX_FLAGS */ + emit_u8(r, 0); /* trailing pad to 13 */ + 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. + * + * 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 + */ + constexpr uint8_t kKnownBw = 1u << 0; + constexpr uint8_t kKnownMcs = 1u << 1; + constexpr uint8_t kKnownGi = 1u << 2; + constexpr uint8_t kKnownFec = 1u << 4; + constexpr uint8_t kKnownStbc = 1u << 5; + + uint8_t known = kKnownMcs | kKnownGi | kKnownFec | kKnownBw | kKnownStbc; + uint8_t flags = 0; + /* BW: 0=20, 1=40. (20L/20U upper/lower not exposed.) */ + if (cfg.bw_mhz >= 40) flags |= 0x01; + if (cfg.sgi) flags |= 0x04; + if (cfg.ldpc) flags |= 0x10; + if (cfg.stbc) flags |= 0x20; /* one STBC stream */ + + std::vector r; + r.reserve(13); + emit_u8(r, kRadiotapVersion); + emit_u8(r, 0); + emit_u16_le(r, 13); + emit_u32_le(r, kPresentTxFlags | kPresentMcs); + emit_u16_le(r, kTxFlagsNoAck); + emit_u8(r, known); + emit_u8(r, flags); + emit_u8(r, cfg.ht_mcs <= 31 ? cfg.ht_mcs : 0); + 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. */ + 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. */ + const uint16_t known = (1u << 0) | (1u << 2) | (1u << 6); + uint8_t vht_flags = 0; + if (cfg.stbc) vht_flags |= 0x01; + if (cfg.sgi) vht_flags |= 0x04; + const uint8_t mcs_nss_user0 = static_cast( + ((cfg.vht_mcs & 0x0F) << 4) | (cfg.vht_nss & 0x0F)); + const uint8_t coding_user0 = cfg.ldpc ? 0x01 : 0x00; + + std::vector r; + r.reserve(22); + emit_u8(r, kRadiotapVersion); + emit_u8(r, 0); + emit_u16_le(r, 22); + emit_u32_le(r, kPresentTxFlags | kPresentVht); + emit_u16_le(r, kTxFlagsNoAck); + emit_u16_le(r, known); + emit_u8(r, vht_flags); + emit_u8(r, bw_code); + emit_u8(r, mcs_nss_user0); + emit_u8(r, 0); /* user 1 mcs_nss */ + emit_u8(r, 0); /* user 2 mcs_nss */ + emit_u8(r, 0); /* user 3 mcs_nss */ + emit_u8(r, coding_user0); + emit_u8(r, 0); /* group_id */ + emit_u16_le(r, 0); /* partial_aid */ + return r; +} + +std::string to_upper_stripped(const char* raw) { + std::string s; + for (const char* p = raw; *p; ++p) { + if (*p == ' ' || *p == '\t' || *p == '\n') continue; + s.push_back(static_cast(std::toupper(static_cast(*p)))); + } + return s; +} + +bool parse_uint(const std::string& s, size_t pos, unsigned* out) { + if (pos >= s.size()) return false; + unsigned v = 0; + size_t i = pos; + while (i < s.size() && s[i] >= '0' && s[i] <= '9') { + v = v * 10 + static_cast(s[i] - '0'); + ++i; + if (v > 1000) return false; + } + if (i == pos) return false; + *out = v; + 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; } + + /* 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; + } + } + + /* VHT: VHTSS_MCS, NSS 1..4, MCS 0..9. */ + if (s.rfind("VHT", 0) == 0) { + unsigned nss; + if (parse_uint(s, 3, &nss) && nss >= 1 && nss <= 4) { + size_t after_nss = 3; + while (after_nss < s.size() && s[after_nss] >= '0' && s[after_nss] <= '9') + ++after_nss; + const std::string tail = "SS_MCS"; + 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; + } + } + } + } + + /* Unrecognised — fall back to default 6M legacy. */ + std::fprintf(stderr, + "warning: unrecognised DEVOURER_STREAM_RATE=%s," + " falling back to 6M legacy\n", raw); + return cfg; +} + +} // namespace devourer diff --git a/src/RadiotapBuilder.h b/src/RadiotapBuilder.h new file mode 100644 index 0000000..c28fefa --- /dev/null +++ b/src/RadiotapBuilder.h @@ -0,0 +1,76 @@ +/* Stream-carrier radiotap helper shared between the stream 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). + * + * 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). + */ + +#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; + + /* 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; +}; + +/* 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); + +/* 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(); + +} // namespace devourer + +#endif // RADIOTAP_BUILDER_H diff --git a/txdemo/stream_duplex_demo/main.cpp b/txdemo/stream_duplex_demo/main.cpp index 8fd9d7b..d01a986 100644 --- a/txdemo/stream_duplex_demo/main.cpp +++ b/txdemo/stream_duplex_demo/main.cpp @@ -53,6 +53,7 @@ #endif #include "FrameParser.h" +#include "RadiotapBuilder.h" #include "RtlUsbAdapter.h" #include "WiFiDriver.h" #include "logger.h" @@ -63,13 +64,15 @@ static constexpr uint16_t kRealtekProductIds[] = { 0x8812, 0x0811, 0xa811, 0xb811, 0x8813, }; -// Same radiotap + probe-request header as StreamTxDemo / PrecoderDemo. The -// canonical SA matcher in the packet processor below is identical to -// demo/main.cpp's, so any tooling that already grep'd -// lines keeps working unchanged. -static const uint8_t kRadiotapLegacy6M[13] = { - 0x00, 0x00, 0x0d, 0x00, 0x04, 0x80, 0x00, - 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00}; +// Same probe-request header as StreamTxDemo / PrecoderDemo; radiotap is now +// built once at startup from DEVOURER_STREAM_RATE — accepts legacy +// (6M..54M), HT (MCS0..MCS31), or VHT (VHT1SS_MCS0..VHT4SS_MCS9) carrier +// modes. Default is 6M legacy OFDM, bit-identical to the historic +// kRadiotapLegacy6M constant. The canonical SA matcher in the packet +// 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()); static const uint8_t kCanonicalSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00}; static std::vector build_dot11_probe_req() { @@ -136,7 +139,7 @@ struct TxArgs { static void tx_thread(TxArgs args) { auto dot11 = build_dot11_probe_req(); std::vector tx_buf; - tx_buf.reserve(sizeof(kRadiotapLegacy6M) + dot11.size() + args.max_psdu); + tx_buf.reserve(kStreamRadiotap.size() + dot11.size() + args.max_psdu); long tx_count = 0; while (!args.should_stop->load()) { @@ -162,8 +165,8 @@ static void tx_thread(TxArgs args) { break; } tx_buf.clear(); - tx_buf.insert(tx_buf.end(), kRadiotapLegacy6M, - kRadiotapLegacy6M + sizeof(kRadiotapLegacy6M)); + tx_buf.insert(tx_buf.end(), kStreamRadiotap.begin(), + kStreamRadiotap.end()); tx_buf.insert(tx_buf.end(), dot11.begin(), dot11.end()); tx_buf.insert(tx_buf.end(), psdu.begin(), psdu.end()); bool ok = args.rtl->send_packet(tx_buf.data(), tx_buf.size()); diff --git a/txdemo/stream_tx_demo/main.cpp b/txdemo/stream_tx_demo/main.cpp index c1582e8..7dc1279 100644 --- a/txdemo/stream_tx_demo/main.cpp +++ b/txdemo/stream_tx_demo/main.cpp @@ -58,6 +58,7 @@ #endif #include "FrameParser.h" +#include "RadiotapBuilder.h" #include "RtlUsbAdapter.h" #include "WiFiDriver.h" #include "logger.h" @@ -68,12 +69,14 @@ static constexpr uint16_t kRealtekProductIds[] = { 0x8812, 0x0811, 0xa811, 0xb811, 0x8813, }; -// Identical legacy-6M radiotap + 802.11 probe-request header to PrecoderDemo. -// Same canonical SA, same matcher in demo/main.cpp's RX path. Keep these -// three in lockstep — see CLAUDE.md. -static const uint8_t kRadiotapLegacy6M[13] = { - 0x00, 0x00, 0x0d, 0x00, 0x04, 0x80, 0x00, - 0x00, 0x0c, 0x00, 0x08, 0x00, 0x00}; +// Identical 802.11 probe-request header to PrecoderDemo; radiotap is now +// built once at startup from DEVOURER_STREAM_RATE — accepts legacy +// (6M..54M), HT (MCS0..MCS31), or VHT (VHT1SS_MCS0..VHT4SS_MCS9) carrier +// modes. Default is 6M legacy OFDM, bit-identical to the historic +// 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()); static const uint8_t kCanonicalSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00}; static std::vector build_dot11_probe_req() { @@ -215,7 +218,7 @@ int main(int argc, char **argv) { auto dot11 = build_dot11_probe_req(); std::vector tx_buf; - tx_buf.reserve(sizeof(kRadiotapLegacy6M) + dot11.size() + max_psdu); + tx_buf.reserve(kStreamRadiotap.size() + dot11.size() + max_psdu); logger->info( "stream TX ready (legacy 6M OFDM, ch {}); reading length-prefixed PSDUs " @@ -243,8 +246,8 @@ int main(int argc, char **argv) { } tx_buf.clear(); - tx_buf.insert(tx_buf.end(), kRadiotapLegacy6M, - kRadiotapLegacy6M + sizeof(kRadiotapLegacy6M)); + tx_buf.insert(tx_buf.end(), kStreamRadiotap.begin(), + kStreamRadiotap.end()); tx_buf.insert(tx_buf.end(), dot11.begin(), dot11.end()); tx_buf.insert(tx_buf.end(), psdu.begin(), psdu.end()); bool ok = rtlDevice->send_packet(tx_buf.data(), tx_buf.size());