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
47 changes: 47 additions & 0 deletions src/jaguar3/RtlJaguar3Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)");
}
Expand Down Expand Up @@ -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<uint32_t>(0x0520);
uint32_t v524 = _device.rtw_read<uint32_t>(0x0524);
if (disabled) {
v520 |= (1u << 15);
v524 &= ~(1u << 11);
} else {
v520 &= ~(1u << 15);
v524 |= (1u << 11);
}
_device.rtw_write<uint32_t>(0x0520, v520);
_device.rtw_write<uint32_t>(0x0524, v524);
}

void RtlJaguar3Device::SetCcaMode(bool disabled) {
std::lock_guard<std::mutex> 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
Expand All @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions src/jaguar3/RtlJaguar3Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,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:
Expand All @@ -144,6 +152,10 @@ class RtlJaguar3Device : public IRtlDevice {
std::atomic<bool> _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
Expand Down
165 changes: 165 additions & 0 deletions tests/dis_cca_onair.sh
Original file line number Diff line number Diff line change
@@ -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
# <devourer-tx-hit>hits (canonical-SA beacons decoded); we also log median IGI +
# OFDM false-alarm from <devourer-energy>.
#
# 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"<devourer-tx-hit>.*hits=(\d+)", log):
hits = max(hits, int(m.group(1)))
igis, fas = [], []
for m in re.finditer(r"<devourer-energy>.*", 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
Loading