From 64244a4ae473c2f78b5643b71cc319a46d82b40b Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:51:01 +0300 Subject: [PATCH 1/4] txdemo: DEVOURER_USB_BUS/PORT topology selector (mirror of RX demo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WiFiDriverTxDemo could only pick a TX adapter by first-match VID:PID, so two identical RTL8814AU dongles (same VID:PID:serial) couldn't be told apart on the TX side — the RX demo already had this (demo/main.cpp). Needed to TX-select one of several identical adapters, e.g. for the #132 chip-STBC TX-diversity measurement. Same semantics: DEVOURER_USB_BUS + optional DEVOURER_USB_PORT (dotted libusb port path); unset falls back to the VID:PID open loop. Co-Authored-By: Claude Opus 4.8 --- txdemo/main.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/txdemo/main.cpp b/txdemo/main.cpp index 80b5efc..63f4d9b 100644 --- a/txdemo/main.cpp +++ b/txdemo/main.cpp @@ -161,7 +161,44 @@ int main(int argc, char **argv) { target_vid = static_cast(std::strtoul(vid_env, nullptr, 0)); logger->info("DEVOURER_VID={:04x} (overriding default VID)", target_vid); } + /* DEVOURER_USB_BUS (+ optional DEVOURER_USB_PORT) select a device by USB + * topology when several share one VID:PID and even the serial — e.g. two + * identical RTL8814AU dongles. DEVOURER_USB_PORT is the dotted libusb port + * path (sysfs `devpath` / `lsusb -t`). Unset = the VID:PID open loop below. + * Mirrors the RX demo (demo/main.cpp). */ + if (const char *bus_env = std::getenv("DEVOURER_USB_BUS")) { + const auto want_bus = + static_cast(std::strtoul(bus_env, nullptr, 0)); + const char *port_env = std::getenv("DEVOURER_USB_PORT"); + libusb_device **list = nullptr; + ssize_t n = libusb_get_device_list(context, &list); + for (ssize_t i = 0; i < n && handle == NULL; ++i) { + libusb_device_descriptor dd{}; + if (libusb_get_device_descriptor(list[i], &dd) != 0) continue; + if (dd.idVendor != target_vid) continue; + if (target_pid != 0 && dd.idProduct != target_pid) continue; + if (libusb_get_bus_number(list[i]) != want_bus) continue; + if (port_env != nullptr) { + uint8_t ports[8]; + int pc = libusb_get_port_numbers(list[i], ports, sizeof(ports)); + std::string path; + for (int p = 0; p < pc; ++p) + path += (path.empty() ? "" : ".") + std::to_string(ports[p]); + if (path != port_env) continue; + } + if (libusb_open(list[i], &handle) == 0) + logger->info("Opened device {:04x}:{:04x} on bus {} port {}", + dd.idVendor, dd.idProduct, want_bus, + port_env ? port_env : "(any)"); + } + if (list != nullptr) libusb_free_device_list(list, 1); + if (handle == NULL) + logger->error("DEVOURER_USB_BUS={} PORT={} matched no device", want_bus, + port_env ? port_env : "(any)"); + } + for (uint16_t pid : kRealtekProductIds) { + if (handle != NULL) break; if (target_pid != 0 && pid != target_pid) continue; handle = libusb_open_device_with_vid_pid(context, target_vid, pid); if (handle != NULL) { From 6d7893bc2e5d69761b0f3b80bc3265e1133b6b2f Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:52:13 +0300 Subject: [PATCH 2/4] Add STBC on-air sanity-gate runner (#132) tests/stbc_sanity.sh: TX canonical beacons at MCS1/STBC from an 8814 (chip encodes Alamouti internally), RX on an 8812 (whose descriptor exposes the received STBC bit, unlike the 8814 RX), and report whether frames arrive with stbc=1. PASS = chip emits decodable STBC; FAIL(stbc=0) = descriptor STBC ignored (would need CSD); INCONCLUSIVE = link too weak. The gate that decides whether the #132 chip-STBC TX-diversity measurement can proceed. Co-Authored-By: Claude Opus 4.8 --- tests/stbc_sanity.sh | 70 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100755 tests/stbc_sanity.sh diff --git a/tests/stbc_sanity.sh b/tests/stbc_sanity.sh new file mode 100755 index 0000000..e61eb4e --- /dev/null +++ b/tests/stbc_sanity.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# STBC on-air sanity gate: does the chip actually EMIT decodable STBC? +# +# Sends canonical-SA beacons at MCS1/STBC from an RTL8814AU (which encodes +# Alamouti across two TX antennas internally, sidestepping the SDR 2-channel +# problem), and receives on an RTL8812AU — the 8812/8821 RX descriptor exposes +# the received HT-SIG STBC bit, which the 8814 RX does NOT (it reports defaults). +# A pass = frames arrive with stbc=1 in the line. +# +# Hardware (all on ONE host, adapters inches apart for a STRONG link — a clean +# yes/no needs the frame to decode easily; range confounds a zero result): +# TX = RTL8814AU (0bda:8813), selected by USB_BUS +# RX = RTL8812AU (0bda:8812) ← must be an 8812/8821 to read the STBC bit +# +# Env: +# TX_BUS=N [TX_PORT=a.b.c] which 8814 transmits (topology select) +# CHANNEL=6 test channel +# MCS=MCS1 HT rate to carry STBC (STBC needs HT/VHT, not legacy) +# DURATION=20 RX capture seconds +# +# Usage: sudo TX_BUS=4 ./tests/stbc_sanity.sh +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$HERE/.." && pwd)" +CHANNEL="${CHANNEL:-6}"; MCS="${MCS:-MCS1}"; DURATION="${DURATION:-20}" +RXLOG="$(mktemp -t devourer-stbc-rx.XXXXXX.log)" +TXLOG="$(mktemp -t devourer-stbc-tx.XXXXXX.log)" + +cleanup() { + for comm in WiFiDriverDemo WiFiDriverTxDem; do pkill -x "$comm" 2>/dev/null || true; done + rm -f "$RXLOG" "$TXLOG" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +echo "== building =="; cmake --build "$ROOT/build" -j >/dev/null +DEMO="$ROOT/build/WiFiDriverDemo"; TXDEMO="$ROOT/build/WiFiDriverTxDemo" + +# Preflight: both chips present. +lsusb | grep -qi "0bda:8812" || { echo "no RTL8812AU (RX) present — plug it into this host" >&2; exit 1; } +lsusb | grep -qi "0bda:8813" || { echo "no RTL8814AU (TX) present" >&2; exit 1; } + +echo "== TX: 8814 (bus ${TX_BUS:-first}) beacon at ${MCS}/STBC, ch$CHANNEL ==" +TX_ENV=(DEVOURER_PID=0x8813 DEVOURER_CHANNEL="$CHANNEL" DEVOURER_TX_RATE="${MCS}/STBC") +[ -n "${TX_BUS:-}" ] && TX_ENV+=(DEVOURER_USB_BUS="$TX_BUS") +[ -n "${TX_PORT:-}" ] && TX_ENV+=(DEVOURER_USB_PORT="$TX_PORT") +stdbuf -oL -eL env "${TX_ENV[@]}" "$TXDEMO" >"$TXLOG" 2>&1 & +for _ in $(seq 1 30); do grep -q 'TX #.* rc=1' "$TXLOG" 2>/dev/null && break; sleep 1; done +grep -q 'TX #.* rc=1' "$TXLOG" || { echo "TX beacon never injected — check $TXLOG" >&2; exit 3; } +echo " TX injecting ($(grep -m1 'fixed rate' "$TXLOG" 2>/dev/null || echo '?'))" + +echo "== RX: 8812 for ${DURATION}s, reading the STBC bit ==" +timeout "$((DURATION+2))" env DEVOURER_PID=0x8812 DEVOURER_STREAM_OUT=1 \ + DEVOURER_CHANNEL="$CHANNEL" "$DEMO" >"$RXLOG" 2>/dev/null & +sleep "$DURATION"; pkill -x WiFiDriverDemo 2>/dev/null || true; wait 2>/dev/null || true + +TOTAL=$(grep -c '' "$RXLOG" || true) +STBC1=$(grep -c '.*stbc=1' "$RXLOG" || true) +echo "== result: canonical frames=$TOTAL with stbc=1: $STBC1 ==" +grep -m3 '' "$RXLOG" | sed -E 's/body=.*//' +if [ "${STBC1:-0}" -gt 0 ]; then + echo "PASS — the chip emits decodable STBC on-air (stbc=1 seen). Proceed to the" + echo " TX-diversity mobility measurement (STBC vs single-stream, moving TX)." +elif [ "${TOTAL:-0}" -gt 0 ]; then + echo "FAIL — frames decode but stbc=0: the 8814 is NOT emitting STBC for a" + echo " descriptor STBC bit. TX diversity would need CSD instead (#128)." +else + echo "INCONCLUSIVE — no canonical frames. Move TX/RX closer (MCS1 needs a strong" + echo " link) or confirm both adapters are on ch$CHANNEL." +fi From a01b7b01cffcc0bbc9942552393b6386c9fa36b2 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:58:41 +0300 Subject: [PATCH 3/4] stbc_sanity: wait only on the RX (the persistent TX beacon hung `wait`) Bare `wait` blocked on the still-running TX beacon, so the script never printed its verdict. Capture the RX pid and wait on it alone. --- tests/stbc_sanity.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/stbc_sanity.sh b/tests/stbc_sanity.sh index e61eb4e..246f9cc 100755 --- a/tests/stbc_sanity.sh +++ b/tests/stbc_sanity.sh @@ -52,7 +52,10 @@ echo " TX injecting ($(grep -m1 'fixed rate' "$TXLOG" 2>/dev/null || echo '?') echo "== RX: 8812 for ${DURATION}s, reading the STBC bit ==" timeout "$((DURATION+2))" env DEVOURER_PID=0x8812 DEVOURER_STREAM_OUT=1 \ DEVOURER_CHANNEL="$CHANNEL" "$DEMO" >"$RXLOG" 2>/dev/null & -sleep "$DURATION"; pkill -x WiFiDriverDemo 2>/dev/null || true; wait 2>/dev/null || true +rxpid=$! +sleep "$DURATION" +kill "$rxpid" 2>/dev/null || true +wait "$rxpid" 2>/dev/null || true # only the RX — the TX beacon runs on TOTAL=$(grep -c '' "$RXLOG" || true) STBC1=$(grep -c '.*stbc=1' "$RXLOG" || true) From 723e402faf936e176a4a39a076d505825a7b2213 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:19:46 +0300 Subject: [PATCH 4/4] txdemo: don't kill the TX event loop on transient libusb errors; add STBC toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes from the #132 chip-STBC TX-diversity work: 1. FIX: usb_event_loop() broke out of the loop on any non-TIMEOUT return from libusb_handle_events_timeout_completed, including the transient LIBUSB_ERROR_INTERRUPTED (EINTR — a signal hitting the underlying poll, more likely under concurrent USB load). Exiting that thread permanently stops servicing async TX-completion callbacks, so submitted URBs are never freed and within a few frames every libusb_submit_transfer fails — a "TX works briefly then every send fails" wedge, seen intermittently while an RX ran concurrently. Now INTERRUPTED is ignored like TIMEOUT and other errors are logged (rate- limited) without breaking. This is a provable-by-inspection bug matching the observed symptom; it could not be verified against the intermittent failure because that no longer reproduces. 2. FEATURE: DEVOURER_TX_STBC_TOGGLE=1 alternates the STBC bit every frame (rate from DEVOURER_TX_RATE, must be HT/VHT). The RX reports the received STBC bit per frame, so an alternating TX lets one moving capture compare STBC vs single-stream delivery on the same fading — the transmit-side mobility measurement, tagged for free by the receiver. Verified: RX sees ~50/50 stbc=0/stbc=1, alternating per sequence number. Co-Authored-By: Claude Opus 4.8 --- txdemo/main.cpp | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/txdemo/main.cpp b/txdemo/main.cpp index 63f4d9b..7758b28 100644 --- a/txdemo/main.cpp +++ b/txdemo/main.cpp @@ -111,9 +111,18 @@ void usb_event_loop(Logger_t _logger, libusb_context *ctx) { while (!g_devourer_should_stop) { struct timeval tv {0, 100000}; int r = libusb_handle_events_timeout_completed(ctx, &tv, nullptr); - if (r < 0 && r != LIBUSB_ERROR_TIMEOUT) { - _logger->error("Error handling events: {}", r); - break; + /* TIMEOUT is normal poll-expiry; INTERRUPTED (EINTR) is a transient wakeup + * (a signal hit the underlying poll, more likely under concurrent USB load). + * Neither is fatal. Do NOT break on them: this thread services async TX + * completions, so if it exits, the completion callbacks stop firing, + * submitted URBs are never freed, and within a few frames every + * libusb_submit_transfer fails — the "TX works briefly then every send + * fails" wedge (seen intermittently while an RX ran concurrently). Keep + * polling; only log a genuinely unexpected error and carry on. */ + if (r < 0 && r != LIBUSB_ERROR_TIMEOUT && r != LIBUSB_ERROR_INTERRUPTED) { + static int logged = 0; + if (logged++ < 5) + _logger->error("libusb_handle_events: {} (continuing)", r); } } } @@ -470,7 +479,17 @@ int main(int argc, char **argv) { * it applies to Jaguar3 (8822CU/EU) too. The demo's beacon is rate-less, so * without this its Jaguar3 TX fell back to MGN_1M (1 Mbps) regardless of * DEVOURER_TX_RATE. Per-packet radiotap still overrides. */ - rtlDevice->SetTxMode(devourer::parse_tx_mode_env()); + const devourer::TxMode tx_mode_base = devourer::parse_tx_mode_env(); + rtlDevice->SetTxMode(tx_mode_base); + + /* DEVOURER_TX_STBC_TOGGLE=1 alternates the STBC bit every frame (keeping the + * rate from DEVOURER_TX_RATE, which must be HT/VHT for STBC to apply). The RX + * reports the received STBC bit per frame, so an alternating TX lets one + * moving capture compare STBC vs single-stream delivery on the same fading — + * the transmit-side mobility measurement, tagged for free by the receiver. */ + const bool stbc_toggle = std::getenv("DEVOURER_TX_STBC_TOGGLE") != nullptr; + if (stbc_toggle) + logger->info("DEVOURER_TX_STBC_TOGGLE: alternating STBC on/off per frame"); std::vector tx_buf(beacon_frame, beacon_frame + sizeof(beacon_frame)); @@ -710,6 +729,11 @@ int main(int argc, char **argv) { tx_count, switch_us, ms_since_start(), mode); fflush(stdout); } + if (stbc_toggle) { + devourer::TxMode m = tx_mode_base; + m.stbc = static_cast(tx_count & 1); /* 0,1,0,1,… per frame */ + rtlDevice->SetTxMode(m); + } rc = rtlDevice->send_packet(tx_buf.data(), tx_buf.size()); ++tx_count; if (!hop_channels.empty() && ++frames_in_dwell >= hop_dwell)