From 600de64b725e70862afd049a996126ba8a062d06 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:09:45 +0300 Subject: [PATCH] =?UTF-8?q?dis=5Fcca=20/=20EDCCA-disable:=20measured=20NUL?= =?UTF-8?q?L=20for=20monitor=20RX=20=E2=80=94=20safe=20knob=20+=20harness,?= =?UTF-8?q?=20not=20promoted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adaptive-link recipes flag dis_cca as a community "doubles range / removes stuttering" tweak on the rtl88x2eu. Measure-first before shipping it as API. Finding (8822EU, swept B210 AWGN, DEVOURER_DIS_CCA A/B): - Full vendor recipe DEAFENS the RX: the BB 0x1d58[0xff8]=0x1ff write is "OFDM CCA off" (the CW-tone path uses it to stop OFDM detection), so it drops 8822EU delivery from ~6800 to ~10 frames. - MAC EDCCA-disable alone (0x520[15]/0x524[11]) is an exact NULL: delivery within 1% at every noise gain (33700 vs 33400 hits over 46..76 dB; IGI pinned, FA identical). Mechanism: EDCCA gates TX *deferral*, not RX decode — and devourer injects in monitor mode, which already bypasses the CSMA/EDCCA backoff the community's range benefit comes from. So there is nothing a runtime RX-side knob adds here. Disposition (same discipline as the #190 DIG and the J3 per-packet-power probe): - SetCcaMode is a Jaguar3 research knob only (DEVOURER_DIS_CCA=1) — NOT promoted to IRtlDevice. It writes ONLY the safe MAC EDCCA bit; the RX-deafening BB writes are deliberately omitted (documented in the code). - tests/dis_cca_onair.sh kept as the reusable swept-AWGN A/B rig for anyone re-measuring on a real (non-monitor) link — this bench can't bury the wanted beacon under B210 AWGN (near-field front-end limit), so the null is "no effect in the testable regime" + the mechanism above. Co-Authored-By: Claude Opus 4.8 --- src/jaguar3/RtlJaguar3Device.cpp | 47 +++++++++ src/jaguar3/RtlJaguar3Device.h | 12 +++ tests/dis_cca_onair.sh | 165 +++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100755 tests/dis_cca_onair.sh diff --git a/src/jaguar3/RtlJaguar3Device.cpp b/src/jaguar3/RtlJaguar3Device.cpp index 6dc2917..31e1444 100644 --- a/src/jaguar3/RtlJaguar3Device.cpp +++ b/src/jaguar3/RtlJaguar3Device.cpp @@ -54,6 +54,10 @@ void RtlJaguar3Device::Init(Action_ParsedRadioPacket packetProcessor, _hal.coex_wlan_only_init(); /* lock antenna to WLAN (disable BT/LTE coex) */ _brought_up = true; + /* DEVOURER_DIS_CCA=1 — MAC EDCCA-disable research knob (see SetCcaMode). */ + if (std::getenv("DEVOURER_DIS_CCA")) + SetCcaMode(true); + /* DEVOURER_BF_ARM_BFEE=aa:bb:cc:dd:ee:ff — beamforming self-sounding probe * (beamformee side), Jaguar-3 variant. Arms the hardware CSI responder to * reply to NDPA+NDP from the given beamformer MAC with a VHT Compressed @@ -402,6 +406,10 @@ void RtlJaguar3Device::InitWrite(SelectedChannel channel) { /* Start the coex runtime thread: it drains C2H and re-applies the 5 GHz coex * decision every ~2 s so sustained TX isn't silenced by the FW's PTA. (The CW * tone path returned earlier and never reaches here.) */ + /* DEVOURER_DIS_CCA=1 — measure-first EDCCA-disable knob (see SetCcaMode). + * Applied before the coex thread starts so the writes don't contend. */ + if (std::getenv("DEVOURER_DIS_CCA")) + SetCcaMode(true); _coex_thread = std::thread([this] { coex_runtime_loop(); }); _logger->info("Jaguar3: ready for TX (monitor inject)"); } @@ -625,6 +633,41 @@ RxEnergy RtlJaguar3Device::GetRxEnergy() { return e; } +/* Disable / restore the MAC EDCCA energy-detect gate (the vendor dis_cca proc's + * MAC half: BIT_DIS_EDCCA 0x520[15] + BIT_EDCCA_MSK_COUNTDOWN 0x524[11]). Caller + * holds _reg_mu. + * + * The vendor recipe ALSO writes three BB registers (0x1a9c[20], 0x1a14[9:8], + * 0x1d58[0xff8]); those are deliberately NOT done here. 0x1d58[0xff8]=0x1ff is + * the OFDM-CCA-off write (the CW-tone path uses it to stop OFDM detection for a + * bare carrier), so applying it makes the RX deaf to OFDM — MEASURED: the full + * recipe dropped 8822EU delivery from ~6800 to ~10 frames. The MAC EDCCA bit is + * the only part that's safe to touch on a live RX. See docs / the help-wanted + * issue for the measured null and why (EDCCA gates TX deferral, which devourer's + * monitor inject already bypasses). */ +void RtlJaguar3Device::apply_cca_mode_locked(bool disabled) { + uint32_t v520 = _device.rtw_read(0x0520); + uint32_t v524 = _device.rtw_read(0x0524); + if (disabled) { + v520 |= (1u << 15); + v524 &= ~(1u << 11); + } else { + v520 &= ~(1u << 15); + v524 |= (1u << 11); + } + _device.rtw_write(0x0520, v520); + _device.rtw_write(0x0524, v524); +} + +void RtlJaguar3Device::SetCcaMode(bool disabled) { + std::lock_guard lk(_reg_mu); + _cca_disabled = disabled; + if (_brought_up) + apply_cca_mode_locked(disabled); + _logger->info("Jaguar3: MAC EDCCA {}", disabled ? "DISABLED (dis_cca)" + : "enabled (default)"); +} + void RtlJaguar3Device::SetMonitorChannel(SelectedChannel channel) { /* Serialize against the coex thread's housekeeping tick (and any concurrent * FastRetune) — channel config is register RMW. Init/InitWrite call the @@ -642,6 +685,10 @@ void RtlJaguar3Device::SetMonitorChannel(SelectedChannel channel) { _pwr_ref_valid = false; if (_brought_up && (_tx_pwr_offset_steps != 0 || _tx_pwr_override >= 0)) apply_tx_power_current(/*full=*/true); + /* dis_cca is sticky — the channel set rewrote the BB CCA registers, so + * re-assert the disable if it was armed. */ + if (_brought_up && _cca_disabled) + apply_cca_mode_locked(true); } void RtlJaguar3Device::FastRetune(uint8_t channel, bool cache_rf) { diff --git a/src/jaguar3/RtlJaguar3Device.h b/src/jaguar3/RtlJaguar3Device.h index 228edcf..a87038b 100644 --- a/src/jaguar3/RtlJaguar3Device.h +++ b/src/jaguar3/RtlJaguar3Device.h @@ -121,6 +121,14 @@ class RtlJaguar3Device : public IRtlDevice { * _reg_mu against the coex runtime thread. The read side of the CW tone. */ RxEnergy GetRxEnergy() override; + /* dis_cca / EDCCA-disable investigation knob (DEVOURER_DIS_CCA). Writes the + * vendor rtw_proc.c dis_cca recipe (MAC BIT_DIS_EDCCA 0x520[15] + EDCCA-mask + * countdown 0x524[11], BB 0x1a9c[20]/0x1a14[9:8]/0x1d58[0xff8]); disabled=false + * restores the inverse. Sticky across SetMonitorChannel; serialized on _reg_mu + * against the coex tick. Measure-first — kept for the swept-AWGN A/B; not on + * IRtlDevice until the measurement justifies it. */ + void SetCcaMode(bool disabled); + bool should_stop = false; private: @@ -142,6 +150,10 @@ class RtlJaguar3Device : public IRtlDevice { std::atomic _txpwr_sat_high{false}; /* Bring-up completion: gates the live apply (+ _reg_mu use) in the setters. */ bool _brought_up = false; + /* dis_cca sticky state — re-applied after SetMonitorChannel (the channel set + * rewrites the BB CCA registers). Caller holds _reg_mu. */ + bool _cca_disabled = false; + void apply_cca_mode_locked(bool disabled); /* TX+RX intent (DEVOURER_TX_WITH_RX at InitWrite / an RX-side Init): * consumed as skip_path_b_ofdm_ref by EVERY TXAGC ref write, so no offset * churn can ever touch 0x41e8 while RX is alive (the 8822E RX-desense diff --git a/tests/dis_cca_onair.sh b/tests/dis_cca_onair.sh new file mode 100755 index 0000000..bae87d2 --- /dev/null +++ b/tests/dis_cca_onair.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# MEASURE-FIRST gate for the dis_cca / EDCCA-disable knob (SetCcaMode, +# DEVOURER_DIS_CCA). The OpenIPC-FPV community reports dis_cca "doubles range / +# removes stuttering" on the rtl88x2eu. devourer injects in monitor mode, which +# already bypasses the MAC CSMA/CCA backoff on the *TX* side — so any real +# benefit must be RX-side: EDCCA muting the receiver (or forcing it to hold off) +# when in-band energy crosses the energy-detect threshold. dis_cca removes that +# gate, so the RX should keep demodulating the wanted frames through interference +# that would otherwise pin CCA busy. +# +# The test: ONE 8822EU DUT, ONE swept B210 AWGN interferer, ONE marginal beacon. +# A/B is DEVOURER_DIS_CCA off vs on at each noise gain. delivery = the final +# hits (canonical-SA beacons decoded); we also log median IGI + +# OFDM false-alarm from . +# +# MEASURED RESULT (8822EU, 5-point AWGN sweep 46..76 dB): NULL. The MAC +# EDCCA-disable (DEVOURER_DIS_CCA=1) leaves delivery within 1% at every noise +# gain (33700 vs 33400 hits, ratio 0.99; IGI pinned, FA identical). The full +# vendor recipe (incl. the BB 0x1d58 OFDM-CCA-off write) instead DEAFENS the RX +# (~6800 -> ~10 hits), so it is not implemented. Mechanism: EDCCA gates TX +# deferral, not RX decode, and devourer injects in monitor mode — which already +# bypasses the CSMA/EDCCA backoff the community's "range" benefit comes from. +# So SetCcaMode is NOT promoted to IRtlDevice; the J3 knob + this harness are +# kept for anyone re-measuring on a real (non-monitor) link. This bench can't +# bury the wanted beacon under B210 AWGN (near-field front-end limit), so the +# null is "no effect in the testable regime" + the mechanism above. +# +# Usage: sudo -v && tests/dis_cca_onair.sh +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="${DISCCA_OUT:-/tmp/devourer-dis-cca}" +CH="${CH:-36}" +BEACON_PWR="${BEACON_PWR:-10}" # 8812AU TXAGC index — marginal link +GAINS="${GAINS:-40 46 52 58 64 70 76}" # B210 AWGN gain ladder (dB) +DUR="${DUR:-14}" # RX cell seconds per (arm, gain) +PY="$ROOT/tests/.venv/bin/python" +{ [ -x "$PY" ] && "$PY" -c 'import uhd' 2>/dev/null; } || PY=python3 +mkdir -p "$OUT" + +TX_PID=0x8812 TX_VID=0x0bda # 8812AU beacon source +DUT_PID=0xa81a DUT_VID=0x0bda # 8822EU — Jaguar3, the community chip + +cleanup() { + sudo -n pkill -x WiFiDriverDemo 2>/dev/null || true + sudo -n pkill -x WiFiDriverTxDem 2>/dev/null || true + sudo -n pkill -f sdr_interferer 2>/dev/null || true + true +} +trap cleanup EXIT INT TERM + +plugged() { lsusb -d "$(printf '%04x:%04x' "$2" "$1")" >/dev/null 2>&1; } +for d in "$TX_PID $TX_VID beacon-8812AU" "$DUT_PID $DUT_VID DUT-8822EU"; do + read -r p v name <<<"$d" + plugged "$p" "$v" || { echo "SKIP: $name ($p:$v) not plugged"; exit 0; } +done + +echo "== building ==" +cmake --build "$ROOT/build" -j --target WiFiDriverDemo WiFiDriverTxDemo >/dev/null || exit 1 + +# One RX cell against a running interferer + beacon. $5 = "1" arms dis_cca. +# Echoes "hits igi_med fa_med". +run_cell() { # $1=gain $2=tag $3=discca(0|1) + local gain="$1" tag="$2" discca="$3" + sudo -n "$PY" "$ROOT/tests/sdr_interferer.py" --channel "$CH" \ + --tx-gain "$gain" --rate 20e6 --mode noise --secs $((DUR + 12)) \ + >"$OUT/$tag-noise.log" 2>&1 & + local jam=$! + sleep 5 + sudo -n env DEVOURER_PID="$TX_PID" DEVOURER_VID="$TX_VID" \ + DEVOURER_CHANNEL="$CH" DEVOURER_TX_RATE=MCS3 DEVOURER_TX_PWR="$BEACON_PWR" \ + DEVOURER_TX_GAP_US=1500 \ + timeout $((DUR + 8)) "$ROOT/build/WiFiDriverTxDemo" >/dev/null 2>&1 & + local tx=$! + sleep 3 + local discca_env="" + [ "$discca" = "1" ] && discca_env="DEVOURER_DIS_CCA=1" + sudo -n env DEVOURER_PID="$DUT_PID" DEVOURER_VID="$DUT_VID" \ + DEVOURER_CHANNEL="$CH" DEVOURER_RX_ENERGY_MS=500 $discca_env \ + timeout "$DUR" "$ROOT/build/WiFiDriverDemo" >"$OUT/$tag.log" 2>&1 || true + kill "$tx" 2>/dev/null; wait "$tx" 2>/dev/null + kill "$jam" 2>/dev/null; wait "$jam" 2>/dev/null + sudo -n pkill -f sdr_interferer 2>/dev/null + sleep 3 + python3 - "$OUT/$tag.log" <<'PYEOF' +import re, statistics, sys +log = open(sys.argv[1], errors="replace").read() +hits = 0 +for m in re.finditer(r".*hits=(\d+)", log): + hits = max(hits, int(m.group(1))) +igis, fas = [], [] +for m in re.finditer(r".*", log): + s = m.group(0) + gi = re.search(r"\bigi=(-?\d+)", s) + fo = re.search(r"\bfa_ofdm=(-?\d+|-)", s) + if gi and gi.group(1) != "-": + igis.append(int(gi.group(1))) + if fo and fo.group(1) not in ("-",): + fas.append(int(fo.group(1))) +im = int(statistics.median(igis)) if igis else -1 +fm = int(statistics.median(fas)) if fas else -1 +print(f"{hits} {im} {fm}") +PYEOF +} + +# Confirm the knob actually lands its registers once (no interferer) before the sweep. +echo "== register-landing check (no interferer) ==" +sudo -n env DEVOURER_PID="$DUT_PID" DEVOURER_VID="$DUT_VID" DEVOURER_CHANNEL="$CH" \ + DEVOURER_DIS_CCA=1 timeout 6 "$ROOT/build/WiFiDriverDemo" 2>&1 \ + | grep -m1 "CCA/EDCCA" || echo " WARN: no CCA/EDCCA log line seen" + +printf "%-8s | %-22s | %-22s\n" "gain_dB" "dis_cca=0 (hits/igi/fa)" "dis_cca=1 (hits/igi/fa)" +printf -- "---------+------------------------+------------------------\n" +: >"$OUT/summary.tsv" +for gain in $GAINS; do + read -r a_hits a_igi a_fa <<<"$(run_cell "$gain" "off-g$gain" 0)" + read -r b_hits b_igi b_fa <<<"$(run_cell "$gain" "on-g$gain" 1)" + printf "%-8s | %-22s | %-22s\n" "$gain" \ + "$a_hits/$a_igi/$a_fa" "$b_hits/$b_igi/$b_fa" + printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" \ + "$gain" "$a_hits" "$a_igi" "$a_fa" "$b_hits" "$b_igi" "$b_fa" >>"$OUT/summary.tsv" +done + +echo +python3 - "$OUT/summary.tsv" <<'PYEOF' +import sys +rows = [] +for line in open(sys.argv[1]): + g, ah, ai, afa, bh, bi, bfa = line.split() + rows.append((float(g), int(ah), int(bh))) +if len(rows) < 3: + print("insufficient points"); sys.exit(0) +a_max = max(r[1] for r in rows) or 1 +b_max = max(r[2] for r in rows) or 1 +def crossing(pts, mx): + half = mx / 2.0 + for (g0, h0), (g1, h1) in zip(pts, pts[1:]): + if h0 >= half >= h1 and h0 != h1: + return g0 + (g1 - g0) * (h0 - half) / (h0 - h1) + return None +a_pts = [(r[0], r[1]) for r in rows] +b_pts = [(r[0], r[2]) for r in rows] +ac, bc = crossing(a_pts, a_max), crossing(b_pts, b_max) +tot_a = sum(r[1] for r in rows); tot_b = sum(r[2] for r in rows) +print(f"dis_cca=0 50%-delivery gain: {ac if ac else 'n/a'}") +print(f"dis_cca=1 50%-delivery gain: {bc if bc else 'n/a'}") +print(f"total hits: dis_cca=0 {tot_a} dis_cca=1 {tot_b} " + f"(ratio {tot_b/max(tot_a,1):.2f})") +if ac and bc: + d = bc - ac + print(f"crossing shift (dis_cca ON - OFF): {d:+.1f} dB") + if d >= 3: + print("VERDICT: dis_cca HELPS (>=3 dB) — SHIP the knob") + elif d <= 1 and tot_b <= tot_a * 1.15: + print("VERDICT: NULL (<=1 dB, no delivery gain) — DO NOT promote; document") + else: + print("VERDICT: marginal — inspect the curve before deciding") +else: + r = tot_b / max(tot_a, 1) + if r >= 1.3: + print("VERDICT: dis_cca HELPS (>=30% more delivery) — SHIP the knob") + elif r <= 1.15: + print("VERDICT: NULL — DO NOT promote; document") + else: + print("VERDICT: marginal — inspect the curve before deciding") +PYEOF