From fa05a5f8238e9e1417235711656e5062ccc843a7 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Fri, 7 Aug 2026 18:23:28 +0000 Subject: Initial push --- stacks/max25-bcpr/tools/max25-bcpr-ctl | 379 +++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100755 stacks/max25-bcpr/tools/max25-bcpr-ctl (limited to 'stacks/max25-bcpr/tools/max25-bcpr-ctl') diff --git a/stacks/max25-bcpr/tools/max25-bcpr-ctl b/stacks/max25-bcpr/tools/max25-bcpr-ctl new file mode 100755 index 0000000..39510a7 --- /dev/null +++ b/stacks/max25-bcpr/tools/max25-bcpr-ctl @@ -0,0 +1,379 @@ +#!/usr/bin/env bash +# max25-bcpr-ctl — BayCom/based PC-COM helper (preflight / start / stop). +# Lite policy: plain English operator messages (new Linux user level). +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" + +msg() { echo "max25-bcpr-ctl: $*"; } +ok() { msg "OK — $*"; } +warn(){ msg "WARN — $*"; } +err() { msg "ERROR — $*" >&2; } + +usage() { + cat </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 +} + +setserial_port_irq() { + local bin="$1" dev="$2" out port irq + out="$("$bin" -g "$dev" 2>/dev/null)" || return 1 + 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() { + 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 + if command -v lsof >/dev/null 2>&1; then + out="$(lsof "$serial" 2>/dev/null || true)" + [[ -n "$out" ]] && return 0 + 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 fail=0 dry serial irq iobase name sysirq got ss ss_port ss_irq pair + local -a seen_irq=() seen_serial=() + if [[ ! -f "$INI" ]]; then + err "config not found ($INI) — run max25-lite-setup or copy stacks/max25-bcpr/share/max25-bcpr.ini.example" + return 1 + fi + 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" + msg "checking BayCom/based PC-COM setup (config: $INI)" + + if mod="$(baycom_modules_loaded)"; then + err "Linux BayCom kernel driver is loaded ($mod) — unload it first: sudo modprobe -r $mod" + fail=1 + else + ok "no conflicting Linux BayCom kernel driver" + fi + + ss="$(find_setserial)" + if [[ -z "$ss" ]]; then + warn "setserial not installed — limited port checks (optional: sudo apt install setserial)" + else + ok "setserial found" + 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 + msg "section [$sec] disabled — skipped" + continue + fi + irq="$(ini_get "$sec" irq)" + iobase="$(ini_get "$sec" iobase)" + msg "port [$sec]: serial=$serial irq=${irq:-?} iobase=${iobase:-?}" + + if is_truthy "$dry"; then + msg "dry-run mode — skipping live port checks" + continue + fi + + if [[ ! -e "$serial" ]]; then + if is_freebsd && [[ -n "$iobase" && "$iobase" != "0" ]]; then + warn "$serial not present — using direct I/O address $iobase (FreeBSD)" + else + err "serial port missing ($serial) — check cable, enable UART in BIOS, or fix max25-bcpr.ini" + fail=1; continue + fi + else + ok "serial port exists ($serial)" + fi + + if [[ -e "$serial" ]]; then + if port_busy "$serial"; then + err "serial port busy ($serial) — close minicom, other TNC tools, or second max25d" + fail=1 + else + ok "serial port free ($serial)" + fi + elif is_freebsd && [[ -n "$iobase" && "$iobase" != "0" ]]; then + ok "no tty node — direct I/O path" + fi + + if [[ -z "$irq" || "$irq" == "0" ]]; then + err "IRQ missing in [$sec] — set irq= in max25-bcpr.ini (often 4 for COM1 / ttyS0)" + fail=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 + err "IRQ mismatch — config says $irq but system says $got — fix max25-bcpr.ini" + fail=1 + elif [[ -n "$irq" && "$irq" != "0" ]]; then + ok "IRQ $irq matches system" + fi + else + warn "cannot read IRQ from $sysirq — check config by hand" + 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 + err "I/O address mismatch — config $iobase vs setserial $ss_port" + fail=1 + else + ok "I/O address matches setserial" + fi + else + warn "setserial did not report port address for $serial" + fi + if [[ -n "$ss_irq" && -n "$irq" && "$irq" != "0" && "$ss_irq" != "$irq" ]]; then + err "IRQ mismatch — config $irq vs setserial $ss_irq" + fail=1 + elif [[ -n "$ss_irq" && -n "$irq" && "$irq" != "0" ]]; then + ok "IRQ matches setserial" + fi + elif [[ -n "$iobase" && -z "$ss" ]]; then + warn "cannot verify I/O address $iobase without setserial" + fi + + for s in "${seen_serial[@]:-}"; do + if [[ -n "$s" && "$s" == "$serial" ]]; then + err "duplicate serial port $serial in config" + fail=1 + fi + done + for i in "${seen_irq[@]:-}"; do + if [[ -n "$i" && -n "$irq" && "$irq" != "0" && "$i" == "$irq" ]]; then + err "duplicate IRQ $irq — use different IRQ per port" + fail=1 + fi + done + seen_serial+=("$serial") + [[ -n "$irq" && "$irq" != "0" ]] && seen_irq+=("$irq") + done + + if [[ "$fail" -eq 0 ]]; then + ok "checks passed — you can run: max25-bcpr-ctl start -c $INI" + else + err "checks failed — fix errors above, then run preflight again" + fi + return "$fail" +} + +cmd_start() { + local bin init dry + cmd_preflight || 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 + ok "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)" || { + err "test program max25-bcprd not found — run: cmake -B build && cmake --build build" + return 1 + } + "$bin" -c "$INI" --dry-run /dev/null 2>&1 & + echo $! >"$PIDFILE" + ok "started test mode (dry-run) pid $(cat "$PIDFILE") — no radio TX" + return 0 + fi + init="$(resolve_max25_bcprd_init)" || { + err "start helper max25-bcprd-init not found — run: sudo cmake --install build" + return 1 + } + if ! "$init" -c "$INI" /dev/null; then + ok "PC-COM service running (pid $(cat "$PIDFILE")) — next: run-max25d.sh" + else + err "service exited unexpectedly — check errors above" + 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 + ok "PC-COM service 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 + msg "PC-COM service: running (pid $(cat "$PIDFILE"))" + else + msg "PC-COM service: not running — start with: max25-bcpr-ctl start -c $INI" + fi + for link in "$STATE_DIR"/kiss-bc*; do + [[ -e "$link" ]] || continue + msg " link $link → $(readlink -f "$link" 2>/dev/null || echo '?')" + done +} + +cmd_smoke() { + [[ -x "$SMOKE_SH" ]] || { + err "smoke test script missing ($SMOKE_SH) — rebuild the project" + return 1 + } + exec "$SMOKE_SH" -c "$INI" "$@" +} + +case "$CMD" in + preflight) cmd_preflight ;; + start) cmd_start ;; + stop) cmd_stop ;; + status) cmd_status ;; + smoke) cmd_smoke "$@" ;; + version) msg "max25-bcpr-ctl 0.5.0-lite (BayCom/based PC-COM)" ;; + *) usage; exit 1 ;; +esac -- cgit v1.3.1