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
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ those are the ones listed below.
`DEVOURER_USB_PORT=a.b.c` select by USB topology when two adapters share
VID:PID **and** serial.
- `DEVOURER_CHANNEL=N` — monitor channel.
- `DEVOURER_TX_RATE=<rate>[/<bw>][/SGI][/LDPC][/STBC][/ER|/ER106][/DCM]` — TX
- `DEVOURER_TX_RATE=<rate>[/<bw>][/SGI][/LDPC][/STBC][/NOAGG][/ER|/ER106][/DCM]` — TX
mode for rate-less frames (`MCS7/40/SGI`, `VHT2SS_MCS3/80/LDPC`, `1M`...).
Unset = 6M legacy. CCK rates are 2.4 GHz-only; `1M` buys ~9 dB link budget
over `6M`. Rate resolution never reads the band, so `VHT*` rates air on
Expand All @@ -259,7 +259,11 @@ those are the ones listed below.
extended-range PPDU + dual-carrier modulation (`docs/he-extended-range.md`).
The library itself is radiotap-driven — a frame carrying its own rate
radiotap overrides the mode per-packet (ER SU = radiotap-HE FORMAT=EXT_SU).
Programmatic: `SetTxMode` / `ClearTxMode`.
Programmatic: `SetTxMode` / `ClearTxMode`. `/NOAGG` keeps the frame out
of an A-MPDU (`TxMode::no_agg`, `AdapterCaps::tx_no_agg_ok`,
`docs/aggregation.md`).
- `DEVOURER_TX_ALT_RATE=<rate spec>` — txdemo: alternate frames at a second
rate (mixed-rate harness, `tests/tx_no_agg_onair.sh`).
- `DEVOURER_SKIP_RESET=1` — skip `libusb_reset_device` before claim (only
helps when firmware state is intact). Kestrel adapters skip the reset
unconditionally — a USB reset on running firmware can land the chip in the
Expand Down
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,17 @@ target_include_directories(RadiotapNoackSelftest PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src)
add_test(NAME radiotap_noack COMMAND RadiotapNoackSelftest)

# Headless guard for TxMode::no_agg's wire form - the devourer-private
# TX_FLAGS bit that keeps a frame out of an A-MPDU (RadiotapTxFlags.h). Every
# builder layout, NOACK both ways, and the /NOAGG parse token.
add_executable(RadiotapNoaggSelftest
tests/radiotap_noagg_selftest.cpp
)
target_link_libraries(RadiotapNoaggSelftest PRIVATE devourer)
target_include_directories(RadiotapNoaggSelftest PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src)
add_test(NAME radiotap_noagg COMMAND RadiotapNoaggSelftest)

# Headless round-trip guard for the 802.11ax Trigger frame path (src/TriggerTwt.h
# build_basic_trigger + he_ru_alloc, src/TriggerParse.h parse_trigger): build ->
# parse a Basic Trigger, RU-allocation golden vectors. Generation-neutral (the
Expand Down
31 changes: 31 additions & 0 deletions docs/aggregation.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,37 @@ airtime ground truth):
accounting-grade only for un-aggregated frames; under A-MPDU, use the
windowed RX receipts (`src/cell/RxReceipt.h`) as the delivery truth — the
receiver-side ledger is unaffected by TX aggregation.
- **An aggregate airs at its first MPDU's rate, so a mixed-rate stream loses
its per-frame rates.** Consecutive co-queued data frames are folded into one
PPDU at the rate and bandwidth of the frame that opened it; a frame's own
MCS/BW/LDPC/STBC are dropped whenever it lands behind a frame of another
rate. `TxMode::no_agg` (`/NOAGG` in the rate grammar, a devourer-private
radiotap TX_FLAGS bit, `src/RadiotapTxFlags.h`) keeps one frame out: on
Jaguar3 it writes the descriptor `AGG_EN=0` + `BK=1`, the vendor
rtl8822eu recipe for data frames it does not aggregate. Honoured only where
`AdapterCaps::tx_no_agg_ok` is set (Jaguar3); elsewhere the bit is ignored.
Measured with `tests/tx_no_agg_onair.sh` (one 8812EU transmitting, an
8812EU witness, ch36, `0/6`, 4 senders, 1000 B, MCS5 and MCS0 alternating
by frame): without the flag 40.3 % / 40.4 % of the MCS0 frames aired at
MCS0 (two arms; the rest at MCS5), with it 100.0 % / 100.0 %; the MCS5
frames kept their rate in every arm. With the flag parsed but the
descriptor write disabled the flagged frames folded again (39.8 %). The
counterpart: a flagged frame breaks the aggregate around it, and flagging
every other frame — this harness's worst case — cut the witness's heard
rate from 2372 to 1250 frames/s (−47 %). The flag also reaches a
rate-less frame through the `SetTxMode` default
(`DEVOURER_TX_RATE=.../NOAGG`), the harness's `basenoagg` arm: 100.0 % of
both parities at their own rate (×2, 8812EU → 8812EU), while with the
default's `no_agg` not propagated the same arm folds like the control
(38.5 % / 40.1 % vs 39.1 %). The cost of occasional flagged frames
(control traffic inside a video stream) is not measured. The 8822C die,
measured on one 8812CU (an independent rig, same script unchanged,
an 8822BU external-antenna witness, same ch/`0/6`/4 senders/1000 B/MCS5 +
MCS0): the fold is deeper there — 23.8 % / 24.6 % of the MCS0 frames kept
their rate without the flag, 100.0 % / 100.0 % with it — and the
every-other-frame cost correspondingly larger, 2749–2913 frames/s heard
unflagged vs 1156–1159 flagged (about −60 %). One unit per die; the 8812EU
arm was not repeated on that rig.
- `ppdu_cnt` reads 0 on the 8812CU RX used for the bench; `paggr` + `tsfl`
clustering are the working RX markers.

