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
13 changes: 9 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,16 @@ Both `WiFiDriverDemo` and `WiFiDriverTxDemo` honour:
0/4 = path A CCK/OFDM, 1/5 = B, 2/6 = C, 3/7 = D). Default all paths (`0xFF`).
`0x11` = A only, `0x33` = A+B, `0x77` = A+B+C. Validated on the 8814: a masked
path's per-chain RSSI collapses to the noise floor while the enabled ones stay
up. Applied after the channel set (so it survives IQK); a later channel switch
reverts it, so it targets a single-channel RX capture. The knob for measuring
the chip's hardware maximal-ratio-combining gain — compare frame delivery at
up. Routes through the runtime `RtlJaguarDevice::SetRxPathMask(mask)` /
`GetRxPathMask()` API (the adaptive-link spatial-diversity lever — a
controller with a motion/fade signal switches chains live instead of on the
toggle's fixed timer); once set, the mask is **sticky** — re-applied after
every `SetMonitorChannel` (a channel set runs IQK, which saves/restores
`0x808`), so it survives channel hops. The knob for measuring the chip's
hardware maximal-ratio-combining gain — compare frame delivery at
`0x11`/`0x33`/`0x77`/`0xFF` over a *marginal* link (see
`docs/measuring-spatial-diversity.md`). A **toggle spec**
`docs/measuring-spatial-diversity.md`); register + stickiness validation in
`tests/rx_path_mask_check.sh`. A **toggle spec**
`0xAA:0xBB[:0xCC]@<ms>` (e.g. `0x22:0xFF@300`) cycles the mask on a timer
thread instead of setting it once, printing a `<devourer-rxpath-mask>0xNN`
marker inline with the frame stream on each switch — the stimulus for the
Expand Down
17 changes: 16 additions & 1 deletion src/jaguar1/RtlJaguarDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ void RtlJaguarDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
start_rx_path_toggle(masks, interval_ms);
} else {
auto mask = static_cast<uint8_t>(std::strtoul(spec.c_str(), nullptr, 0));
_device.rtw_write8(0x808, mask);
SetRxPathMask(mask);
_logger->info("DEVOURER_RX_PATHS: RX-path mask 0x808[7:0]=0x{:02x}", mask);
}
}
Expand Down Expand Up @@ -753,6 +753,21 @@ void RtlJaguarDevice::SetMonitorChannel(SelectedChannel channel) {
_channel = channel;
_radioManagement->set_channel_bwmode(channel.Channel, channel.ChannelOffset,
channel.ChannelWidth);
/* Re-apply a runtime RX-path mask: the channel set runs IQK, which
* saves/restores 0x808 and thereby reverts the mask to the table default.
* Gated on the mask having been set (SetRxPathMask), so the default
* all-paths behaviour is byte-identical when the knob is unused. */
if (_rx_path_mask >= 0)
_device.rtw_write8(0x808, static_cast<uint8_t>(_rx_path_mask.load()));
}

void RtlJaguarDevice::SetRxPathMask(uint8_t mask) {
_rx_path_mask = mask;
_device.rtw_write8(0x808, mask);
}

int RtlJaguarDevice::GetRxPathMask() {
return _radioManagement->phy_query_bb_reg_public(0x808, 0x000000ff);
}

