Files
2026-09-13 13:30:21 +08:00

102 lines
4.4 KiB
Bash

#!/usr/bin/env bash
# Rendezvous building block — re-find a peer when the link has dropped. The
# mains-powered "ground station" parks a modulated continuous beacon
# (DEVOURER_CONT_TX) on a known channel; the battery-powered "drone" scans the
# candidate channels with its frame-free energy sensor (DEVOURER_RX_SWEEP) and
# locks onto the channel carrying the beacon. This is the asymmetric-duty
# rendezvous the adaptive-link design describes — an expensive always-on beaconer,
# a cheap low-duty listener — built from the continuous-TX stimulus + the RX
# energy sensor. Two adapters, no SDR; the beacon works on all three generations.
#
# sudo tests/rendezvous.sh --beacon-pid 0x8812 --scanner-pid 0xc812 \
# --beacon-channel 6 --channels 1,6,11
#
# The scanner should report its peak (or, on a saturating 1T1R part, dip) at the
# beacon's channel — "peer found". The scanner does not know --beacon-channel; it
# discovers it from the sweep.
set -u
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
DEMO_TX="$ROOT/build/txdemo"
DEMO_RX="$ROOT/build/rxdemo"
BEACON_VID=0x0bda BEACON_PID="" BEACON_CHANNEL=6 RATE=MCS0
SCANNER_VID=0x0bda SCANNER_PID="" CHANNELS="1,6,11" DWELL_MS=350 ROUNDS=4
OUT=/tmp/devourer-rendezvous
while [ $# -gt 0 ]; do
case "$1" in
--beacon-vid) BEACON_VID="$2"; shift 2 ;;
--beacon-pid) BEACON_PID="$2"; shift 2 ;;
--beacon-channel) BEACON_CHANNEL="$2"; shift 2 ;;
--rate) RATE="$2"; shift 2 ;;
--scanner-vid) SCANNER_VID="$2"; shift 2 ;;
--scanner-pid) SCANNER_PID="$2"; shift 2 ;;
--channels) CHANNELS="$2"; shift 2 ;;
--dwell-ms) DWELL_MS="$2"; shift 2 ;;
--rounds) ROUNDS="$2"; shift 2 ;;
--outdir) OUT="$2"; shift 2 ;;
*) echo "unknown arg: $1" >&2; exit 2 ;;
esac
done
[ -n "$BEACON_PID" ] && [ -n "$SCANNER_PID" ] || {
echo "need --beacon-pid and --scanner-pid" >&2; exit 2; }
cleanup() { for c in txdemo rxdemo; do pkill -x "$c" 2>/dev/null || true; done; }
trap cleanup EXIT INT TERM
echo "== building =="
cmake --build "$ROOT/build" -j >/dev/null
mkdir -p "$OUT"; rm -f "$OUT"/sweep.log
cleanup; sleep 1
echo "== ground beacon: $BEACON_PID modulated continuous carrier on ch$BEACON_CHANNEL =="
timeout -sINT 600 env DEVOURER_VID="$BEACON_VID" DEVOURER_PID="$BEACON_PID" \
DEVOURER_CHANNEL="$BEACON_CHANNEL" DEVOURER_TX_RATE="$RATE" DEVOURER_CONT_TX=1 \
"$DEMO_TX" >"$OUT/beacon.log" 2>&1 &
for _ in $(seq 40); do grep -q "continuous TX armed\|carrier up" "$OUT/beacon.log" && break; sleep 0.5; done
grep -q "continuous TX armed\|carrier up" "$OUT/beacon.log" && echo " beacon up" \
|| echo " WARN: beacon not up (see $OUT/beacon.log)"
nbins="$(awk -F',' '{print NF}' <<< "$CHANNELS")"
secs="$(( (nbins * ROUNDS * DWELL_MS) / 1000 + 8 ))"
echo "== drone scan: sweep $CHANNELS x $ROUNDS rounds (~${secs}s), locking onto the beacon =="
timeout -sINT "$secs" env DEVOURER_VID="$SCANNER_VID" DEVOURER_PID="$SCANNER_PID" \
DEVOURER_RX_SWEEP="$CHANNELS" DEVOURER_RX_SWEEP_DWELL_MS="$DWELL_MS" \
"$DEMO_RX" 2>/dev/null | grep --line-buffered "devourer-energy" \
| tee "$OUT/sweep.log" || true
cleanup
echo "== rendezvous result =="
# Pick the channel with the most total in-band energy (OFDM + CCK CCA) — the
# modulated beacon can register on either detector, so sum both rather than key
# on cca_ofdm alone (the CW-tone sweep's metric).
python3 - "$OUT/sweep.log" <<'PY'
import re, sys, statistics
per = {}
for line in open(sys.argv[1]):
kv = dict(re.findall(r"(\w+)=(-?\d+)", line))
if "ch" not in kv:
continue
ch = int(kv["ch"])
e = int(kv.get("cca_ofdm", 0)) + int(kv.get("cca_cck", 0))
per.setdefault(ch, []).append(e)
if not per:
print("no sweep samples — beacon or scan failed"); sys.exit(1)
energy = {ch: statistics.median(v) for ch, v in per.items()}
width = 46
peak = max(energy.values()) or 1
for ch in sorted(energy):
bar = "#" * round(width * energy[ch] / peak)
print(f" ch {ch:>3} | {bar:<{width}} {int(energy[ch]):>6}")
best = max(energy, key=energy.__getitem__)
floor = statistics.median(list(energy.values()))
if energy[best] > 3 * max(floor, 1):
print(f"\nRENDEZVOUS: peer found on channel {best} "
f"(energy {int(energy[best])} vs floor {int(floor)}).")
else:
print(f"\nRENDEZVOUS: no clear peer — strongest ch {best} "
f"(energy {int(energy[best])}, floor {int(floor)}); beacon too weak or "
f"off the swept list.")
PY