1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
|
#!/usr/bin/env bash
# tx-rx-test.sh — unified TX/RX release test (BayCom/based max25-bcpr + classic TNC).
# Default: L0 offline only (CI / make test). Live/TX are operator-gated.
# Public: BayCom/based / max25-bcpr. Never Konverter/converter.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Tree: scripts/ → repo root. Install: PREFIX/bin or PREFIX/sbin → no stacks/.
if [[ -d "${SCRIPT_DIR}/../stacks/max25-bcpr" ]]; then
ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "$ROOT"
_BCPR_CTL_DEFAULT="${ROOT}/stacks/max25-bcpr/tools/max25-bcpr-ctl"
_BCPR_INI_DEFAULT="${ROOT}/stacks/max25-bcpr/share/max25-bcpr.ini.example"
else
ROOT=""
cd "${PWD:-/}"
_BCPR_CTL_DEFAULT="$(command -v max25-bcpr-ctl 2>/dev/null || true)"
_BCPR_INI_DEFAULT="/etc/max25/max25-bcpr.ini"
fi
LEVEL=0
DEVICE=all # all | modem | tnc
DO_LIVE=0
DO_TX=0
FORCE_TX=0
SECONDS_LIVE=15
TX_SECONDS=3
BCPR_INI="${BCPR_INI:-$_BCPR_INI_DEFAULT}"
SOCK="${MAX25_SOCK:-/run/max25/modem.sock}"
ERR=0
BCPR_CTL="${BCPR_CTL:-$_BCPR_CTL_DEFAULT}"
if [[ -z "$BCPR_CTL" ]] || [[ ! -x "$BCPR_CTL" ]]; then
if command -v max25-bcpr-ctl >/dev/null 2>&1; then
BCPR_CTL="$(command -v max25-bcpr-ctl)"
elif [[ -n "$ROOT" && -x "$ROOT/stacks/max25-bcpr/tools/max25-bcpr-ctl" ]]; then
BCPR_CTL="$ROOT/stacks/max25-bcpr/tools/max25-bcpr-ctl"
fi
fi
BCPR_SMOKE="${BCPR_SMOKE:-}"
if [[ -z "$BCPR_SMOKE" ]]; then
if [[ -n "$ROOT" && -x "$ROOT/stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh" ]]; then
BCPR_SMOKE="$ROOT/stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh"
elif command -v max25-bcpr-rxtx-smoke.sh >/dev/null 2>&1; then
BCPR_SMOKE="$(command -v max25-bcpr-rxtx-smoke.sh)"
elif [[ -x /usr/local/sbin/max25-bcpr-rxtx-smoke.sh ]]; then
BCPR_SMOKE=/usr/local/sbin/max25-bcpr-rxtx-smoke.sh
fi
fi
usage() {
cat <<'USAGE'
Usage: tx-rx-test.sh [--level N] [--device all|modem|tnc] [--live] [--tx] [--seconds N] [--tx-seconds N] [-c bcpr.ini]
L0 offline TX/RX proofs (default) — CI / release-check / ctest
L1 soft preflight (no attach / no RF)
L2 live attach (modem: max25-bcprd timed; tnc: max25d sock status)
L3 live RX listen (same as L2 + RX readiness)
L4 TX (--tx required; PTT / RF risk; default ~3s MCR key)
--live implies at least L2. --tx requires --live (L4).
--tx-seconds: modem PTT key target (default 3; proven ≈376B info).
Default: NO TX. Hard time caps. Safe for make test / CI.
Static rule (§0.20): live --tx is refused unless RX was detected in this
run (modem: Soft-DCD/noise → state_dir/dcd-bc* or rx-activity-bc*;
TNC: live CONNECT/RX path OK first). Override only with --force-tx
(against policy — debug only).
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--level) LEVEL="$2"; shift 2 ;;
--device) DEVICE="$2"; shift 2 ;;
--live) DO_LIVE=1; shift ;;
--tx) DO_TX=1; shift ;;
--force-tx) FORCE_TX=1; shift ;;
--seconds) SECONDS_LIVE="$2"; shift 2 ;;
--tx-seconds) TX_SECONDS="$2"; shift 2 ;;
-c) BCPR_INI="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown arg: $1"; usage; exit 2 ;;
esac
done
case "$DEVICE" in
all|modem|tnc) ;;
*) echo "ERROR: --device must be all|modem|tnc"; exit 2 ;;
esac
if [[ "$DO_TX" -eq 1 && "$DO_LIVE" -eq 0 ]]; then
echo "ERROR: --tx requires --live"
exit 2
fi
if [[ "$DO_LIVE" -eq 1 && "$LEVEL" -lt 2 ]]; then
LEVEL=2
fi
if [[ "$DO_TX" -eq 1 ]]; then
LEVEL=4
fi
if [[ "$SECONDS_LIVE" -lt 1 ]]; then
SECONDS_LIVE=1
fi
if [[ "$SECONDS_LIVE" -gt 60 ]]; then
echo "WARN: capping --seconds from $SECONDS_LIVE to 60"
SECONDS_LIVE=60
fi
if [[ "$TX_SECONDS" -lt 1 ]]; then
TX_SECONDS=1
fi
if [[ "$TX_SECONDS" -gt 12 ]]; then
echo "WARN: capping --tx-seconds from $TX_SECONDS to 12"
TX_SECONDS=12
fi
need_live=$((TX_SECONDS + 5))
if [[ "$DO_TX" -eq 1 && "$SECONDS_LIVE" -lt "$need_live" ]]; then
echo "NOTE: raising --seconds from $SECONDS_LIVE to $need_live for --tx-seconds $TX_SECONDS"
SECONDS_LIVE=$need_live
fi
log() { printf '%s\n' "$*"; }
ok() { log "PASS: $*"; }
fail() { log "FAIL: $*"; ERR=1; }
stage() { log ""; log "=== $* ==="; }
want_modem() { [[ "$DEVICE" == "all" || "$DEVICE" == "modem" ]]; }
want_tnc() { [[ "$DEVICE" == "all" || "$DEVICE" == "tnc" ]]; }
run_l0() {
stage "L0 offline TX/RX"
if [[ -n "$ROOT" && -f "$ROOT/stacks/daemon/test_tx_rx_offline.py" ]]; then
if python3 "$ROOT/stacks/daemon/test_tx_rx_offline.py"; then
ok "test_tx_rx_offline (TNC + bcpr host)"
else
fail "test_tx_rx_offline"
fi
else
ok "test_tx_rx_offline skipped (installed layout — use bcpr-rxtx-smoke)"
fi
if want_tnc; then
if [[ -f stacks/tncs/test_tnc_serial_recovery.py ]]; then
if python3 stacks/tncs/test_tnc_serial_recovery.py >/dev/null; then
ok "tnc serial recovery unit"
else
fail "tnc serial recovery unit"
fi
else
ok "tnc serial recovery unit skipped (stacks/tncs/ not shipped in this build — Lite: modem/BayCom-based only)"
fi
if python3 stacks/daemon/test_kiss_bridge.py >/dev/null; then
ok "kiss_bridge unit (TNC KISS)"
else
fail "kiss_bridge unit"
fi
fi
if want_modem; then
# Lite default tree builds max25-bcpr (MAX25_BUILD_MAX25_BCPR=ON). Do not
# auto-rebuild it here for L0 in CI / release-check — only smoke when
# binaries already exist or MAX25_BCPR_ALLOW_BUILD=1.
bcprd=""
for cand in \
"${BCPR_BUILD_DIR:-}/bin/max25-bcprd" \
"${BUILD_DIR:-}/bin/max25-bcprd" \
build/bin/max25-bcprd \
build-default/bin/max25-bcprd \
"build-max25-bcpr/bin/max25-bcprd" \
"/tmp/max25-build-max25-bcpr-${USER:-user}/bin/max25-bcprd"; do
if [[ -n "$cand" && -x "$cand" ]]; then
bcprd="$cand"
break
fi
done
if [[ -z "$bcprd" && "${MAX25_BCPR_ALLOW_BUILD:-0}" != "1" ]]; then
ok "max25-bcpr L0 skipped (not built — run cmake --build with MAX25_BUILD_MAX25_BCPR=ON, default in Lite)"
elif [[ -x stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh ]]; then
if [[ -n "$bcprd" ]]; then
if MAX25_BCPR_NO_AUTOBUILD=1 bash stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh -c "$BCPR_INI"; then
ok "max25-bcpr-rxtx-smoke L0 (BayCom/based)"
else
fail "max25-bcpr-rxtx-smoke L0"
fi
else
if bash stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh -c "$BCPR_INI"; then
ok "max25-bcpr-rxtx-smoke L0 (BayCom/based)"
else
fail "max25-bcpr-rxtx-smoke L0"
fi
fi
else
fail "max25-bcpr-rxtx-smoke.sh missing"
fi
fi
}
run_l1() {
stage "L1 soft preflight"
if want_modem; then
if [[ -n "${BCPR_CTL:-}" && -x "$BCPR_CTL" ]]; then
set +e
"$BCPR_CTL" -c "$BCPR_INI" preflight
pf_rc=$?
set -e
if [[ "$pf_rc" -eq 0 ]]; then
ok "max25-bcpr-ctl preflight"
else
if [[ "$DO_LIVE" -eq 1 ]]; then
fail "max25-bcpr-ctl preflight (required for --live modem)"
else
log "NOTE: bcpr preflight rc=$pf_rc — soft at L1 without --live"
fi
fi
else
fail "max25-bcpr-ctl missing (install max25-bcpr or set BCPR_CTL)"
fi
fi
if want_tnc; then
if [[ ! -d stacks/tncs ]]; then
ok "tnc preflight skipped (stacks/tncs/ not shipped in this build — Lite: modem/BayCom-based only)"
elif [[ -f share/clients/tnc2c.yaml ]] && [[ -f stacks/tncs/tnc_serial_recovery.py ]]; then
ok "tnc client profile + recovery module present"
else
fail "tnc profile/recovery missing"
fi
fi
}
live_tnc_sock() {
local mode="$1" # rx|tx
local sock="$SOCK"
if [[ ! -S "$sock" ]]; then
if [[ -S /tmp/max25/modem.sock ]]; then
sock=/tmp/max25/modem.sock
else
fail "no max25d unix socket ($SOCK) — start max25d with TNC first"
return 1
fi
fi
log "using unix socket $sock (mode=$mode)"
python3 - "$sock" "$mode" <<'PY' || return 1
import socket, sys, time
sock_path, mode = sys.argv[1], sys.argv[2]
class LineReader:
def __init__(self, s):
self.s = s
self.buf = b""
def read(self, timeout=8.0):
self.s.settimeout(timeout)
while b"\n" not in self.buf:
chunk = self.s.recv(4096)
if not chunk:
raise RuntimeError("connection closed")
self.buf += chunk
line, self.buf = self.buf.split(b"\n", 1)
return line.decode("utf-8", errors="replace")
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(sock_path)
r = LineReader(s)
hello = r.read()
assert hello == "OK", hello
status = r.read()
assert status.startswith("STATUS "), status
s.sendall(b"CONNECT\n")
ev = r.read()
assert ev == "EVENT connected", ev
assert r.read() == "OK"
if mode == "tx":
s.sendall(b"SEND TXRXTEST\n")
rx = r.read()
assert rx.startswith("RX "), rx
assert r.read() == "OK"
print("TNC live TX: SEND OK (host path)")
else:
# RX readiness: connected + status shows device
s.sendall(b"GET STATUS\n")
st = r.read()
assert "device=" in st, st
assert r.read() == "OK"
print("TNC live RX: CONNECT + STATUS OK (listen ready)")
s.close()
PY
}
run_live() {
stage "L2/L3 live (${SECONDS_LIVE}s) device=$DEVICE"
if want_modem; then
local smoke_args=(-c "$BCPR_INI" --live --seconds "$SECONDS_LIVE")
if [[ "$DO_TX" -eq 1 ]]; then
smoke_args+=(--tx --tx-seconds "$TX_SECONDS")
if [[ "${FORCE_TX:-0}" -eq 1 ]]; then
smoke_args+=(--force-tx)
fi
log "WARN: modem --tx asserts PTT (~${TX_SECONDS}s; watch LED/wattmeter) — only after RX (§0.20)"
fi
_smoke="${BCPR_SMOKE:-stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh}"
if [[ ! -x "$_smoke" ]]; then
fail "max25-bcpr-rxtx-smoke.sh missing (install max25-bcpr or set BCPR_SMOKE)"
elif bash "$_smoke" "${smoke_args[@]}"; then
ok "bcpr live smoke (BayCom/based)"
else
fail "bcpr live smoke"
fi
fi
if want_tnc; then
local TNC_RX_OK=0
if live_tnc_sock rx; then
ok "tnc live CONNECT (RX ready)"
TNC_RX_OK=1
else
fail "tnc live CONNECT"
fi
if [[ "$DO_TX" -eq 1 ]]; then
if [[ "$TNC_RX_OK" -ne 1 && "${FORCE_TX:-0}" -eq 0 ]]; then
fail "TNC TX blocked (§0.20): RX/CONNECT failed"
else
log "WARN: TNC --tx sends one UI frame over air if radio keyed"
if live_tnc_sock tx; then
ok "tnc live CONNECT+SEND"
else
fail "tnc live CONNECT+SEND"
fi
fi
fi
fi
}
# --- main ---
log "tx-rx-test level=$LEVEL device=$DEVICE live=$DO_LIVE tx=$DO_TX tx_seconds=$TX_SECONDS"
run_l0
if [[ "$LEVEL" -ge 1 ]]; then
run_l1
fi
if [[ "$LEVEL" -ge 2 || "$DO_LIVE" -eq 1 ]]; then
run_live
fi
stage "summary"
if [[ "$ERR" -eq 0 ]]; then
if [[ "$DO_TX" -eq 1 ]]; then
ok "TX/RX complete through L4 (live + TX)"
elif [[ "$DO_LIVE" -eq 1 ]]; then
ok "TX/RX complete through L2/L3 (live RX, no TX)"
elif [[ "$LEVEL" -ge 1 ]]; then
ok "TX/RX L0+L1 complete"
else
ok "TX/RX L0 offline complete — use --live / --tx for hardware"
fi
exit 0
fi
fail "one or more stages failed"
exit 1
|