void RtlJaguarDevice::FastRetune(uint8_t channel, bool cache_rf) {
Expand Down
21 changes: 21 additions & 0 deletions src/jaguar1/RtlJaguarDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,23 @@ class RtlJaguarDevice : public IRtlDevice {
bool send_packet(const uint8_t* packet, size_t length) override;
SelectedChannel GetSelectedChannel() override;

/* Runtime RX-chain selection — the adaptive-link spatial-diversity lever
* (the read/write superset of the DEVOURER_RX_PATHS env knob). Writes the
* RX-path-enable mask 0x808[7:0] (bits 0/4 = path A CCK/OFDM, 1/5 = B,
* 2/6 = C, 3/7 = D): 0x11 = A only, 0x33 = A+B, 0x77 = A+B+C, 0xFF = all.
* The 8814 has 4 chains to trade; on 8812/8821 the high bits are no-ops.
* Masking a chain drops its per-chain RSSI to the noise floor — it changes
* WHICH chains combine, not their gain. A controller with a motion/fade
* signal switches this live instead of on the env toggle's fixed timer.
*
* STICKY: once set, the mask is cached and re-applied after every
* SetMonitorChannel (IQK saves/restores 0x808, so a channel set would
* otherwise revert it to the table default) — the same channel-sticky
* contract as the runtime TX-power knobs. GetRxPathMask reads 0x808 back;
* -1 = never set (chip at the table default, all paths). */
void SetRxPathMask(uint8_t mask);
int GetRxPathMask();

bool should_stop = false;

/* Per-queue free-page snapshot read from REG_FIFOPAGE_INFO_1..5
Expand Down Expand Up @@ -219,6 +236,10 @@ class RtlJaguarDevice : public IRtlDevice {
uint32_t interval_ms);
std::thread _rxmask_thread;
std::atomic<bool> _rxmask_stop{false};
/* Cached RX-path mask for SetRxPathMask stickiness: -1 = never set (leave
* the chip's table default across channel sets). Atomic so the toggle
* thread and a control-plane SetMonitorChannel see a consistent value. */
std::atomic<int> _rx_path_mask{-1};

std::thread _therm_thread;
std::atomic<bool> _therm_stop{false};
Expand Down
125 changes: 125 additions & 0 deletions tests/rx_path_mask_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# Hardware validation for the runtime RX-chain mask (SetRxPathMask, issue #189).
# The 8814AU is the only plugged part with >2 chains to trade, so it is the RX;
# an 8812AU beacon on the same channel is the source. DEVOURER_RX_ALLPATHS emits
# per-chain RSSI (A,B,C,D) on <devourer-rxpath> lines — masking a chain drops
# its RSSI to the noise floor, which is the functional truth the check keys on.
#
# Two things proven:
# functional mask 0x33 (A+B) collapses chains C,D vs mask 0xFF (all up) —
# the write reaches the right chains (0x808 byte 0).
# sticky the mask is applied via DEVOURER_RX_PATHS (which now routes
# through SetRxPathMask) and the RX runs under DEVOURER_RX_SWEEP_FULL
# (a full SetMonitorChannel per dwell — IQK saves/restores 0x808).
# C,D staying collapsed across those channel sets proves the
# re-apply; without it the first SetMonitorChannel reverts to
# all-paths and C,D pop back up.
#
# Usage: sudo -v && tests/rx_path_mask_check.sh
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="${RXPATH_CHECK_OUT:-/tmp/devourer-rxpath-check}"
RX_PID=0x8813 RX_VID=0x0bda # RTL8814AU (4T4R) as the receiver
TX_PID=0x8812 TX_VID=0x0bda # RTL8812AU beacon source
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 WiFiDriverDemo 2>/dev/null || true
pkill -x WiFiDriverTxDem 2>/dev/null || true
true
}
trap cleanup EXIT INT TERM

plugged() { lsusb -d "$(printf '%04x:%04x' "$2" "$1")" >/dev/null 2>&1; }
if ! plugged "$RX_PID" "$RX_VID"; then echo "SKIP: 8814AU (RX) not plugged"; exit 0; fi
if ! plugged "$TX_PID" "$TX_VID"; then echo "SKIP: 8812AU (TX) not plugged"; exit 0; fi

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

# Median per-chain RSSI (chains A B C D) over a run's <devourer-rxpath> lines,
# restricted to a channel by cross-referencing the sweep's dwell markers is
# overkill here: the beacon is on ONE channel, so only that channel's dwells
# catch frames — every <devourer-rxpath> line already belongs to it.
medians() { # $1=log -> "A B C D"
python3 - "$1" <<'PYEOF'
import re, statistics, sys
ch = {0: [], 1: [], 2: [], 3: []}
rx = re.compile(r"<devourer-rxpath>.*\brssi=(-?\d+),(-?\d+),(-?\d+),(-?\d+)")
for line in open(sys.argv[1], errors="replace"):
m = rx.search(line)
if m:
for i in range(4):
ch[i].append(int(m.group(i + 1)))
out = []
for i in range(4):
out.append(str(int(statistics.median(ch[i]))) if ch[i] else "nan")
print(" ".join(out), len(ch[0])) # A B C D count
PYEOF
}

# --- beacon source: 8812AU on CH, canonical SA, for the whole session --------
run_rx() { # $1=mask $2=outfile $3=extra_env
sudo -n env DEVOURER_PID="$TX_PID" DEVOURER_VID="$TX_VID" DEVOURER_CHANNEL="$CH" \
DEVOURER_TX_RATE=MCS3 DEVOURER_TX_GAP_US=1500 \
timeout 40 "$ROOT/build/WiFiDriverTxDemo" >/dev/null 2>&1 &
local txpid=$!
sleep 4
sudo -n env DEVOURER_PID="$RX_PID" DEVOURER_VID="$RX_VID" DEVOURER_CHANNEL="$CH" \
DEVOURER_RX_ALLPATHS=1 DEVOURER_RX_PATHS="$1" $3 \
timeout 28 "$ROOT/build/WiFiDriverDemo" >"$2" 2>&1 || true
kill "$txpid" 2>/dev/null; wait "$txpid" 2>/dev/null
sleep 2
}

echo "== run 1: mask 0xFF (all paths), static channel =="
run_rx 0xFF "$OUT/all.log" ""
read -r a0 b0 c0 d0 n0 <<<"$(medians "$OUT/all.log")"
echo " medians A=$a0 B=$b0 C=$c0 D=$d0 ($n0)"

echo "== run 2: mask 0x33 (A+B only), static channel =="
run_rx 0x33 "$OUT/ab.log" ""
read -r a1 b1 c1 d1 n1 <<<"$(medians "$OUT/ab.log")"
echo " medians A=$a1 B=$b1 C=$c1 D=$d1 ($n1)"

echo "== run 3: mask 0x33 under DEVOURER_RX_SWEEP_FULL (SetMonitorChannel/dwell) =="
run_rx 0x33 "$OUT/ab-sweep.log" "DEVOURER_RX_SWEEP=$CH,$((CH+4)) DEVOURER_RX_SWEEP_FULL=1 DEVOURER_RX_SWEEP_DWELL_MS=500"
read -r a2 b2 c2 d2 n2 <<<"$(medians "$OUT/ab-sweep.log")"
echo " medians A=$a2 B=$b2 C=$c2 D=$d2 ($n2)"

# --- assertions --------------------------------------------------------------
lt() { [ "$1" != nan ] && [ "$2" != nan ] && [ "$1" -lt "$2" ] 2>/dev/null; }

# functional: at 0xFF all four chains report a real RSSI (Realtek RSSI is a
# positive dB-ish index, floor is single digits / 0); at 0x33 chains C,D drop
# well below their 0xFF level AND below the enabled A,B.
if [ "$n0" -lt 20 ] || [ "$n1" -lt 20 ]; then
fail "too few frames caught (n_all=$n0 n_ab=$n1) — check the beacon/link"
else
if lt "$c1" "$c0" && lt "$d1" "$d0" && lt "$c1" "$a1" && lt "$d1" "$b1"; then
pass "functional: mask 0x33 collapsed C,D (0xFF C=$c0 D=$d0 -> 0x33 C=$c1 D=$d1; A,B stay up A=$a1 B=$b1)"
else
fail "functional: C,D did not collapse under 0x33 (0xFF C=$c0 D=$d0 / 0x33 C=$c1 D=$d1 A=$a1 B=$b1)"
fi
fi

# sticky: under sweep-full (a full SetMonitorChannel per dwell), C,D must STAY
# collapsed — without the re-apply the channel set reverts 0x808 to all-paths
# and C,D would rise back to ~their 0xFF level.
if [ "$n2" -lt 20 ]; then
fail "sweep run caught too few frames (n=$n2)"
elif lt "$c2" "$a2" && lt "$d2" "$b2" && lt "$c2" "$c0"; then
pass "sticky: mask held across SetMonitorChannel dwells (C=$c2 D=$d2 vs enabled A=$a2 B=$b2, 0xFF C was $c0)"
else
fail "sticky: C,D rose after a full channel set — re-apply not working (sweep C=$c2 D=$d2 A=$a2 B=$b2; 0xFF C=$c0)"
fi

echo
echo "== rx-path-mask check: PASS=$PASS FAIL=$FAIL SKIP=$SKIP =="
[ "$FAIL" -eq 0 ]
Loading