Expand Down
2 changes: 1 addition & 1 deletion docs/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Emitters: L = library, RX/TX/... = demo. Optional fields in [brackets];
| ev | emitter | fields |
|---|---|---|
| `init.timing` | L (`src/InitTimer.h`) + demos | stage ("scope.stage", e.g. "demo.first_rx_frame", "txdemo.first_tx_submit"), ms, [xfers] (register transfers the stage spent on that adapter's transport, USB only — present on the Jaguar3 `j3hal.*` / `j3init.*` stages) |
| `adapter.caps` | RX, TX, doctor, txpower (`examples/common/caps_event.h`) | supported, chip, names, chip_id "0x..", gen, variant, transport, tx_chains, rx_chains, n_ss, stbc, ldpc, sgi, bw_max, bw[] (MHz), txpwr_max, txpwr_step_qdb, txpwr_step_measured, txpwr_min_qdb, txpwr_max_qdb, txpwr_rate_diffs, txpwr_rate_diffs_hw, txpwr_rate_diffs_measured, tune_2g4[]\|null, tune_5g[]\|null, char_2g4[]\|null, char_5g[]\|null, ldpc_rx_ht, ldpc_rx_vht, ldpc_rx_flag, vht_2g4, per_pkt_txpwr, per_pkt_txpwr_steps, per_pkt_txpwr_step_qdb, per_pkt_txpwr_min_qdb, per_pkt_txpwr_max_qdb, per_pkt_txpwr_measured, narrowband, fastretune, ack_responder, station_mode, tx_retry_limit, he_er_su, per_chain_rssi, hw_rx_tsf, hw_beacon_txtsf, tsf_write, xtal_cap_max, xtal_cap_default |
| `adapter.caps` | RX, TX, doctor, txpower (`examples/common/caps_event.h`) | supported, chip, names, chip_id "0x..", gen, variant, transport, tx_chains, rx_chains, n_ss, stbc, ldpc, sgi, bw_max, bw[] (MHz), txpwr_max, txpwr_step_qdb, txpwr_step_measured, txpwr_min_qdb, txpwr_max_qdb, txpwr_rate_diffs, txpwr_rate_diffs_hw, txpwr_rate_diffs_measured, tune_2g4[]\|null, tune_5g[]\|null, char_2g4[]\|null, char_5g[]\|null, ldpc_rx_ht, ldpc_rx_vht, ldpc_rx_flag, vht_2g4, per_pkt_txpwr, per_pkt_txpwr_steps, per_pkt_txpwr_step_qdb, per_pkt_txpwr_min_qdb, per_pkt_txpwr_max_qdb, per_pkt_txpwr_measured, narrowband, fastretune, ack_responder, station_mode, tx_retry_limit, tx_no_agg, he_er_su, per_chain_rssi, hw_rx_tsf, hw_beacon_txtsf, tsf_write, xtal_cap_max, xtal_cap_default |
| `debug.wreg` | L (`DEVOURER_LOG_WRITES`) | addr "0x0nnn", width, val "0x…" |
| `hop.prof` | L (`DEVOURER_HOP_PROF`) | gen, ch, `<stage>_us`…, total_us |
| `tx.fail` | L (send failure; regress.py keys on it) | {status, actual_len, timeout} or {rc, timeout} |
Expand Down
1 change: 1 addition & 0 deletions examples/common/caps_event.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ inline void emit_adapter_caps(EventSink &sink, IRadio *dev) {
.f("ack_responder", c.ack_responder_ok ? 1 : 0)
.f("station_mode", c.station_mode_ok ? 1 : 0)
.f("tx_retry_limit", c.tx_retry_limit_ok ? 1 : 0)
.f("tx_no_agg", c.tx_no_agg_ok ? 1 : 0)
.f("he_er_su", c.he_er_su_ok ? 1 : 0)
.f("per_chain_rssi", c.per_chain_rssi ? 1 : 0)
.f("hw_rx_tsf", c.hw_rx_timestamp ? 1 : 0)
Expand Down
48 changes: 35 additions & 13 deletions examples/tx/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,36 @@ int main(int argc, char **argv) {
if (tx_threads > 1)
logger->info("DEVOURER_TX_THREADS — {} parallel senders", tx_threads);
}
/* DEVOURER_TX_ALT_RATE=<rate spec> — every ODD-counter frame carries its own
* rate radiotap built from this spec (the DEVOURER_TX_RATE grammar, /NOAGG
* included); even frames keep the rate-less default. Needs
* DEVOURER_TX_QOS_DATA: the receiver tells the two apart by the stamped
* counter's parity (rx.seq pctr). The A-MPDU mixed-rate harness
* (tests/tx_no_agg_onair.sh). Replaces the frame each send, so it does not
* combine with the hop markers or DEVOURER_TX_STBC_TOGGLE. */
std::vector<uint8_t> tx_base_buf, tx_alt_buf;
if (const char *e = std::getenv("DEVOURER_TX_ALT_RATE")) {
if (!qos_stamp) {
logger->warn("DEVOURER_TX_ALT_RATE needs DEVOURER_TX_QOS_DATA — ignored");
} else {
const size_t rl = tx_buf[2] | (tx_buf[3] << 8);
tx_base_buf = tx_buf;
/* NOACK like the base frame's rate-less radiotap. */
tx_alt_buf =
devourer::build_stream_radiotap(devourer::parse_tx_mode_str(e));
tx_alt_buf.insert(tx_alt_buf.end(), tx_buf.begin() + rl, tx_buf.end());
logger->info("DEVOURER_TX_ALT_RATE={} — odd-counter frames", e);
}
}
/* Stamp the QoS per-frame counter at MPDU bytes 26..29 (after the frame's
* own radiotap), picking the ALT_RATE variant by parity first. */
auto stamp_counter = [&](std::vector<uint8_t> &b, uint32_t v) {
if (!tx_alt_buf.empty())
b = (v & 1) ? tx_alt_buf : tx_base_buf;
const size_t rl = b.size() >= 4 ? (b[2] | (b[3] << 8)) : b.size();
if (qos_stamp && b.size() >= rl + 26 + 4)
std::memcpy(b.data() + rl + 26, &v, 4);
};
std::atomic<long> tx_counter{0}; /* shared frame-stamp source (threads>1) */
std::vector<std::thread> tx_aux;

Expand Down Expand Up @@ -2336,10 +2366,7 @@ int main(int argc, char **argv) {
/* QoS spike frames carry a per-frame counter at body[0..3] (MPDU bytes
* 26..29) so the receiver can count UNIQUE frames vs hardware re-airings
* (the A-MPDU engine renumbers seqs per aggregate, so seq can't). */
if (qos_stamp && tx_buf.size() >= 10 + 26 + 4) {
uint32_t v = static_cast<uint32_t>(tx_count);
std::memcpy(tx_buf.data() + 10 + 26, &v, 4);
}
stamp_counter(tx_buf, static_cast<uint32_t>(tx_count));
/* Lazy-start the auxiliary senders on the first main-loop pass (the
* chip is up and the first frame primed by then). */
if (tx_threads > 1 && tx_aux.empty()) {
Expand All @@ -2354,10 +2381,7 @@ int main(int argc, char **argv) {
tx_counter.fetch_add(static_cast<long>(bufs.size()));
for (size_t k = 0; k < bufs.size(); ++k) {
auto &b = bufs[k];
if (qos_stamp && b.size() >= 10 + 26 + 4) {
uint32_t v = static_cast<uint32_t>(base + (long)k);
std::memcpy(b.data() + 10 + 26, &v, 4);
}
stamp_counter(b, static_cast<uint32_t>(base + (long)k));
views.push_back(TxPacketView{b.data(), b.size()});
}
rtlDevice->send_packets(views.data(), views.size());
Expand All @@ -2377,11 +2401,9 @@ int main(int argc, char **argv) {
tx_batch_views.clear();
for (long k = 0; k < tx_batch; ++k) {
auto &b = tx_batch_bufs[static_cast<size_t>(k)];
if (qos_stamp && b.size() >= 10 + 26 + 4) {
uint32_t v = static_cast<uint32_t>(
tx_threads > 1 ? tx_counter.fetch_add(1) : tx_count + k);
std::memcpy(b.data() + 10 + 26, &v, 4);
}
stamp_counter(b, static_cast<uint32_t>(tx_threads > 1
? tx_counter.fetch_add(1)
: tx_count + k));
tx_batch_views.push_back(TxPacketView{b.data(), b.size()});
}
const size_t okn = rtlDevice->send_packets(tx_batch_views.data(),
Expand Down
9 changes: 9 additions & 0 deletions src/AdapterCaps.h
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@ struct AdapterCaps {
* FALSE on every other backend: not ported. */
bool station_mode_ok = false;

/* TxMode::no_agg is honoured: a frame carrying the radiotap TX_FLAGS
* kRadiotapTxFlagNoAgg bit (RadiotapTxFlags.h) airs as its own PPDU at its
* own rate and bandwidth even while SetAmpduMode is on. TRUE on Jaguar3,
* on-air-measured on one 8812EU and one 8812CU (tests/tx_no_agg_onair.sh;
* the 8822C folds deeper and pays more for the flag, docs/aggregation.md).
* False everywhere else: the bit is ignored and a flagged frame can still
* be folded into an aggregate at its neighbour's rate. */
bool tx_no_agg_ok = false;

/* --- feature flags --- */
/* Per-packet TX power: a per-frame power trim driven by radiotap
* DBM_TX_POWER (dB delta vs the calibrated table / session base) or a
Expand Down
7 changes: 5 additions & 2 deletions src/RadiotapBuilder.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "RadiotapBuilder.h"

#include "ieee80211_radiotap.h" /* HE field masks */
#include "RadiotapTxFlags.h" /* kRadiotapTxFlagNoAgg */

#include <cctype>
#include <cstdio>
Expand Down Expand Up @@ -296,7 +297,8 @@ std::vector<uint8_t> build_stream_radiotap(const TxMode& cfg) {
}

std::vector<uint8_t> build_stream_radiotap(const TxMode& cfg, bool no_ack) {
const uint16_t tx_flags = no_ack ? kTxFlagsNoAck : 0;
const uint16_t tx_flags = static_cast<uint16_t>(
(no_ack ? kTxFlagsNoAck : 0) | (cfg.no_agg ? kRadiotapTxFlagNoAgg : 0));
switch (cfg.mode) {
case TxMode::Mode::HT: return build_ht(cfg, tx_flags);
case TxMode::Mode::VHT: return build_vht(cfg, tx_flags);
Expand All @@ -315,7 +317,7 @@ TxMode parse_tx_mode_str(const std::string& spec) {
const std::string s = to_upper_stripped(spec.c_str());

/* Split on '/': first token = rate, rest = bandwidth (numeric) or modifier
* flags (SGI / LDPC / STBC). */
* flags (SGI / LDPC / STBC / NOAGG). */
std::vector<std::string> tokens;
size_t start = 0;
while (start <= s.size()) {
Expand All @@ -340,6 +342,7 @@ TxMode parse_tx_mode_str(const std::string& spec) {
if (t == "SGI") cfg.sgi = true;
else if (t == "LDPC") cfg.ldpc = true;
else if (t == "STBC") cfg.stbc = true;
else if (t == "NOAGG") cfg.no_agg = true;
else if (t == "ER" || t == "ER106" || t == "DCM") {
/* HE ER SU / DCM are 802.11ax-only modifiers (Kestrel). */
if (cfg.mode != TxMode::Mode::HE) {
Expand Down
8 changes: 6 additions & 2 deletions src/RadiotapBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,21 @@ std::vector<uint8_t> build_stream_radiotap(const TxMode& mode);
* Only the MT7612U reads this bit. The Realtek backends ignore radiotap
* TX_FLAGS and keep their existing ACK behaviour whatever no_ack says:
* Jaguar1 always marks the descriptor BMC; Jaguar2/3, Kestrel and the
* RTL8733B set BMC from addr1's group bit. */
* RTL8733B set BMC from addr1's group bit. mode.no_agg rides the same field
* as a separate, devourer-private bit (RadiotapTxFlags.h), independent of
* no_ack. */
std::vector<uint8_t> build_stream_radiotap(const TxMode& mode, bool no_ack);

/* Parse a TX-mode spec string into a TxMode. Single slash-separated string:
* <rate>[/<bw>][/SGI][/LDPC][/STBC][/ER|/ER106][/DCM] (case-insensitive)
* <rate>[/<bw>][/SGI][/LDPC][/STBC][/NOAGG][/ER|/ER106][/DCM]
* (case-insensitive)
* <rate> : 6M|9M|12M|18M|24M|36M|48M|54M | MCS0..MCS31 |
* VHT1SS_MCS0..VHT4SS_MCS9 | HE1SS_MCS0..HE4SS_MCS11
* <bw> : 20|40|80|160 (default 20)
* ER / ER106 / DCM (HE rates only, Kestrel): HE ER SU extended-range PPDU
* (242-tone RU, MCS0-2 / 106-tone RU, MCS0) and dual-carrier modulation
* (MCS 0/1/3/4; excludes STBC). Out-of-spec combos are clamped (W log).
* NOAGG : TxMode::no_agg - the frame never joins an A-MPDU.
* Empty or unrecognised falls back to 6M legacy.
*
* The rate is resolved without reference to the band, so a VHT rate on a
Expand Down
14 changes: 14 additions & 0 deletions src/RadiotapTxFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ extern "C" {

namespace devourer {

/* Devourer-private TX_FLAGS bit: "never aggregate this frame" (TxMode::no_agg).
* Under an A-MPDU session the MAC folds consecutive co-queued frames into one
* PPDU aired at its FIRST MPDU's rate and bandwidth, so a frame's own
* MCS/BW/LDPC/STBC are silently replaced (docs/aggregation.md has the
* measurement). A backend that honours the bit (AdapterCaps::tx_no_agg_ok)
* airs the frame as its own PPDU. radiotap.org assigns TX_FLAGS 0x0001-0x0020;
* this bit sits above them and is not a registered assignment, so a future
* radiotap definition of 0x0100 would collide with it. */
constexpr uint16_t kRadiotapTxFlagNoAgg = 0x0100;

inline bool radiotap_tx_no_agg(uint16_t tx_flags) {
return (tx_flags & kRadiotapTxFlagNoAgg) != 0;
}

struct RadiotapMcsField {
bool have_mcs = false;
uint8_t mcs = 0; /* HT MCS index 0..31, valid when have_mcs */
Expand Down
9 changes: 9 additions & 0 deletions src/TxMode.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ struct TxMode {
bool sgi = false;
bool ldpc = false;
bool stbc = false;

/* Never aggregate this frame, even inside an A-MPDU session: it airs as its
* own PPDU at its own rate and bandwidth. Carried per frame in radiotap
* TX_FLAGS (kRadiotapTxFlagNoAgg, RadiotapTxFlags.h); honoured where
* AdapterCaps::tx_no_agg_ok is set, ignored elsewhere. false keeps the
* radiotap byte-identical. As the SetTxMode default it applies to rate-less
* frames only, like the rate fields: a frame with its own rate radiotap
* takes no_agg from its own TX_FLAGS. */
bool no_agg = false;
};

/* The descriptor inputs send_packet writes, derived from a TxMode. `fixed_rate`
Expand Down
6 changes: 6 additions & 0 deletions src/jaguar3/FrameParserJaguar3.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ constexpr size_t RXDESC_SIZE_8822C = 24; /* RX_DESC_SIZE_88XX */
* to aggregate co-queued same-RA/TID frames into an A-MPDU (spike knobs
* DEVOURER_TX_AMPDU / DEVOURER_TX_QSEL; see DeviceConfig debug section). */
#define SET_TX_DESC_AGG_EN_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x08, 12, 1, v)
/* BK (halmac SET_TX_DESC_BK, dword2[16]): break - the MAC does not merge this
* frame into an A-MPDU with its queue neighbours. The vendor rtl8822eu xmit
* path writes AGG_EN=0 + BK=1 for every data frame it does not aggregate
* (EAPOL/ARP/DHCP included); devourer does the same for a TxMode::no_agg
* frame (RadiotapTxFlags.h). */
#define SET_TX_DESC_BK_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x08, 16, 1, v)
#define SET_TX_DESC_MAX_AGG_NUM_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x0C, 17, 5, v)
#define SET_TX_DESC_AMPDU_DENSITY_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x08, 20, 3, v)
#define SET_TX_DESC_RTY_LMT_EN_8822C(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x10, 17, 1, v)
Expand Down
Loading
Loading