From 73737c1b11012ec17962018223a646be4b262bbb Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:27:29 +0300 Subject: [PATCH] Jaguar2: per-packet TX power via TX-descriptor TXPWR_OFSET MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The forum note ("driver already does per-rate, per-packet txpower") is right about the mechanism — each packet's RATE selects its TXAGC row, which devourer already does — but the 8822B/8821C silicon also has a genuine per-PACKET power knob the vendor uses only for phydm dynamic tracking and devourer didn't wire: the TX-descriptor TXPWR_OFSET field (+0x14[30:28]), a hardware LUT applied per-frame on top of the rate's power at ZERO USB cost (a bitfield in the descriptor send_packet already builds). Vendor LUT: 0=none, 1=-3, 2=-7, 3=-11, 4=+3, 5=+6 dB (PHYDM_OFFSET_* / phydm_dynamictxpower.h). Measured first (tests/txpkt_pwr_ofset_onair.sh, chip-RSSI ground): the field moves on-air power standalone — 5/5 LUT steps correct direction, negative rungs tracking dB nearly exactly (-3->-4, -7->-8, -11->-13 raw), positive rungs ceiling-clipped by the near-field RSSI. So it's real, not a phydm-gated no-op. Wired two ways: - send_packet honours a per-frame radiotap DBM_TX_POWER field as a dB delta, quantized to the LUT (jaguar2::txpkt_pwr_step_for_db, unit-tested in tests/txpkt_pwr_selftest.cpp) — the adaptive-link per-packet integration point (wfb-style injection sets it per frame; the vendor skips this field so there's no absolute-dBm convention to violate). - RtlJaguar2Device::SetTxPacketPowerStep sets a session default for rate-less frames (also the measurement knob, DEVOURER_TX_PKT_OFSET in txdemo). Both paths on-air-validated identical (+6->rssi 101, -11->rssi 83 via either the descriptor default or the radiotap field). offset 0 keeps the descriptor byte-identical. The classic 8812AU has no such field — its per-packet power IS rate selection, already supported; 8822C's 2-bit TXPWR_OFSET_TYPE left unwired. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/cmake-multi-platform.yml | 2 +- CLAUDE.md | 16 +++ CMakeLists.txt | 12 ++ src/jaguar2/FrameParserJaguar2.h | 38 ++++- src/jaguar2/RtlJaguar2Device.cpp | 25 +++- src/jaguar2/RtlJaguar2Device.h | 14 ++ tests/txpkt_pwr_ofset_onair.sh | 154 +++++++++++++++++++++ tests/txpkt_pwr_selftest.cpp | 53 +++++++ txdemo/main.cpp | 43 ++++++ 9 files changed, 354 insertions(+), 3 deletions(-) create mode 100755 tests/txpkt_pwr_ofset_onair.sh create mode 100644 tests/txpkt_pwr_selftest.cpp diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 382d206..f42a017 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -147,7 +147,7 @@ jobs: cmake --build build --target WiFiDriver StreamTxDemo StreamDuplexDemo StreamStdinSelftest ToneMaskSelftest BfReportDecodeSelftest SweepSpecSelftest - TxPowerQuantSelftest LinkHealthSelftest + TxPowerQuantSelftest LinkHealthSelftest TxPktPwrSelftest - name: Test working-directory: build diff --git a/CLAUDE.md b/CLAUDE.md index 96dff1d..7600c4d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,6 +86,22 @@ the same stream-count bases as HT (vendor parity) so the offset covers VHT there; the Jaguar2 TXAGC block and the 8814A's packed port are write-only, so their `GetTxPowerState` reports the software shadow (`hw_readback=false`). +**Per-packet TX power (Jaguar2 only).** Distinct from the per-rate TXAGC that +`SetTxPowerOffsetQdb` shifts, the 8822B/8821C TX descriptor has a `TXPWR_OFSET` +field (`+0x14[30:28]`) — a hardware LUT applied per-frame *on top of* the +rate's power at **zero USB cost** (a bitfield in the descriptor `send_packet` +already builds): `0`=none, `1`=-3, `2`=-7, `3`=-11, `4`=+3, `5`=+6 dB +(vendor `PHYDM_OFFSET_*`). `send_packet` honours a per-frame radiotap +`DBM_TX_POWER` field as a dB delta quantized to that LUT +(`jaguar2::txpkt_pwr_step_for_db`, selftest `tests/txpkt_pwr_selftest.cpp`) — +the adaptive-link per-packet power knob (wfb-style injection sets it per +frame); `RtlJaguar2Device::SetTxPacketPowerStep` sets a session default for +rate-less frames. On-air-confirmed both paths (`tests/txpkt_pwr_ofset_onair.sh`: +the LUT tracks on-air power ±dB). The classic 8812AU has no such descriptor +field — its per-packet power *is* per-rate selection (radiotap rate → the +TXAGC table), which the library already does; Jaguar3's `TXPWR_OFSET_TYPE` is +a 2-bit variant left unwired. + **RTL8821AU is Jaguar wave 1** (CHIP_8821 = 7, shares the enum with CHIP_8812), not Jaguar2 — distinct from the Jaguar2 **RTL8821C** (8811CU/8821CU) despite the similar name. The Jaguar2 **8822BU/8812BU** and **8811CU/8821CU** USB parts are diff --git a/CMakeLists.txt b/CMakeLists.txt index 52f1ad9..8416603 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -352,6 +352,18 @@ target_link_libraries(LinkHealthSelftest PRIVATE WiFiDriver) add_test(NAME link_health_classify COMMAND LinkHealthSelftest) +# Headless guard for the Jaguar2 per-packet TX-power quantizer +# (jaguar2::txpkt_pwr_step_for_db) — dB delta -> the descriptor TXPWR_OFSET LUT +# step. Gated on Jaguar2 being built (the header is behind DEVOURER_HAVE_JAGUAR2). +if(DEVOURER_JAGUAR2_8822B OR DEVOURER_JAGUAR2_8821C) + add_executable(TxPktPwrSelftest + tests/txpkt_pwr_selftest.cpp + ) + target_link_libraries(TxPktPwrSelftest PRIVATE WiFiDriver) + target_include_directories(TxPktPwrSelftest PRIVATE src) + add_test(NAME txpkt_pwr_quant COMMAND TxPktPwrSelftest) +endif() + # Headless guard for the beamforming-report decoder (src/BfReportDecode.h) — the # LSB-first Givens-angle unpacking + per-tone variance behind WiFiSenseDemo. It # decodes a real captured VHT MU report and checks the angles against an offline diff --git a/src/jaguar2/FrameParserJaguar2.h b/src/jaguar2/FrameParserJaguar2.h index 3b3288a..1e40891 100644 --- a/src/jaguar2/FrameParserJaguar2.h +++ b/src/jaguar2/FrameParserJaguar2.h @@ -43,6 +43,12 @@ constexpr size_t RXDESC_SIZE_8822B = 24; /* RX_DESC_SIZE_88XX */ #define SET_TX_DESC_DATA_BW_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x14, 5, 2, v) #define SET_TX_DESC_DATA_LDPC_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x14, 7, 1, v) #define SET_TX_DESC_DATA_STBC_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x14, 8, 2, v) +/* Per-packet TX-power offset (halmac TXPWR_OFSET, txdesc+0x14[30:28]). A 3-bit + * hardware LUT applied per-frame on top of the rate's TXAGC — the vendor's + * dynamic-TX-power field (phydm dpt_lv): 0=none, 1=-3dB, 2=-7dB, 3=-11dB, + * 4=+3dB, 5=+6dB (PHYDM_OFFSET_* in phydm_dynamictxpower.h). Zero-cost + * per-packet power trim — a bitfield in the descriptor devourer already builds. */ +#define SET_TX_DESC_TXPWR_OFSET_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x14, 28, 3, v) #define SET_TX_DESC_NAVUSEHDR_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x0C, 15, 1, v) #define SET_TX_DESC_NDPA_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x0C, 22, 2, v) #define SET_TX_DESC_DISQSELSEQ_8822B(d, v) SET_BITS_TO_LE_4BYTE((d) + 0x00, 31, 1, v) @@ -87,11 +93,37 @@ inline void cal_txdesc_chksum_8822b(uint8_t *txdesc) { /* Fill an 8822B data/monitor-inject TX descriptor (48 bytes, zeroed by caller) * and finalise its checksum. Mirrors the Jaguar3 inject field choices. */ +/* Quantize a requested per-packet power delta (dB) to the nearest hardware + * TXPWR_OFSET LUT step. The field is discrete — {0, -3, -7, -11, +3, +6} dB — + * so an adaptive link's continuous request lands on the closest rung. Pure; + * unit-tested in tests/txpkt_pwr_selftest.cpp. */ +inline uint8_t txpkt_pwr_step_for_db(int db) { + static const struct { int db; uint8_t step; } lut[] = { + {0, 0}, {-3, 1}, {-7, 2}, {-11, 3}, {3, 4}, {6, 5}}; + uint8_t best = 0; + int best_err = 1 << 30; + for (const auto &e : lut) { + int err = db > e.db ? db - e.db : e.db - db; + if (err < best_err) { + best_err = err; + best = e.step; + } + } + return best; +} + +/* Inverse: the nominal dB of a LUT step (for readback / logging). */ +inline int txpkt_pwr_db_for_step(uint8_t step) { + static const int db[] = {0, -3, -7, -11, 3, 6}; + return step < 6 ? db[step] : 0; +} + inline void fill_data_tx_desc_8822b(uint8_t *d, uint16_t pkt_size, uint8_t rate_hw, uint8_t rate_id, uint8_t bw, bool short_gi, bool ldpc, uint8_t stbc, bool bmc = false, uint8_t wheader_len = 12, - bool ndpa = false, uint8_t data_sc = 0) { + bool ndpa = false, uint8_t data_sc = 0, + uint8_t pwr_ofset = 0) { SET_TX_DESC_TXPKTSIZE_8822B(d, pkt_size); SET_TX_DESC_OFFSET_8822B(d, static_cast(TXDESC_SIZE_8822B)); SET_TX_DESC_LS_8822B(d, 1); @@ -113,6 +145,10 @@ inline void fill_data_tx_desc_8822b(uint8_t *d, uint16_t pkt_size, * BW, the common case). Non-zero places e.g. a 40-in-80 frame on the primary * lower-40 instead of the block centre. */ SET_TX_DESC_DATA_SC_8822B(d, data_sc); + /* Per-packet TX-power offset (0 = none, byte-identical to the prior desc). + * Inside the 32-byte checksummed region (0x14), so set before the checksum. */ + if (pwr_ofset) + SET_TX_DESC_TXPWR_OFSET_8822B(d, pwr_ofset & 0x7); SET_TX_DESC_DATA_SHORT_8822B(d, short_gi ? 1 : 0); SET_TX_DESC_DATA_LDPC_8822B(d, ldpc ? 1 : 0); SET_TX_DESC_DATA_STBC_8822B(d, stbc & 0x3); diff --git a/src/jaguar2/RtlJaguar2Device.cpp b/src/jaguar2/RtlJaguar2Device.cpp index 9a4ec8c..1d53b8f 100644 --- a/src/jaguar2/RtlJaguar2Device.cpp +++ b/src/jaguar2/RtlJaguar2Device.cpp @@ -638,6 +638,13 @@ devourer::TxPowerState RtlJaguar2Device::GetTxPowerState() { return s; } +void RtlJaguar2Device::SetTxPacketPowerStep(uint8_t step) { + _tx_pkt_pwr_step = step & 0x7; + _logger->info("Jaguar2: per-packet TXPWR_OFSET default step = {} " + "(0=none 1=-3 2=-7 3=-11 4=+3 5=+6 dB)", + step & 0x7); +} + devourer::ThermalStatus RtlJaguar2Device::GetThermalStatus() { devourer::ThermalStatus t; if (!_brought_up) @@ -670,6 +677,11 @@ bool RtlJaguar2Device::send_packet(const uint8_t *packet, size_t length) { bool rate_from_radiotap = false; /* Per-packet hop target from a radiotap CHANNEL field (0 = none present). */ int radiotap_channel = 0; + /* Per-packet TX-power delta (dB) from a radiotap DBM_TX_POWER field; on an + * INJECTED frame we treat it as a delta to the rate's calibrated power and + * quantize to the descriptor TXPWR_OFSET LUT (the vendor skips this field, so + * there is no absolute-dBm convention to violate). INT_MIN = not present. */ + int radiotap_pkt_pwr_db = INT_MIN; auto *rtap_hdr = reinterpret_cast( const_cast(packet)); @@ -692,6 +704,10 @@ bool RtlJaguar2Device::send_packet(const uint8_t *packet, size_t length) { radiotap_channel = devourer::freq_to_chan(get_unaligned_le16(it.this_arg)); break; + case IEEE80211_RADIOTAP_DBM_TX_POWER: + /* Signed 1-byte dB — the per-packet power delta (see the field decl). */ + radiotap_pkt_pwr_db = static_cast(*it.this_arg); + break; case IEEE80211_RADIOTAP_MCS: { uint8_t mcs_flags = it.this_arg[1]; if ((mcs_flags & IEEE80211_RADIOTAP_MCS_BW_MASK) == @@ -803,11 +819,18 @@ bool RtlJaguar2Device::send_packet(const uint8_t *packet, size_t length) { * NDPA so the armed sounding engine (DEVOURER_BF_ARM_SOUNDER, InitWrite) * follows each with a hardware-generated NDP. Same knob as Jaguar-1/-3. */ static const bool ndpa = std::getenv("DEVOURER_TX_NDPA") != nullptr; + /* Per-packet TX power: a radiotap DBM_TX_POWER on this frame (quantized to + * the descriptor LUT) wins per-packet; otherwise the SetTxPacketPowerStep + * default. Zero cost — a descriptor bitfield, no register write. */ + const uint8_t pkt_pwr_step = + radiotap_pkt_pwr_db != INT_MIN + ? jaguar2::txpkt_pwr_step_for_db(radiotap_pkt_pwr_db) + : _tx_pkt_pwr_step.load(); std::vector usb_frame(jaguar2::TXDESC_SIZE_8822B + frame_len, 0); jaguar2::fill_data_tx_desc_8822b( usb_frame.data(), static_cast(frame_len), MRateToHwRate(fixed_rate), rate_id, bw_desc, sgi != 0, ldpc != 0, stbc, - bmc, static_cast(hdrlen >> 1), ndpa, data_sc); + bmc, static_cast(hdrlen >> 1), ndpa, data_sc, pkt_pwr_step); std::memcpy(usb_frame.data() + jaguar2::TXDESC_SIZE_8822B, dot11, frame_len); int rc = _device.bulk_send_sync_ep(_device.first_bulk_out_ep(), diff --git a/src/jaguar2/RtlJaguar2Device.h b/src/jaguar2/RtlJaguar2Device.h index 26c0557..2a749ad 100644 --- a/src/jaguar2/RtlJaguar2Device.h +++ b/src/jaguar2/RtlJaguar2Device.h @@ -80,6 +80,18 @@ class RtlJaguar2Device : public IRtlDevice { devourer::TxPowerState GetTxPowerState() override; devourer::ThermalStatus GetThermalStatus() override; + /* Per-packet TX-power offset — the zero-cost per-frame power trim the + * adaptive link wants (distinct from the per-rate TXAGC that + * SetTxPowerOffsetQdb shifts). Sets the 8822B/8821C TX-descriptor TXPWR_OFSET + * field (a hardware LUT applied on top of the rate's power); no register + * write, no channel-set cost. `step` is the raw 3-bit LUT index: 0=none, + * 1=-3dB, 2=-7dB, 3=-11dB, 4=+3dB, 5=+6dB. Applied as the default to every + * frame lacking a per-packet radiotap DBM_TX_POWER field (which, on an + * injected frame, send_packet honours as a per-packet dB delta quantized to + * this LUT — the true per-packet path). On-air-confirmed: the LUT tracks + * on-air power (tests/txpkt_pwr_ofset_onair.sh). */ + void SetTxPacketPowerStep(uint8_t step); + /* Realtek MP single-tone (CW carrier) — radiate a bare RF local-oscillator * carrier at the tuned channel center. Path A; both Jaguar2 variants, per the * vendor hal_mpt_SetSingleToneTx() branches: OFDM/CCK modulators off, RF path A @@ -119,6 +131,8 @@ class RtlJaguar2Device : public IRtlDevice { * override -1 = efuse per-rate baseline; offset in 0.5 dB index steps. */ std::atomic _tx_pwr_override{-1}; std::atomic _tx_pwr_offset_steps{0}; + /* Default per-packet TXPWR_OFSET LUT step (0 = none) — see SetTxPacketPowerStep. */ + std::atomic _tx_pkt_pwr_step{0}; /* Bring-up completion: gates the live apply in the TX-power setters. */ bool _brought_up = false; /* Re-program TXAGC from the current knob state at the current channel: diff --git a/tests/txpkt_pwr_ofset_onair.sh b/tests/txpkt_pwr_ofset_onair.sh new file mode 100755 index 0000000..789064d --- /dev/null +++ b/tests/txpkt_pwr_ofset_onair.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# MEASURE the Jaguar2 per-packet TX-descriptor power lever (TXPWR_OFSET). Does +# setting the 3-bit descriptor field actually move on-air power standalone (no +# phydm dynamic-txpwr enable), and by the vendor LUT (0=none, 1=-3, 2=-7, 3=-11, +# 4=+3, 5=+6 dB)? Measure-first, before wiring the radiotap per-packet path. +# +# TX = 8822BU (the descriptor field is Jaguar2) at a FIXED rate/power, stepping +# DEVOURER_TX_PKT_OFSET; ground = a second devourer part reporting per-frame +# RSSI (the same chip-RSSI sensor the TX-power slope work uses, since the B210 +# front end limits on near-field frames). If the field works, ground RSSI +# tracks the LUT: step 4 (+3) up ~3 dB, step 5 (+6) up ~6 dB, step 1/2/3 +# (-3/-7/-11) down, all vs step 0. +# +# Usage: sudo -v && tests/txpkt_pwr_ofset_onair.sh [ground_pid] +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="${TXPKT_OUT:-/tmp/devourer-txpkt-ofset}" +CH="${CH:-36}" +TX_PID=0x012d TX_VID=0x2357 # 8822BU (T3U) — has TXPWR_OFSET +GROUND_PID="${1:-0xc812}" GROUND_VID="${GROUND_VID:-0x0bda}" +# LUT steps to sweep, paired with their nominal dB (for the assertion). +STEPS="0 4 5 1 2 3" +declare -A DB=( [0]=0 [4]=3 [5]=6 [1]=-3 [2]=-7 [3]=-11 ) +mkdir -p "$OUT" + +cleanup() { pkill -x WiFiDriverTxDem 2>/dev/null||true; pkill -x WiFiDriverDemo 2>/dev/null||true; true; } +trap cleanup EXIT INT TERM +plugged() { lsusb -d "$(printf '%04x:%04x' "$2" "$1")" >/dev/null 2>&1; } +plugged "$TX_PID" "$TX_VID" || { echo "SKIP: 8822BU (TX) not plugged"; exit 0; } +plugged "$GROUND_PID" "$GROUND_VID" || { echo "SKIP: ground $GROUND_PID not plugged"; exit 0; } + +echo "== building ==" +cmake --build "$ROOT/build" -j --target WiFiDriverTxDemo WiFiDriverDemo >/dev/null || exit 1 + +echo "== ground RX ($GROUND_PID) on ch$CH ==" +: >"$OUT/ground.log" +sudo -n env DEVOURER_PID="$GROUND_PID" DEVOURER_VID="$GROUND_VID" \ + DEVOURER_CHANNEL="$CH" DEVOURER_STREAM_OUT=1 \ + stdbuf -oL timeout 300 "$ROOT/build/WiFiDriverDemo" 2>"$OUT/ground.err" \ + | while IFS= read -r l; do printf '%s %s\n' "$(date +%s.%N)" "$l"; done \ + >>"$OUT/ground.log" & +GJ=$! +sleep 12 + +: >"$OUT/cells.txt" +for step in $STEPS; do + t0="$(date +%s.%N)" + # Fixed rate MCS3, fixed base power, only the per-packet offset varies. + sudo -n env DEVOURER_PID="$TX_PID" DEVOURER_VID="$TX_VID" DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE=MCS3 DEVOURER_TX_PWR=40 DEVOURER_TX_PKT_OFSET="$step" \ + DEVOURER_TX_GAP_US=1500 \ + timeout 12 "$ROOT/build/WiFiDriverTxDemo" >/dev/null 2>&1 || true + t1="$(date +%s.%N)" + echo "$step ${DB[$step]} $t0 $t1" >>"$OUT/cells.txt" + sleep 2 +done +sudo -n pkill -x WiFiDriverDemo 2>/dev/null; wait "$GJ" 2>/dev/null + +# --- radiotap path: same effect via a per-packet DBM_TX_POWER field (not the +# device default) — proves send_packet honours per-frame power. Two cells +# at the LUT extremes; append to cells.txt with a 'r' tag column. +echo "== ground still up: radiotap DBM_TX_POWER cells ==" >&2 +sudo -n env DEVOURER_PID="$GROUND_PID" DEVOURER_VID="$GROUND_VID" \ + DEVOURER_CHANNEL="$CH" DEVOURER_STREAM_OUT=1 \ + stdbuf -oL timeout 90 "$ROOT/build/WiFiDriverDemo" 2>/dev/null \ + | while IFS= read -r l; do printf '%s %s\n' "$(date +%s.%N)" "$l"; done \ + >>"$OUT/ground.log" & +GJ2=$! +sleep 10 +for pair in "0:0" "6:6" "-11:-11"; do + db="${pair%%:*}" + t0="$(date +%s.%N)" + sudo -n env DEVOURER_PID="$TX_PID" DEVOURER_VID="$TX_VID" DEVOURER_CHANNEL="$CH" \ + DEVOURER_TX_RATE=MCS3 DEVOURER_TX_PWR=40 DEVOURER_TX_PKT_PWR_DB="$db" \ + DEVOURER_TX_GAP_US=1500 \ + timeout 12 "$ROOT/build/WiFiDriverTxDemo" >/dev/null 2>&1 || true + t1="$(date +%s.%N)" + echo "r$db $db $t0 $t1" >>"$OUT/cells.txt" + sleep 2 +done +sudo -n pkill -x WiFiDriverDemo 2>/dev/null; wait "$GJ2" 2>/dev/null + +python3 - "$OUT/ground.log" "$OUT/cells.txt" <<'PYEOF' +import re, statistics, sys +frames = [] +rx = re.compile(r"^([0-9.]+) .*.*\brssi=(-?\d+),") +for line in open(sys.argv[1], errors="replace"): + m = rx.match(line) + if m: + frames.append((float(m.group(1)), int(m.group(2)))) +def med(t0, t1): + win = [r for (t, r) in frames if t0 + 6 <= t <= t1 - 1] + return (int(statistics.median(win)), len(win)) if len(win) >= 15 else (None, len(win)) + +desc = [] # (step:int, db, rssi) — device-default path +rtap = [] # (db, rssi) — radiotap path +for line in open(sys.argv[2]): + label, db, t0, t1 = line.split(); db, t0, t1 = int(db), float(t0), float(t1) + rssi, n = med(t0, t1) + if rssi is None: + print(f"{label} ({db:+d} dB): too few frames ({n})"); continue + if label.startswith("r"): + rtap.append((db, rssi)) + print(f" db={db:+d} rssi_med={rssi} frames={n}") + else: + desc.append((int(label), db, rssi)) + print(f" step={label} nominal_db={db:+d} rssi_med={rssi} frames={n}") + +fail = 0 +# --- device-default (descriptor LUT) path --- +base = next((r[2] for r in desc if r[0] == 0), None) +if base is None or len(desc) < 4: + print("RESULT-DESC inconclusive (no step-0 baseline)"); fail = 1 +else: + print(f"\ndescriptor-default baseline (step 0) rssi = {base}") + moved = correct = total = 0 + for step, db, rssi in desc: + if step == 0: + continue + total += 1 + d = rssi - base + moved += abs(d) >= 1 + correct += (db > 0 and d > 0) or (db < 0 and d < 0) + print(f" step {step} ({db:+3d} dB): rssi {base}->{rssi} = {d:+d} raw") + if moved == 0: + print("RESULT-DESC INERT — descriptor TXPWR_OFSET did not move on-air power") + fail = 1 + elif correct >= total - 1: + print(f"RESULT-DESC WORKS — {correct}/{total} steps correct direction") + else: + print(f"RESULT-DESC PARTIAL — {correct}/{total} correct sign"); fail = 1 + +# --- radiotap per-packet path --- +rbase = next((r[1] for r in rtap if r[0] == 0), None) +if rbase is None or len(rtap) < 3: + print("RESULT-RADIOTAP skipped (no cells)") +else: + print(f"\nradiotap DBM_TX_POWER baseline (0 dB) rssi = {rbase}") + rcorrect = rtotal = 0 + for db, rssi in rtap: + if db == 0: + continue + rtotal += 1 + d = rssi - rbase + rcorrect += (db > 0 and d > 0) or (db < 0 and d < 0) + print(f" radiotap {db:+3d} dB: rssi {rbase}->{rssi} = {d:+d} raw") + if rcorrect == rtotal and rtotal >= 2: + print(f"RESULT-RADIOTAP WORKS — {rcorrect}/{rtotal} per-packet power via " + f"radiotap DBM_TX_POWER moved on-air power correctly") + else: + print(f"RESULT-RADIOTAP FAIL — {rcorrect}/{rtotal} correct"); fail = 1 + +sys.exit(1 if fail else 0) +PYEOF diff --git a/tests/txpkt_pwr_selftest.cpp b/tests/txpkt_pwr_selftest.cpp new file mode 100644 index 0000000..79f8522 --- /dev/null +++ b/tests/txpkt_pwr_selftest.cpp @@ -0,0 +1,53 @@ +/* Headless guard for the per-packet TX-power quantizer (jaguar2 + * txpkt_pwr_step_for_db): maps a requested per-packet dB delta to the nearest + * hardware TXPWR_OFSET LUT step {0, -3, -7, -11, +3, +6} dB. A rounding + * regression fails `ctest` instead of only surfacing as wrong on-air power. */ +#include + +#include "jaguar2/FrameParserJaguar2.h" + +static int g_fail = 0; + +static void expect(int db, uint8_t want_step) { + uint8_t got = jaguar2::txpkt_pwr_step_for_db(db); + if (got == want_step) + return; + ++g_fail; + std::printf("FAIL: %+d dB -> step %u, want %u (=%+d dB)\n", db, got, + want_step, jaguar2::txpkt_pwr_db_for_step(want_step)); +} + +int main() { + /* Exact LUT points. */ + expect(0, 0); + expect(-3, 1); + expect(-7, 2); + expect(-11, 3); + expect(3, 4); + expect(6, 5); + + /* Nearest-rung rounding. */ + expect(-1, 0); /* 1 from 0, 2 from -3 -> step 0 */ + expect(-2, 1); /* 2 from 0, 1 from -3 -> -3 */ + expect(-5, 1); /* 2 from -3, 2 from -7 -> ties to first (-3) */ + expect(-9, 2); /* 2 from -7, 2 from -11 -> ties to first (-7) */ + expect(-20, 3); /* clamps to the most-negative rung */ + expect(1, 0); /* 1 from 0, 2 from +3 -> 0 */ + expect(2, 4); /* 2 from 0, 1 from +3 -> +3 */ + expect(4, 4); /* +3 */ + expect(5, 5); /* 2 from +3, 1 from +6 -> +6 */ + expect(20, 5); /* clamps to +6 */ + + /* Inverse. */ + if (jaguar2::txpkt_pwr_db_for_step(2) != -7) { + ++g_fail; + std::printf("FAIL: db_for_step(2) != -7\n"); + } + + if (g_fail) { + std::printf("%d failure(s)\n", g_fail); + return 1; + } + std::printf("txpkt-pwr quantizer selftest: all OK\n"); + return 0; +} diff --git a/txdemo/main.cpp b/txdemo/main.cpp index 7575799..40d34e6 100644 --- a/txdemo/main.cpp +++ b/txdemo/main.cpp @@ -89,6 +89,26 @@ static std::vector build_frame_with_channel(uint16_t freq, return f; } +/* Rebuild a rate-less beacon's radiotap to carry a DBM_TX_POWER field (signed + * dB), for validating the library's per-packet TX-power path (Jaguar2 honours + * it as a per-packet offset quantized to the TXPWR_OFSET LUT). Keeps it + * rate-less so SetTxMode still supplies the rate. `body`/`body_len` is the + * 802.11 frame (radiotap already stripped). Present = DBM_TX_POWER(bit10) | + * TX_FLAGS(bit15); fields in ascending bit order with natural alignment. */ +static std::vector build_frame_with_pkt_pwr(int8_t db, + const uint8_t *body, + size_t body_len) { + std::vector f; + const uint8_t hdr[] = {0x00, 0x00, 0x0c, 0x00, 0x00, 0x84, 0x00, 0x00}; + f.insert(f.end(), hdr, hdr + sizeof(hdr)); + f.push_back(static_cast(db)); /* DBM_TX_POWER (offset 8, 1 signed B) */ + f.push_back(0x00); /* pad to 2-byte align for TX_FLAGS */ + f.push_back(0x08); /* TX_FLAGS le16 */ + f.push_back(0x00); + f.insert(f.end(), body, body + body_len); + return f; +} + static int g_rx_count = 0; static void packetProcessor(const Packet &packet) { /* C2H packets are chip-side status (one per TX during concurrent TX+RX on @@ -442,6 +462,17 @@ int main(int argc, char **argv) { if (const char *p = std::getenv("DEVOURER_TX_PWR")) rtlDevice->SetTxPower(static_cast(std::strtol(p, nullptr, 0))); + /* DEVOURER_TX_PKT_OFSET=N — (Jaguar2 8822B/8821C) default per-packet + * TXPWR_OFSET LUT step written into every TX descriptor: 0=none, 1=-3dB, + * 2=-7dB, 3=-11dB, 4=+3dB, 5=+6dB. The measurement driver for the descriptor + * per-packet power lever (tests/txpkt_pwr_ofset_onair.sh). */ +#if defined(DEVOURER_HAVE_JAGUAR2) + if (const char *p = std::getenv("DEVOURER_TX_PKT_OFSET")) { + if (jag2) + jag2->SetTxPacketPowerStep(static_cast(std::strtol(p, nullptr, 0))); + } +#endif + /* DEVOURER_TX_WITH_RX — concurrent RX alongside the TX loop on the SAME * claimed adapter. Two modes: * @@ -567,6 +598,18 @@ int main(int argc, char **argv) { std::vector tx_buf(beacon_frame, beacon_frame + sizeof(beacon_frame)); + /* DEVOURER_TX_PKT_PWR_DB=N — carry a per-packet TX-power delta (dB) in the + * beacon's radiotap DBM_TX_POWER field. Exercises the LIBRARY's per-packet + * path (vs DEVOURER_TX_PKT_OFSET, which sets the device default) — the + * adaptive-link integration point. Rebuilds the rate-less radiotap; the + * 802.11 body starts after the beacon's 10-byte radiotap prefix. */ + if (const char *e = std::getenv("DEVOURER_TX_PKT_PWR_DB")) { + const int8_t db = static_cast(std::strtol(e, nullptr, 0)); + tx_buf = build_frame_with_pkt_pwr(db, beacon_frame + 10, + sizeof(beacon_frame) - 10); + logger->info("DEVOURER_TX_PKT_PWR_DB={} — per-packet power via radiotap", db); + } + /* 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 * writes real_packet_length (= PSDU) into the 16-bit TX-desc PKT_SIZE, so N