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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ add_library(WiFiDriver
src/TxMode.h
src/TxPower.cpp
src/TxPower.h
src/TxStats.h
src/ThermalStatus.h
src/TxCaps.h
src/LinkHealth.cpp
Expand Down
8 changes: 8 additions & 0 deletions src/IRtlDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "TxCaps.h"
#include "TxMode.h"
#include "TxPower.h"
#include "TxStats.h"

/* Packet is the parsed-RX type handed to the RX callback. Forward-declared here
* (a reference in the std::function signature needs only an incomplete type) so
Expand Down Expand Up @@ -148,6 +149,13 @@ class IRtlDevice {
virtual void SetTxMode(const devourer::TxMode & /*mode*/) {}
virtual void ClearTxMode() {}

/* TX submission health snapshot (see TxStats.h) — the driver-side drop /
* congestion signal an adaptive-link controller uses to detect a full TX FIFO
* (a bulk-OUT TIMEOUT = recoverable back-pressure) vs a hard error. Counted at
* the shared USB bulk-OUT layer, so every generation reports it. Default is an
* all-zero snapshot. */
virtual devourer::TxStats GetTxStats() { return {}; }

/* Frame-free RX energy / channel-busy snapshot (see RxSense.h) — the read side
* of the DEVOURER_CW_TONE emitter, used for spectrum-sensing / interferer
* detection. Reads the chip's phydm false-alarm + CCA counters, DIG/IGI, and
Expand Down
20 changes: 20 additions & 0 deletions src/RtlUsbAdapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,14 @@ void RtlUsbAdapter::transfer_callback(struct libusb_transfer *transfer) {
/* Flag the bulk-OUT as possibly halted so the next send_packet (on the TX
* thread) re-clear_halts it before the following frame. */
self->_tx_wedged->store(true, std::memory_order_relaxed);
/* Async completion failure — the real drop for the async TX path. A
* TIMED_OUT status is the FIFO-full back-pressure (congestion); anything
* else is a hard error. Record the negated status as the rc. */
self->_tx_stats->failed.fetch_add(1, std::memory_order_relaxed);
self->_tx_stats->last_rc.store(-transfer->status, std::memory_order_relaxed);
self->_tx_stats->last_timeout.store(
transfer->status == LIBUSB_TRANSFER_TIMED_OUT,
std::memory_order_relaxed);
self->_logger->error("Failed to send packet, status: {}, actual length: {}",
transfer->status, transfer->actual_length);
}
Expand Down Expand Up @@ -530,6 +538,9 @@ bool RtlUsbAdapter::send_packet(uint8_t *packet, size_t length) {
* every completion with status=-2 (ENOENT/cancelled), data_len=0. */
transfer->flags |= LIBUSB_TRANSFER_ADD_ZERO_PACKET;
auto start = std::chrono::high_resolution_clock::now();
/* Count the submission here; async completion (incl. TIMED_OUT) is counted in
* transfer_callback, a submit error just below. */
_tx_stats->submitted.fetch_add(1, std::memory_order_relaxed);
int rc = rc = libusb_submit_transfer(transfer);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> elapsed = end - start;
Expand All @@ -538,6 +549,10 @@ bool RtlUsbAdapter::send_packet(uint8_t *packet, size_t length) {
elapsed.count());
return true;
} else {
_tx_stats->failed.fetch_add(1, std::memory_order_relaxed);
_tx_stats->last_rc.store(rc, std::memory_order_relaxed);
_tx_stats->last_timeout.store(rc == LIBUSB_ERROR_TIMEOUT,
std::memory_order_relaxed);
_logger->error("Failed to send packet, error code: {}", rc);
libusb_free_transfer(transfer);
return false;
Expand All @@ -556,9 +571,14 @@ int RtlUsbAdapter::bulk_send_sync_ep(uint8_t ep, uint8_t *packet, size_t length,
* normal TX-queue operation, not the per-send hot path. Resetting the
* data toggle bit corrupts the chip's state machine. */
int actual = 0;
_tx_stats->submitted.fetch_add(1, std::memory_order_relaxed);
int rc = libusb_bulk_transfer(_dev_handle, ep, packet,
static_cast<int>(length), &actual, timeout_ms);
if (rc != LIBUSB_SUCCESS) {
_tx_stats->failed.fetch_add(1, std::memory_order_relaxed);
_tx_stats->last_rc.store(rc, std::memory_order_relaxed);
_tx_stats->last_timeout.store(rc == LIBUSB_ERROR_TIMEOUT,
std::memory_order_relaxed);
_logger->error("bulk_send EP {} FAIL rc={} got {}/{}", (int)ep, rc,
actual, (int)length);
return rc;
Expand Down
26 changes: 26 additions & 0 deletions src/RtlUsbAdapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "drv_types.h"
#include "hal_com_reg.h"
#include "logger.h"
#include "TxStats.h"

namespace devourer {
class UsbDeviceLock;
Expand Down Expand Up @@ -72,6 +73,19 @@ class RtlUsbAdapter {
std::shared_ptr<std::atomic<bool>> _tx_wedged =
std::make_shared<std::atomic<bool>>(false);

/* TX submission counters (the driver-drop / congestion signal, see
* TxStats.h). Heap-shared for the same reason as _tx_wedged: RtlUsbAdapter is
* a copyable value type, and the async transfer_callback runs on whichever
* copy submitted — all copies must increment the one set of counters. */
struct TxStatsCounters {
std::atomic<uint64_t> submitted{0};
std::atomic<uint64_t> failed{0};
std::atomic<int> last_rc{0};
std::atomic<bool> last_timeout{false};
};
std::shared_ptr<TxStatsCounters> _tx_stats =
std::make_shared<TxStatsCounters>();

/* Exclusive per-adapter USB lock, acquired by WiFiDriver::CreateRtlDevice and
* held for the device's lifetime (see UsbDeviceLock.h). shared_ptr because
* RtlUsbAdapter is a copyable value type copied into every sub-manager
Expand Down Expand Up @@ -125,6 +139,18 @@ class RtlUsbAdapter {
void bulk_clear_halt(uint8_t ep) { libusb_clear_halt(_dev_handle, ep); }
int bulk_send_sync_ep(uint8_t ep, uint8_t *packet, size_t length,
int timeout_ms);

/* Snapshot of the TX submission counters (see TxStats.h). Safe from any
* thread — the fields are read from atomics (a torn read across the four is
* harmless for a monitoring counter). */
devourer::TxStats GetTxStats() const {
devourer::TxStats s;
s.submitted = _tx_stats->submitted.load(std::memory_order_relaxed);
s.failed = _tx_stats->failed.load(std::memory_order_relaxed);
s.last_error_rc = _tx_stats->last_rc.load(std::memory_order_relaxed);
s.last_was_timeout = _tx_stats->last_timeout.load(std::memory_order_relaxed);
return s;
}
/* Raw bulk-IN read on the discovered bulk-IN endpoint, returning bytes read
* (or a negative libusb error). For chip families whose RX descriptor is not
* the Jaguar1 layout (e.g. Jaguar3), the caller parses the buffer itself. */
Expand Down
30 changes: 30 additions & 0 deletions src/TxStats.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef DEVOURER_TX_STATS_H
#define DEVOURER_TX_STATS_H

#include <cstdint>

namespace devourer {

/* TX submission health — the driver-side drop signal an adaptive-link
* controller uses to detect congestion (the "xtx" cut-bitrate trigger). Counted
* at the USB bulk-OUT layer, so it is family-agnostic (all three HALs push TX
* through the same RtlUsbAdapter).
*
* `submitted` counts every frame handed to the USB stack; `failed` counts those
* that did not complete OK — either a synchronous submit/transfer error or an
* async URB completion with a non-OK status. The distinction that matters for
* congestion vs. a dead path is `last_was_timeout`: a bulk-OUT TIMEOUT
* (LIBUSB_ERROR_TIMEOUT / LIBUSB_TRANSFER_TIMED_OUT) is the chip NAKing because
* its TX FIFO is full — recoverable back-pressure, the xtx case — whereas a
* hard error (NO_DEVICE, pipe stall, ...) is a broken link. `last_error_rc` is
* the raw libusb code of the most recent failure (0 = none yet). */
struct TxStats {
uint64_t submitted = 0;
uint64_t failed = 0;
int last_error_rc = 0; /* raw libusb rc / negated transfer status */
bool last_was_timeout = false;
};

} // namespace devourer

#endif /* DEVOURER_TX_STATS_H */
1 change: 1 addition & 0 deletions src/jaguar1/RtlJaguarDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class RtlJaguarDevice : public IRtlDevice {
void ClearTxMode();

bool send_packet(const uint8_t* packet, size_t length) override;
devourer::TxStats GetTxStats() override { return _device.GetTxStats(); }
SelectedChannel GetSelectedChannel() override;

/* Runtime RX-chain selection — the adaptive-link spatial-diversity lever
Expand Down
1 change: 1 addition & 0 deletions src/jaguar2/RtlJaguar2Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class RtlJaguar2Device : public IRtlDevice {
void FastRetune(uint8_t channel, bool cache_rf) override;
void InitWrite(SelectedChannel channel) override;
bool send_packet(const uint8_t *packet, size_t length) override;
devourer::TxStats GetTxStats() override { return _device.GetTxStats(); }
SelectedChannel GetSelectedChannel() override;
void Stop() override;
void SetTxMode(const devourer::TxMode &mode) override;
Expand Down
1 change: 1 addition & 0 deletions src/jaguar3/RtlJaguar3Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class RtlJaguar3Device : public IRtlDevice {
void FastRetune(uint8_t channel, bool cache_rf) override;
void InitWrite(SelectedChannel channel) override;
bool send_packet(const uint8_t *packet, size_t length) override;
devourer::TxStats GetTxStats() override { return _device.GetTxStats(); }
SelectedChannel GetSelectedChannel() override;
void Stop() override;

Expand Down
80 changes: 80 additions & 0 deletions tests/tx_stats_congestion.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Validate GetTxStats (<devourer-txstats>) — the driver-drop / congestion signal.
# Back-to-back TX (DEVOURER_TX_GAP_US=0) fills the on-chip TX FIFO, so the USB
# bulk-OUT starts NAKing (LIBUSB_ERROR_TIMEOUT / LIBUSB_TRANSFER_TIMED_OUT) and
# `failed` climbs with was_timeout=1 — the recoverable back-pressure an adaptive
# controller reads as "cut bitrate". A paced link (GAP=2000, ~500 fps) drains
# fine, so failed stays ~0. The counter must distinguish the two.
#
# Runs per plugged DUT (each family so both the async send_packet and the sync
# bulk_send_sync_ep counting paths are exercised). No SDR / no second adapter.
#
# Usage: sudo -v && tests/tx_stats_congestion.sh
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="${TXSTATS_OUT:-/tmp/devourer-txstats}"
CH="${CH:-36}"
DUR="${DUR:-12}"
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 WiFiDriverTxDem 2>/dev/null||true; true; }
trap cleanup EXIT INT TERM
plugged() { lsusb -d "$(printf '%04x:%04x' "$2" "$1")" >/dev/null 2>&1; }

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

# pid vid label
DUTS=(
"0x8812 0x0bda 8812AU-J1"
"0x012d 0x2357 8822BU-J2"
"0xa81a 0x0bda 8822EU-J3"
)

# One TX cell; echoes "submitted failed was_timeout last_rc" from the final line.
run_cell() { # $1=pid $2=vid $3=gap $4=tag
local log="$OUT/$4.log"
sudo -n env DEVOURER_PID="$1" DEVOURER_VID="$2" DEVOURER_CHANNEL="$CH" \
DEVOURER_TX_RATE=MCS3 DEVOURER_TX_GAP_US="$3" \
timeout "$DUR" "$ROOT/build/WiFiDriverTxDemo" >"$log" 2>&1 || true
local line
line=$(grep "<devourer-txstats>" "$log" | tail -1)
echo "$line" | sed -E 's/.*submitted=([0-9]+) failed=([0-9]+) was_timeout=([0-9]+) last_rc=(-?[0-9]+).*/\1 \2 \3 \4/'
}

for dut in "${DUTS[@]}"; do
read -r PID VID LABEL <<<"$dut"
if ! plugged "$PID" "$VID"; then skip "$LABEL not plugged"; continue; fi
echo "== $LABEL =="
read -r p_sub p_fail p_to p_rc <<<"$(run_cell "$PID" "$VID" 2000 "$LABEL-paced")"
read -r b_sub b_fail b_to b_rc <<<"$(run_cell "$PID" "$VID" 0 "$LABEL-b2b")"
p_sub=${p_sub:-0}; p_fail=${p_fail:-0}; b_sub=${b_sub:-0}; b_fail=${b_fail:-0}
b_to=${b_to:-0}; b_rc=${b_rc:-0}
echo " paced (gap=2000): submitted=$p_sub failed=$p_fail"
echo " back-to-back (gap=0): submitted=$b_sub failed=$b_fail was_timeout=$b_to last_rc=$b_rc"
if [ "$p_sub" -lt 100 ]; then
fail "$LABEL: paced cell barely transmitted ($p_sub) — TX path issue, inconclusive"
continue
fi
# Congestion signature: back-to-back drives materially more failures than
# paced (either a real climb, or the was_timeout flag set). Some chips never
# fill the FIFO at USB2 speed — then failed stays ~0 in BOTH, which is a
# valid "no congestion observed" (the counter is still correct: it reports
# what happened). We only hard-fail if the counter is obviously broken
# (submitted not counting).
if [ "$b_sub" -lt 100 ]; then
fail "$LABEL: back-to-back submitted=$b_sub not counting"
elif [ "$b_fail" -gt "$p_fail" ] || [ "$b_to" -eq 1 ]; then
pass "$LABEL: congestion visible — b2b failed=$b_fail (was_timeout=$b_to) > paced failed=$p_fail"
else
pass "$LABEL: counter working, no FIFO congestion at this rate (b2b failed=$b_fail == paced) — reported honestly"
fi
done

echo
echo "== tx-stats congestion: PASS=$PASS FAIL=$FAIL SKIP=$SKIP =="
[ "$FAIL" -eq 0 ]
8 changes: 8 additions & 0 deletions txdemo/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,14 @@ int main(int argc, char **argv) {
frames_in_dwell = 0;
if (tx_count <= 10 || tx_count % 500 == 0) {
printf("<devourer-tx>TX #%ld rc=%d\n", tx_count, rc);
/* TX submission health — the driver-drop / congestion feed (xtx). A
* climbing failed with was_timeout=1 is a full TX FIFO (recoverable
* back-pressure); a hard rc is a broken path. */
auto ts = rtlDevice->GetTxStats();
printf("<devourer-txstats>submitted=%llu failed=%llu was_timeout=%d "
"last_rc=%d\n",
(unsigned long long)ts.submitted, (unsigned long long)ts.failed,
ts.last_was_timeout ? 1 : 0, ts.last_error_rc);
fflush(stdout);
}
/* Thermal telemetry via the generation-agnostic GetThermalStatus
Expand Down
Loading