diff options
Diffstat (limited to 'stacks/max25-bcpr/tools/max25-bcpr-ctl')
| -rwxr-xr-x | stacks/max25-bcpr/tools/max25-bcpr-ctl | 349 |
1 files changed, 349 insertions, 0 deletions
diff --git a/stacks/max25-bcpr/tools/max25-bcpr-ctl b/stacks/max25-bcpr/tools/max25-bcpr-ctl new file mode 100755 index 0000000..56f44b6 --- /dev/null +++ b/stacks/max25-bcpr/tools/max25-bcpr-ctl @@ -0,0 +1,349 @@ +#!/usr/bin/env bash +# max25-bcpr-ctl — preflight / start / status / stop / smoke for max25-bcprd (SER12). +# PC-COM / BayCom/based: TCM3105 AFSK + PTT. Host owns HDLC/KISS. +# No calibrate. No baycom_ser_fdx product path. +set -euo pipefail + +INI="${MAX25_BCPR_INI:-${BCPR_INI:-/etc/max25/max25-bcpr.ini}}" +BCPRD="${MAX25_BCPRD:-${BCPRD:-max25-bcprd}}" +BCPRD_INIT="${MAX25_BCPRD_INIT:-max25-bcprd-init}" +PIDFILE="${MAX25_BCPR_PIDFILE:-/tmp/max25-bcpr/max25-bcprd.pid}" +STATE_DIR="/tmp/max25-bcpr" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SMOKE_SH="$SCRIPT_DIR/max25-bcpr-rxtx-smoke.sh" + +usage() { + cat <<USAGE +Usage: max25-bcpr-ctl [-c ini] {preflight|start|status|stop|smoke|version} [smoke-opts] + smoke [--live] [--tx] [--seconds N] [--tx-seconds N] + Default smoke: NO TX. --tx-seconds default 3 (≈376B info / visible PTT). + Warn: SQ-open / RX-edge noise can stress hosts — see docs/BAYCOM-FREEZES.md +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -c) INI="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) break ;; + esac +done +CMD="${1:-}" +if [[ $# -gt 0 ]]; then + shift +fi + +resolve_max25_bcprd_init() { + if [[ -n "${BCPRD_INIT:-}" && "$BCPRD_INIT" != "max25-bcprd-init" && -x "$BCPRD_INIT" ]]; then + echo "$BCPRD_INIT"; return + fi + if command -v "$BCPRD_INIT" >/dev/null 2>&1; then + command -v "$BCPRD_INIT"; return + fi + local root cand + root="$(cd "$SCRIPT_DIR/../../.." && pwd)" + for cand in \ + ${MAX25_BCPR_BUILD_DIR:-${BCPR_BUILD_DIR:-}}/bin/max25-bcprd-init \ + "$root/build-max25-bcpr/bin/max25-bcprd-init" \ + "$root/build-max25-bcpr-${USER:-user}/bin/max25-bcprd-init" \ + "/tmp/max25-build-max25-bcpr-${USER:-user}/bin/max25-bcprd-init" \ + "$root/build/bin/max25-bcprd-init" \ + /usr/local/bin/max25-bcprd-init /usr/bin/max25-bcprd-init; do + [[ -n "$cand" && -x "$cand" ]] && { echo "$cand"; return; } + done + return 1 +} + +resolve_max25_bcprd() { + if [[ -n "${BCPRD:-}" && "$BCPRD" != "max25-bcprd" && -x "$BCPRD" ]]; then + echo "$BCPRD"; return + fi + if command -v "$BCPRD" >/dev/null 2>&1; then + command -v "$BCPRD"; return + fi + local root cand + root="$(cd "$SCRIPT_DIR/../../.." && pwd)" + for cand in \ + ${MAX25_BCPR_BUILD_DIR:-${BCPR_BUILD_DIR:-}}/bin/max25-bcprd \ + "$root/build-max25-bcpr/bin/max25-bcprd" \ + "$root/build-max25-bcpr-${USER:-user}/bin/max25-bcprd" \ + "/tmp/max25-build-max25-bcpr-${USER:-user}/bin/max25-bcprd" \ + "$root/build/bin/max25-bcprd" \ + /usr/local/bin/max25-bcprd /usr/bin/max25-bcprd; do + [[ -n "$cand" && -x "$cand" ]] && { echo "$cand"; return; } + done + return 1 +} + +ini_get() { + local section="$1" key="$2" + awk -F= -v s="[$section]" -v k="$key" ' + $0 ~ /^\[/ { cur=$0; gsub(/[[:space:]]/,"",cur) } + cur==s && $1 ~ "^[[:space:]]*"k"[[:space:]]*$" { + v=$2; gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); print v; exit + }' "$INI" 2>/dev/null || true +} + +is_truthy() { + case "${1,,}" in yes|true|on|1) return 0 ;; *) return 1 ;; esac +} + +find_setserial() { + local p + for p in /usr/bin/setserial /bin/setserial /sbin/setserial /usr/sbin/setserial; do + [[ -x "$p" ]] && { echo "$p"; return; } + done + command -v setserial 2>/dev/null || true +} + +# Parse "Port: 0x3f8" / "irq: 4" from setserial -g +setserial_port_irq() { + local bin="$1" dev="$2" out port irq + out="$("$bin" -g "$dev" 2>/dev/null)" || return 1 + # Typical: /dev/ttyS0, UART: 16550A, Port: 0x3f8, IRQ: 4 + port="$(printf '%s\n' "$out" | sed -n 's/.*Port:[[:space:]]*\(0x[0-9a-fA-F]\+\).*/\1/p' | head -1)" + irq="$(printf '%s\n' "$out" | sed -n 's/.*IRQ:[[:space:]]*\([0-9]\+\).*/\1/p' | head -1)" + printf '%s %s\n' "${port:-}" "${irq:-}" +} + +norm_hex() { + # lowercase 0x… without leading zeros differences: 0x3f8 vs 0x03f8 + local v="${1,,}" + if [[ "$v" =~ ^0x ]]; then + printf '0x%x' "$((v))" + else + printf '%s' "$v" + fi +} + +is_freebsd() { + [[ "$(uname -s)" == "FreeBSD" ]] +} + +port_busy() { + local serial="$1" + local out + # Prefer lsof (parse output); FreeBSD fuser exit status alone is unreliable. + if command -v lsof >/dev/null 2>&1; then + out="$(lsof "$serial" 2>/dev/null || true)" + if [[ -n "$out" ]]; then + return 0 + fi + return 1 + fi + if command -v fuser >/dev/null 2>&1; then + out="$(fuser "$serial" 2>/dev/null || true)" + [[ -n "$out" ]] + return $? + fi + return 1 +} + +baycom_modules_loaded() { + local m + if ! command -v lsmod >/dev/null 2>&1; then + return 1 + fi + for m in baycom_ser_fdx baycom_ser_hdx baycom_par baycom_epp; do + if lsmod 2>/dev/null | awk '{print $1}' | grep -qx "$m"; then + echo "$m" + return 0 + fi + done + return 1 +} + +cmd_preflight() { + local err=0 dry serial irq iobase name sysirq got ss ss_port ss_irq pair + local -a seen_irq=() seen_serial=() + [[ -f "$INI" ]] || { echo "ERROR: missing $INI"; return 1; } + dry="$(ini_get max25-bcpr dry_run)"; [[ -z "$dry" ]] && dry="$(ini_get bcpr dry_run)" + sd="$(ini_get max25-bcpr state_dir)"; [[ -z "$sd" ]] && sd="$(ini_get max25-bcpr state_dir)" + [[ -n "$sd" ]] && STATE_DIR="$sd" + echo "max25-bcpr-ctl preflight ini=$INI" + echo "NOTE: max25-bcpr → device max25e0 (BayCom/based SER12; live via max25-bcprd-init)" + + # Kernel BayCom modules must not share the path (A–O–Z isolate). + if mod="$(baycom_modules_loaded)"; then + echo " ERROR: kernel module loaded: $mod — unload before max25-bcpr (isolate)" + err=1 + else + echo " OK: no baycom_* modules loaded" + fi + + ss="$(find_setserial)" + if [[ -z "$ss" ]]; then + echo " WARN: setserial not found — iobase check limited to INI/sysfs" + else + echo " OK: setserial=$ss" + fi + + for sec in bc0 bc1; do + serial="$(ini_get "$sec" serial)" + [[ -z "$serial" ]] && continue + enabled="$(ini_get "$sec" enabled)" + if [[ -n "$enabled" ]] && ! is_truthy "$enabled"; then + echo " [$sec] enabled=no — skip" + continue + fi + irq="$(ini_get "$sec" irq)" + iobase="$(ini_get "$sec" iobase)" + echo " [$sec] serial=$serial irq=$irq iobase=$iobase" + + if is_truthy "$dry"; then + echo " dry_run=yes — skip live tty/IRQ/idle checks" + continue + fi + + if [[ ! -e "$serial" ]]; then + if is_freebsd && [[ -n "$iobase" && "$iobase" != "0" ]]; then + echo " WARN: $serial missing (uart disabled — SER12 uses iobase $iobase only)" + else + echo " ERROR: $serial missing"; err=1; continue + fi + else + echo " OK: serial node exists" + fi + + if [[ -e "$serial" ]]; then + if port_busy "$serial"; then + echo " ERROR: $serial busy (fuser/lsof) — exclusive lock fail-closed"; err=1 + else + echo " OK: port idle" + fi + elif is_freebsd && [[ -n "$iobase" && "$iobase" != "0" ]]; then + echo " OK: port idle (no tty node — ExSys iobase path)" + fi + + if [[ -z "$irq" || "$irq" == "0" ]]; then + echo " ERROR: irq must be real (non-zero)"; err=1 + fi + + name="${serial##*/}" + sysirq="/sys/class/tty/${name}/irq" + if [[ -r "$sysirq" ]]; then + got="$(tr -d '[:space:]' <"$sysirq")" + if [[ -n "$irq" && "$irq" != "0" && "$got" != "$irq" ]]; then + echo " ERROR: IRQ mismatch INI=$irq sysfs=$got"; err=1 + elif [[ -n "$irq" && "$irq" != "0" ]]; then + echo " OK: IRQ $irq matches sysfs" + fi + else + echo " WARN: cannot read $sysirq" + fi + + if [[ -n "$ss" && -n "$iobase" ]]; then + pair="$(setserial_port_irq "$ss" "$serial" || true)" + ss_port="$(printf '%s' "$pair" | awk '{print $1}')" + ss_irq="$(printf '%s' "$pair" | awk '{print $2}')" + if [[ -n "$ss_port" ]]; then + if [[ "$(norm_hex "$iobase")" != "$(norm_hex "$ss_port")" ]]; then + echo " ERROR: iobase mismatch INI=$iobase setserial=$ss_port"; err=1 + else + echo " OK: iobase $iobase matches setserial" + fi + else + echo " WARN: setserial did not report Port for $serial" + fi + if [[ -n "$ss_irq" && -n "$irq" && "$irq" != "0" && "$ss_irq" != "$irq" ]]; then + echo " ERROR: IRQ mismatch INI=$irq setserial=$ss_irq"; err=1 + elif [[ -n "$ss_irq" && -n "$irq" && "$irq" != "0" ]]; then + echo " OK: IRQ $irq matches setserial" + fi + elif [[ -n "$iobase" && -z "$ss" ]]; then + echo " WARN: cannot verify iobase=$iobase without setserial" + fi + + # Dual uniqueness + for s in "${seen_serial[@]:-}"; do + if [[ -n "$s" && "$s" == "$serial" ]]; then + echo " ERROR: duplicate serial $serial"; err=1 + fi + done + for i in "${seen_irq[@]:-}"; do + if [[ -n "$i" && -n "$irq" && "$irq" != "0" && "$i" == "$irq" ]]; then + echo " ERROR: duplicate IRQ $irq (bc0/bc1 must be distinct)"; err=1 + fi + done + seen_serial+=("$serial") + [[ -n "$irq" && "$irq" != "0" ]] && seen_irq+=("$irq") + done + + return "$err" +} + +cmd_start() { + local bin init dry + cmd_preflight || { echo "preflight failed"; return 1; } + sd="$(ini_get max25-bcpr state_dir)"; [[ -z "$sd" ]] && sd="$(ini_get bcpr state_dir)"; [[ -n "$sd" ]] && STATE_DIR="$sd" + mkdir -p "$STATE_DIR" + PIDFILE="$STATE_DIR/max25-bcprd.pid" + if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "already running pid=$(cat "$PIDFILE")"; return 0 + fi + dry="$(ini_get max25-bcpr dry_run)"; [[ -z "$dry" ]] && dry="$(ini_get bcpr dry_run)" + if is_truthy "$dry"; then + bin="$(resolve_max25_bcprd)" || { echo "max25-bcprd not found — build with -DMAX25_BUILD_MAX25_BCPR=ON"; return 1; } + "$bin" -c "$INI" --dry-run </dev/null >/dev/null 2>&1 & + echo $! >"$PIDFILE" + echo "started max25-bcprd (dry-run) pid=$(cat "$PIDFILE") → max25e0" + return 0 + fi + init="$(resolve_max25_bcprd_init)" || { echo "max25-bcprd-init not found — build/install with setuid"; return 1; } + # Init forks internally; parent writes pidfile and exits (no shell background). + if ! "$init" -c "$INI" </dev/null; then + echo "max25-bcprd-init failed (see stderr above)" + return 1 + fi + if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "started max25-bcprd pid=$(cat "$PIDFILE") → max25e0" + else + echo "max25-bcprd-init exited but pidfile missing or stale"; return 1 + fi +} + +cmd_stop() { + sd="$(ini_get max25-bcpr state_dir 2>/dev/null || true)" + [[ -z "${sd:-}" ]] && sd="$(ini_get bcpr state_dir 2>/dev/null || true)" + [[ -n "${sd:-}" ]] && STATE_DIR="$sd" + PIDFILE="$STATE_DIR/max25-bcprd.pid" + if [[ -f "$PIDFILE" ]]; then + kill "$(cat "$PIDFILE")" 2>/dev/null || true + rm -f "$PIDFILE" + fi + pkill -x max25-bcprd 2>/dev/null || true + pkill -x max25-bcprd-init 2>/dev/null || true + echo "stopped" +} + +cmd_status() { + sd="$(ini_get max25-bcpr state_dir 2>/dev/null || true)" + [[ -z "${sd:-}" ]] && sd="$(ini_get bcpr state_dir 2>/dev/null || true)" + [[ -n "${sd:-}" ]] && STATE_DIR="$sd" + PIDFILE="$STATE_DIR/max25-bcprd.pid" + if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "max25-bcprd: running pid=$(cat "$PIDFILE")" + else + echo "max25-bcprd: not running" + fi + for link in "$STATE_DIR"/kiss-bc*; do + [[ -e "$link" ]] || continue + echo " kiss_link $link -> $(readlink -f "$link" 2>/dev/null || echo '?')" + done +} + +cmd_smoke() { + [[ -x "$SMOKE_SH" ]] || { echo "ERROR: missing $SMOKE_SH"; return 1; } + # Forward remaining args; default INI from -c + exec "$SMOKE_SH" -c "$INI" "$@" +} + +case "$CMD" in + preflight) cmd_preflight ;; + start) cmd_start ;; + stop) cmd_stop ;; + status) cmd_status ;; + smoke) cmd_smoke "$@" ;; + version) echo "max25-bcpr-ctl 0.4.0 (SER12 → max25e0; live via max25-bcprd-init)" ;; + *) usage; exit 1 ;; +esac |
