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
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,16 @@ add_executable(BusyWindowProbe
target_include_directories(BusyWindowProbe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common)
target_link_libraries(BusyWindowProbe PUBLIC devourer PRIVATE PkgConfig::libusb)

# Bench driver for the StopRxLoop contract on IRadio.h: a stop issued while
# Init() is still bringing the chip up must end Init without a loop, and a
# stale stop must not refuse the next loop. tests/stop_during_init.sh runs the
# matrix; a hung Init is reported as DEADLOCK, never waited out.
add_executable(StopDuringInitProbe
tests/stop_during_init_probe.cpp
)
target_include_directories(StopDuringInitProbe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/examples/common)
target_link_libraries(StopDuringInitProbe PUBLIC devourer PRIVATE PkgConfig::libusb)

# Headless regression guard for the binary-stdin framing shared by the two
# stream demos above (examples/common/stream_stdin.h). No libusb, no radio — just the
# set_stdin_binary() + read_exact() path, so a text-mode regression (e.g. the
Expand Down
9 changes: 6 additions & 3 deletions src/IRadio.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,12 @@ class IRadio {

/* Ask a running StartRxLoop to exit (sets a flag; the caller then joins
* whatever thread runs StartRxLoop). Default no-op.
* Note: Covers the Init + StartRxLoop race window; the InitWrite +
* StartRxLoop-on-a-thread shape (DEVOURER_TX_WITH_RX=thread) operates outside
* this window and is out of scope. */
*
* A stop issued while Init() is still bringing the chip up is honoured:
* Init returns without starting its loop. A stop issued before Init() or
* before a standalone StartRxLoop() is stale and cleared by them (both are
* restartable), which includes a stop that lands between InitWrite() and a
* StartRxLoop() on another thread (DEVOURER_TX_WITH_RX=thread). */
virtual void StopRxLoop() {}
virtual void SetMonitorChannel(SelectedChannel channel) = 0;

Expand Down
2 changes: 2 additions & 0 deletions src/jaguar1/RtlJaguarDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1809,6 +1809,8 @@ void RtlJaguarDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
}

void RtlJaguarDevice::start_rx_loop(Action_ParsedRadioPacket packetProcessor, bool clear_stale) {
/* A signal has already landed: skip the setup below rather than build it
* for a loop whose own predicate would exit on its first pass. */
if (g_devourer_should_stop)
return;

Expand Down
2 changes: 2 additions & 0 deletions src/jaguar2/RtlJaguar2Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,8 @@ void RtlJaguar2Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
}

void RtlJaguar2Device::start_rx_loop(Action_ParsedRadioPacket packetProcessor, bool clear_stale) {
/* A signal has already landed: skip the setup below rather than build it
* for a loop whose own predicate would exit on its first pass. */
if (g_devourer_should_stop)
return;

Expand Down
2 changes: 2 additions & 0 deletions src/jaguar3/RtlJaguar3Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ void RtlJaguar3Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
}

void RtlJaguar3Device::start_rx_loop(Action_ParsedRadioPacket packetProcessor, bool clear_stale) {
/* A signal has already landed: skip the setup below rather than build it
* for a loop whose own predicate would exit on its first pass. */
if (g_devourer_should_stop)
return;

Expand Down
2 changes: 2 additions & 0 deletions src/kestrel/RtlKestrelDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,8 @@ void RtlKestrelDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
}

void RtlKestrelDevice::start_rx_loop(Action_ParsedRadioPacket packetProcessor, bool clear_stale) {
/* A signal has already landed: skip the setup below rather than build it
* for a loop whose own predicate would exit on its first pass. */
if (g_devourer_should_stop)
return;

Expand Down
2 changes: 2 additions & 0 deletions src/mt7612u/Mt7612uRadio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ void Mt7612uRadio::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
}

void Mt7612uRadio::start_rx_loop(Action_ParsedRadioPacket packetProcessor, bool clear_stale) {
/* A signal has already landed: skip the setup below rather than build it
* for a loop whose own predicate would exit on its first pass. */
if (g_devourer_should_stop)
return;

Expand Down
2 changes: 2 additions & 0 deletions src/rtl8733b/Rtl8733bDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,8 @@ void Rtl8733bDevice::StartRxLoop(

void Rtl8733bDevice::start_rx_loop(
Action_ParsedRadioPacket packetProcessor, bool clear_stale) {
/* A signal has already landed: skip the setup below rather than build it
* for a loop whose own predicate would exit on its first pass. */
if (g_devourer_should_stop)
return;

Expand Down
23 changes: 23 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,29 @@ quiesce must still return, and a `tx.quiesce_timeout` event in the log says it
never drained. `BUILD=<dir>` points at another tree, which is how a fix is
shown to fix something rather than asserted to.

### `stop_during_init.sh`: a stop that lands inside `Init()`

Drives `build/StopDuringInitProbe` through the `StopRxLoop` contract stated
on its declaration in `src/IRadio.h`: a stop at 100 / 400 / 1000 / 2500 ms
into `Init` (each must find Init still running, end it, and leave a chip
that then receives on a standalone loop), a standalone restart after a stop,
and a stale stop before `Init`. The frame checks need a channel with
traffic; ambient beacons do.

```bash
sudo tests/stop_during_init.sh # 8822BU, ch6
sudo env PID=0xa81a CHANNEL=36 tests/stop_during_init.sh # 8812EU
sudo env VID=0x35bc PID=0x0101 CHANNEL=36 NO_RESET=1 tests/stop_during_init.sh
```

`sudo env VAR=...`, not `VAR=... sudo`: sudo scrubs the environment the
script reads its knobs from. A `DEADLOCK` line means `Init` never came back;
the probe drops the USB lock and exits, so the adapter may need a re-plug.
The Kestrel run needs `NO_RESET=1`: a USB reset on its running firmware
re-enumerates the adapter out from under the probe. Measured on the 8822BU,
8812EU and 8832CU; the 8812AU, RTL8733B and MT7612U carry the same guard and
have not been run.

### `kestrel_prich_onair.sh`: Kestrel per-channel BB programming

Counts delivered frames across the configurations that the vendored
Expand Down
74 changes: 74 additions & 0 deletions tests/stop_during_init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Does a StopRxLoop() issued while Init() is still bringing the chip up end
# Init, and does a stale stop leave the next loop alone? Six scenarios on one
# adapter through build/StopDuringInitProbe (the contract is on StopRxLoop in
# src/IRadio.h):
#
# initstop at 100 / 400 / 1000 / 2500 ms -- the first two or three land
# inside bring-up on every generation (Kestrel's is ~3.4 s, so
# all four do there); the rest hit a running loop. Init must
# still be running when the stop lands, must return after it,
# and the chip it left behind must then receive on a standalone
# loop -- so a bring-up that failed and returned on its own
# cannot pass as an honoured stop.
# restart stop + join, then a standalone StartRxLoop on a new thread --
# must run and see frames.
# stale StopRxLoop before Init -- Init's own loop must still run.
#
# The frame checks need a channel that carries traffic (ambient beacons do).
#
# sudo tests/stop_during_init.sh # 8822BU, ch6
# sudo env PID=0xa81a CHANNEL=36 tests/stop_during_init.sh # 8812EU
# sudo env VID=0x35bc PID=0x0101 CHANNEL=36 NO_RESET=1 tests/stop_during_init.sh
#
# (`sudo env ...` rather than `VAR=... sudo`: sudo scrubs the caller's
# environment, and the script reads its knobs from its own.)
#
# A DEADLOCK line means the Init thread never came back: the probe drops the
# USB lock and _Exits, so the adapter may need a re-plug before the next run.
# Exit 0 = every scenario OK, 1 = at least one failed or produced no result.
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PROBE="$ROOT/build/StopDuringInitProbe"
VID="${VID:-0x0bda}"
PID="${PID:-0xb812}"
CHANNEL="${CHANNEL:-6}"
NO_RESET="${NO_RESET:-0}"
TIMEOUT_S="${TIMEOUT_S:-20}"

[ -x "$PROBE" ] || { echo "missing $PROBE (cmake --build build --target StopDuringInitProbe)" >&2; exit 1; }
# A fresh private directory per run: the script is documented as a sudo run,
# and a predictable path under /tmp would let any local user plant a symlink
# for root's stderr redirection to follow.
LOG="$(mktemp -d "${TMPDIR:-/tmp}/devourer-stop-during-init.XXXXXX")"
child=""
cleanup() {
# The probe's comm is truncated to 15 characters, so a name match cannot
# find it; kill the child this script started, by pid.
[ -n "$child" ] && kill "$child" 2>/dev/null
[ -n "$child" ] && wait "$child" 2>/dev/null
:
}
trap cleanup EXIT INT TERM

extra=()
[ "$NO_RESET" = 1 ] && extra+=(--no-reset)
fail=0
run() { # name args...
local name=$1; shift
local out
timeout $((TIMEOUT_S * 3)) "$PROBE" --vid "$VID" --pid "$PID" --channel "$CHANNEL" \
--timeout-s "$TIMEOUT_S" "${extra[@]}" "$@" >"$LOG/$name.out" 2>"$LOG/$name.err" &
child=$!
wait "$child"
child=""
out=$(grep RESULT "$LOG/$name.out")
echo "[$VID:$PID ch$CHANNEL] $name: ${out:-NO-RESULT}"
case "$out" in *outcome=OK*) ;; *) fail=1 ;; esac
sleep 1
}
for ms in 100 400 1000 2500; do run "initstop_$ms" --mode initstop --stop-at-ms "$ms"; done
run restart --mode restart --stop-at-ms 6000 --rx-ms 4000
run stale --mode stale --rx-ms 6000
if [ $fail = 0 ]; then echo "VERDICT: PASS"; rm -rf "$LOG"; else echo "VERDICT: FAIL (logs in $LOG)"; fi
exit $fail
Loading
Loading