#!/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