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
2 changes: 1 addition & 1 deletion .github/workflows/cmake-multi-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ jobs:
cmake --build build --target
WiFiDriver StreamTxDemo StreamDuplexDemo StreamStdinSelftest
ToneMaskSelftest BfReportDecodeSelftest SweepSpecSelftest
TxPowerQuantSelftest LinkHealthSelftest TxPktPwrSelftest
TxPowerQuantSelftest LinkHealthSelftest TxCapsSelftest TxPktPwrSelftest

- name: Test
working-directory: build
Expand Down
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ add_library(WiFiDriver
src/TxPower.cpp
src/TxPower.h
src/ThermalStatus.h
src/TxCaps.h
src/LinkHealth.cpp
src/LinkHealth.h
src/IRtlDevice.h
Expand Down Expand Up @@ -352,6 +353,15 @@ target_link_libraries(LinkHealthSelftest PRIVATE WiFiDriver)

add_test(NAME link_health_classify COMMAND LinkHealthSelftest)

# Headless guard for the TX-capability derivation (src/TxCaps.h) — the
# STBC-needs-2-chains rule the send_packet 1T1R guard relies on.
add_executable(TxCapsSelftest
tests/tx_caps_selftest.cpp
)
target_link_libraries(TxCapsSelftest PRIVATE WiFiDriver)

add_test(NAME tx_caps_derive COMMAND TxCapsSelftest)

# 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).
Expand Down
8 changes: 8 additions & 0 deletions src/IRtlDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "RxSense.h"
#include "SelectedChannel.h"
#include "ThermalStatus.h"
#include "TxCaps.h"
#include "TxMode.h"
#include "TxPower.h"

Expand Down Expand Up @@ -123,6 +124,13 @@ class IRtlDevice {
* adaptive-link controller. Default returns an all-invalid reading. */
virtual devourer::ThermalStatus GetThermalStatus() { return {}; }

/* Per-chip TX capability report (see src/TxCaps.h): spatial streams, STBC /
* LDPC / SGI support, max bandwidth — derived from the chip identity resolved
* at construction. A caller (or send_packet) uses it to avoid requesting a
* feature the silicon can't do (e.g. STBC on a 1T1R part, which produces a
* frame that never decodes). Default returns supported=false. */
virtual devourer::TxCaps GetTxCaps() { return {}; }

virtual bool send_packet(const uint8_t *packet, size_t length) = 0;
virtual SelectedChannel GetSelectedChannel() = 0;

Expand Down
44 changes: 44 additions & 0 deletions src/TxCaps.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* Per-chip TX capability report — what modulation features a given adapter can
* actually transmit. An adaptive link (or wfb-ng) that blindly sets STBC/LDPC
* radiotap flags on a card that lacks them produces malformed frames that never
* decode — a well-known OpenIPC-FPV footgun ("LDPC/STBC forced on a card that
* lacks them → link dies", e.g. the 1T1R 8811AU/8821AU/8821CU where STBC needs
* ≥2 TX chains). devourer already resolves chip identity at construction; this
* surfaces it so callers can gate features, and send_packet drops an
* unsupported STBC request rather than airing garbage.
*/
#ifndef DEVOURER_TX_CAPS_H
#define DEVOURER_TX_CAPS_H

#include <cstdint>

namespace devourer {

struct TxCaps {
bool supported = false; /* false on a generation that hasn't wired this */
uint8_t n_ss = 0; /* spatial streams the RF supports (1/2/3/4) */
bool stbc_ok = false; /* STBC needs ≥2 TX chains — false on 1T1R parts */
bool ldpc_ok = false; /* LDPC coding supported */
bool sgi_ok = false; /* short guard interval */
uint8_t bw_max_mhz = 20; /* widest TX bandwidth (20/40/80) */
};

/* Build caps from the chain count. The load-bearing rule: STBC needs ≥2 TX
* chains, so a 1T1R part (chains==1) reports stbc_ok=false — the invariant the
* send_packet guard relies on. Pure; unit-tested in tests/tx_caps_selftest.cpp.
* LDPC/SGI/bw default true/80 for the 802.11ac Jaguar family. */
inline TxCaps tx_caps_for_chains(uint8_t chains, bool ldpc = true,
bool sgi = true, uint8_t bw_max_mhz = 80) {
TxCaps c;
c.supported = true;
c.n_ss = chains;
c.stbc_ok = chains >= 2;
c.ldpc_ok = ldpc;
c.sgi_ok = sgi;
c.bw_max_mhz = bw_max_mhz;
return c;
}

} // namespace devourer

#endif /* DEVOURER_TX_CAPS_H */
20 changes: 20 additions & 0 deletions src/jaguar1/RtlJaguarDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,19 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) {

ptxdesc = (struct tx_desc *)usb_frame;

/* Drop an STBC request the chip can't honour: STBC needs >=2 TX chains, so a
* 1T1R part (8811AU/8821AU) that airs an STBC-marked frame produces a
* malformed PPDU that never decodes (a known adaptive-link footgun). Warn
* once; leave 2T2R/4T4R untouched. */
if (stbc && !GetTxCaps().stbc_ok) {
static bool warned = false;
if (!warned) {
_logger->warn("STBC requested but this chip is 1T1R (no STBC) — dropping "
"the STBC flag to keep frames decodable");
warned = true;
}
stbc = 0;
}
_logger->debug("fixed rate:{}, sgi:{}, radiotap_bwidth:{}, ldpc:{}, stbc:{}",
(int)fixed_rate, (int)sgi, (int)bwidth, (int)ldpc, (int)stbc);

Expand Down Expand Up @@ -845,6 +858,13 @@ bool RtlJaguarDevice::ReApplyTxPower() {
return true;
}

devourer::TxCaps RtlJaguarDevice::GetTxCaps() {
/* numTotalRfPath is the TX chain count the EFUSE RF-type resolves to (1 on
* the 8811AU/8821AU 1T1R cuts, 2 on 8812AU, 4 on 8814AU). All Jaguar-1 AC
* parts do LDPC/SGI and VHT80. */
return devourer::tx_caps_for_chains(_eepromManager->numTotalRfPath);
}

devourer::TxPowerState RtlJaguarDevice::GetTxPowerState() {
devourer::TxPowerState s;
s.valid = true;
Expand Down
4 changes: 4 additions & 0 deletions src/jaguar1/RtlJaguarDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ class RtlJaguarDevice : public IRtlDevice {
void SetTxPowerIndexOverride(int idx) override;
bool ReApplyTxPower() override;
devourer::TxPowerState GetTxPowerState() override;
/* Per-chip TX caps (IRtlDevice): n_ss + STBC/LDPC/SGI/bw from the EFUSE
* RF-type. STBC needs >=2 chains, so 1T1R cuts (8811AU/8821AU) report
* stbc_ok=false and send_packet drops an STBC request. */
devourer::TxCaps GetTxCaps() override;
/* 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);
Expand Down
18 changes: 18 additions & 0 deletions src/jaguar2/RtlJaguar2Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,12 @@ void RtlJaguar2Device::SetTxPacketPowerStep(uint8_t step) {
step & 0x7);
}

devourer::TxCaps RtlJaguar2Device::GetTxCaps() {
/* 8821C is 1T1R (no STBC); 8822B is 2T2R. */
const uint8_t chains = _variant == jaguar2::ChipVariant::C8821C ? 1 : 2;
return devourer::tx_caps_for_chains(chains);
}

devourer::ThermalStatus RtlJaguar2Device::GetThermalStatus() {
devourer::ThermalStatus t;
if (!_brought_up)
Expand Down Expand Up @@ -801,6 +807,18 @@ bool RtlJaguar2Device::send_packet(const uint8_t *packet, size_t length) {
}
}

/* Drop STBC on the 1T1R variant (8821C) — STBC needs >=2 TX chains, so an
* STBC-marked frame there is malformed and never decodes. Warn once. */
if (stbc && !GetTxCaps().stbc_ok) {
static bool warned = false;
if (!warned) {
_logger->warn("STBC requested on a 1T1R chip (8821C) — dropping the STBC "
"flag to keep frames decodable");
warned = true;
}
stbc = 0;
}

const uint8_t *dot11 = packet + radiotap_length;
bool bmc = frame_len >= 6 && (dot11[4] & 0x01);

Expand Down
3 changes: 3 additions & 0 deletions src/jaguar2/RtlJaguar2Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ class RtlJaguar2Device : public IRtlDevice {
bool ReApplyTxPower() override;
devourer::TxPowerState GetTxPowerState() override;
devourer::ThermalStatus GetThermalStatus() override;
/* Per-chip TX caps (IRtlDevice): the 8821C is 1T1R (no STBC), the 8822B
* 2T2R. send_packet drops an STBC request the variant can't honour. */
devourer::TxCaps GetTxCaps() override;

/* Per-packet TX-power offset — the zero-cost per-frame power trim the
* adaptive link wants (distinct from the per-rate TXAGC that
Expand Down
9 changes: 9 additions & 0 deletions src/jaguar3/RtlJaguar3Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,10 @@ bool RtlJaguar3Device::ReApplyTxPower() {
return true;
}

devourer::TxCaps RtlJaguar3Device::GetTxCaps() {
return devourer::tx_caps_for_chains(2); /* 8822C/8822E are 2T2R */
}

devourer::TxPowerState RtlJaguar3Device::GetTxPowerState() {
devourer::TxPowerState s;
s.valid = true;
Expand Down Expand Up @@ -976,6 +980,11 @@ bool RtlJaguar3Device::send_packet(const uint8_t *packet, size_t length) {
* as NDPA so the armed sounding engine (DEVOURER_BF_ARM_SOUNDER, InitWrite)
* follows each with a hardware NDP. Same knob as the Jaguar-1 path. */
static const bool ndpa = std::getenv("DEVOURER_TX_NDPA") != nullptr;
/* STBC guard (IRtlDevice contract) — 8822C/8822E are 2T2R so this never
* fires today, but keeps the invariant uniform across families: never air an
* STBC frame the chip can't do. */
if (stbc && !GetTxCaps().stbc_ok)
stbc = 0;
std::vector<uint8_t> usb_frame(jaguar3::TXDESC_SIZE_8822C + frame_len, 0);
jaguar3::fill_data_tx_desc_8822c(
usb_frame.data(), static_cast<uint16_t>(frame_len),
Expand Down
2 changes: 2 additions & 0 deletions src/jaguar3/RtlJaguar3Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class RtlJaguar3Device : public IRtlDevice {
bool ReApplyTxPower() override;
devourer::TxPowerState GetTxPowerState() override;
devourer::ThermalStatus GetThermalStatus() override;
/* Per-chip TX caps (IRtlDevice): 8822C/8822E are 2T2R (STBC ok). */
devourer::TxCaps GetTxCaps() override;
/* Runtime TX-mode default — applied in send_packet when the radiotap carries
* no rate. Without this the Jaguar3 TX path fell back to MGN_1M for rate-less
* frames (so DEVOURER_TX_RATE/an MCS flood went on-air at 1 Mbps): the feature
Expand Down
83 changes: 83 additions & 0 deletions tests/stbc_1t1r_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Validate the STBC/1T1R safety guard (GetTxCaps + send_packet). STBC needs >=2
# TX chains, so forcing it on a 1T1R part produces a malformed PPDU that never
# decodes — the "LDPC/STBC forced on a card that lacks them => link dies"
# footgun from the adaptive-link recipes. Measured A/B on the 8821AU: forcing
# STBC (unguarded) delivered 0 frames; with the guard (STBC dropped) it
# delivered 7000. This test asserts the guard's behaviour per DUT:
#
# 1T1R (8821AU / 8821CU): inject MCS1/STBC -> the "dropping the STBC flag"
# warning fires AND the frame still delivers (STBC dropped to clean MCS1).
# 2T2R (8812AU / 8822BU): inject MCS1/STBC -> NO warning (STBC honoured) and
# the frame delivers.
#
# Ground = a second devourer part counting <devourer-tx-hit> from the canonical SA.
#
# Usage: sudo -v && tests/stbc_1t1r_check.sh
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="${STBC_OUT:-/tmp/devourer-stbc-1t1r}"
CH="${CH:-36}"
mkdir -p "$OUT"

PASS=0; FAIL=0; SKIP=0
pass() { echo " PASS: $*"; PASS=$((PASS+1)); }
fail() { echo " FAIL: $*"; FAIL=$((FAIL+1)); }
skip() { echo " SKIP: $*"; SKIP=$((SKIP+1)); }

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; }

echo "== building =="
cmake --build "$ROOT/build" -j --target WiFiDriverTxDemo WiFiDriverDemo >/dev/null || exit 1

# DUT table: pid vid n_ss(1|2) ground_pid ground_vid
DUTS=(
"0x0120 0x2357 1 0x8812 0x0bda" # 8821AU (1T1R) ground 8812AU
"0xc811 0x0bda 1 0x8812 0x0bda" # 8821CU (1T1R) ground 8812AU
"0x8812 0x0bda 2 0xc812 0x0bda" # 8812AU (2T2R) ground 8822CU
"0x012d 0x2357 2 0xc812 0x0bda" # 8822BU (2T2R) ground 8822CU
)

for dut in "${DUTS[@]}"; do
read -r PID VID NSS GPID GVID <<<"$dut"
if ! plugged "$PID" "$VID"; then skip "$PID not plugged"; continue; fi
if ! plugged "$GPID" "$GVID"; then skip "$PID: ground $GPID not plugged"; continue; fi
tag="${PID#0x}"
echo "== DUT $PID (${NSS}T${NSS}R), ground $GPID =="
: >"$OUT/$tag-g.log"
sudo -n env DEVOURER_PID="$GPID" DEVOURER_VID="$GVID" DEVOURER_CHANNEL="$CH" \
stdbuf -oL timeout 45 "$ROOT/build/WiFiDriverDemo" 2>/dev/null \
| grep --line-buffered "tx-hit" >"$OUT/$tag-g.log" &
GJ=$!
sleep 10
sudo -n env DEVOURER_PID="$PID" DEVOURER_VID="$VID" DEVOURER_CHANNEL="$CH" \
DEVOURER_TX_RATE=MCS1/STBC DEVOURER_TX_GAP_US=2000 \
timeout 18 "$ROOT/build/WiFiDriverTxDemo" >"$OUT/$tag-tx.log" 2>&1 || true
sudo -n pkill -x WiFiDriverDemo 2>/dev/null; wait "$GJ" 2>/dev/null
sleep 2

warned=$(grep -c "dropping the STBC flag" "$OUT/$tag-tx.log")
hits=$(grep -oE "hits=[0-9]+" "$OUT/$tag-g.log" | tail -1 | grep -oE "[0-9]+")
hits=${hits:-0}
echo " warning fired: $warned, delivery hits: $hits"

if [ "$NSS" = "1" ]; then
if [ "$warned" -ge 1 ] && [ "$hits" -ge 200 ]; then
pass "$PID (1T1R): STBC dropped + frame delivered ($hits hits)"
else
fail "$PID (1T1R): want warning+delivery, got warned=$warned hits=$hits"
fi
else
if [ "$warned" -eq 0 ] && [ "$hits" -ge 200 ]; then
pass "$PID (2T2R): STBC honoured, no warning, delivered ($hits hits)"
else
fail "$PID (2T2R): want no-warning+delivery, got warned=$warned hits=$hits"
fi
fi
done

echo
echo "== stbc-1t1r guard: PASS=$PASS FAIL=$FAIL SKIP=$SKIP =="
[ "$FAIL" -eq 0 ]
48 changes: 48 additions & 0 deletions tests/tx_caps_selftest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* Headless guard for the TX-capability derivation (src/TxCaps.h
* tx_caps_for_chains): the load-bearing rule is that STBC needs >=2 TX chains,
* so a 1T1R part must report stbc_ok=false — that's what the send_packet STBC
* guard relies on to not air a malformed frame on 8811AU/8821AU/8821CU. A
* regression here fails `ctest` instead of only surfacing as a dead link. */
#include <cstdio>

#include "TxCaps.h"

static int g_fail = 0;

static void expect(uint8_t chains, uint8_t want_nss, bool want_stbc) {
const devourer::TxCaps c = devourer::tx_caps_for_chains(chains);
if (c.supported && c.n_ss == want_nss && c.stbc_ok == want_stbc)
return;
++g_fail;
std::printf("FAIL: chains=%u -> n_ss=%u stbc_ok=%d, want n_ss=%u stbc_ok=%d\n",
chains, c.n_ss, c.stbc_ok ? 1 : 0, want_nss, want_stbc ? 1 : 0);
}

int main() {
/* 1T1R cuts (8811AU/8821AU/8821CU): STBC impossible. */
expect(1, 1, false);
/* 2T2R (8812AU/8822BU/8822CU/8822EU): STBC ok. */
expect(2, 2, true);
/* 4T4R (8814AU): STBC ok. */
expect(4, 4, true);

/* Defaults for the AC family. */
const devourer::TxCaps c = devourer::tx_caps_for_chains(2);
if (!(c.ldpc_ok && c.sgi_ok && c.bw_max_mhz == 80)) {
++g_fail;
std::printf("FAIL: AC defaults (ldpc=%d sgi=%d bw=%u)\n", c.ldpc_ok,
c.sgi_ok, c.bw_max_mhz);
}
/* The default-constructed (unwired) caps report unsupported. */
if (devourer::TxCaps{}.supported) {
++g_fail;
std::printf("FAIL: default TxCaps should be unsupported\n");
}

if (g_fail) {
std::printf("%d failure(s)\n", g_fail);
return 1;
}
std::printf("tx-caps selftest: all OK\n");
return 0;
}
Loading