summaryrefslogtreecommitdiff
path: root/scripts/tx-rx-test.sh
blob: cdfbdca3cc564f49652199bbeeaccc83ae6003c2 (plain)
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
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com