102 lines
4.4 KiB
Bash
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
|