diff --git a/tests/stbc_sanity.sh b/tests/stbc_sanity.sh new file mode 100755 index 0000000..246f9cc --- /dev/null +++ b/tests/stbc_sanity.sh @@ -0,0 +1,73 @@ +#!/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 & +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) +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 diff --git a/txdemo/main.cpp b/txdemo/main.cpp index 80b5efc..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); } } } @@ -161,7 +170,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) { @@ -433,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)); @@ -673,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)