#!/usr/bin/env bash # max25-ctl — MAX25-Stack-Lite control entry point (Packet Radio / AX.25, localhost). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PREFIX="" ROOT="" if [[ -f "${SCRIPT_DIR}/../plugins/manifest.yaml" ]]; then ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" elif [[ -d "${SCRIPT_DIR}/../share/max25" ]]; then PREFIX="$(cd "${SCRIPT_DIR}/.." && pwd)" ROOT="${MAX25_ROOT:-${PREFIX}}" else ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" fi stack_script() { local rel="$1" if [[ -f "${ROOT}/stacks/${rel}" ]]; then echo "${ROOT}/stacks/${rel}" return 0 fi if [[ -n "${PREFIX}" && -f "${PREFIX}/share/max25/stacks/${rel}" ]]; then echo "${PREFIX}/share/max25/stacks/${rel}" return 0 fi return 1 } resolve_crdop_bin() { if [[ -n "${CRDOP_BIN:-}" && -x "${CRDOP_BIN}" ]]; then echo "${CRDOP_BIN}" return 0 fi if command -v crdopc >/dev/null 2>&1; then command -v crdopc return 0 fi if [[ -x "${ROOT}/build/bin/crdopc" ]]; then echo "${ROOT}/build/bin/crdopc" return 0 fi return 1 } resolve_baycom_ctl() { local ctl="" ctl="$(stack_script "baycom-pr/scripts/baycom-pr-ctl" 2>/dev/null || true)" if [[ -n "${ctl}" && -x "${ctl}" ]]; then echo "${ctl}" return 0 fi if command -v baycom-pr-ctl >/dev/null 2>&1; then command -v baycom-pr-ctl return 0 fi return 1 } MODE="${MAX25_MODE:-standalone}" HARDWARE="" DEVICE="" BAYCOM_INI="" BAYCOM_PROFILE="" ACTION="${1:-help}" shift || true usage() { cat < [options] Commands: help This help discover List local device backends build Build the project (cmake -B build && cmake --build build) start Start a hardware/soft-modem path stop Stop a hardware/soft-modem path status Show device status test Offline tests (no root) Daemon (Linux): stacks/daemon/max25d Launcher (ps shows this path, not python3) stacks/daemon/max25d.py M25/1 daemon implementation stacks/terminal/max25-terminal Operator UI (F10 menu, CALLERID/CALLID) Options (start/stop/status): --hardware HW modems | soft-modems --device DEV max25e0 (BayCom/based) | baycom-kiss | soft-crdop Examples: max25-ctl discover max25-ctl build max25-ctl start --hardware modems --device max25e0 max25-ctl start --hardware modems --device baycom-kiss max25-ctl start --hardware soft-modems --device soft-crdop max25-ctl test Lite scope (max25-terminal, max25d, BayCom/based, CRDOP): README.md SET DEVICE → CONNECT → SEND via max25-terminal after max25d is up EOF } parse_opts() { while [[ $# -gt 0 ]]; do case "$1" in --mode) MODE="$2"; shift 2 ;; --hardware) HARDWARE="$2"; shift 2 ;; --device) DEVICE="$2"; shift 2 ;; --baycom-ini) BAYCOM_INI="$2"; shift 2 ;; --baycom-profile) BAYCOM_PROFILE="$2"; shift 2 ;; *) echo "Unknown option: $1" >&2; usage; exit 1 ;; esac done } resolve_baycom_ini_path() { local device="${1:-max25e0}" local explicit="${2:-${BAYCOM_INI:-}}" local profile="${3:-${BAYCOM_PROFILE:-}}" if [[ -z "${explicit}" && -n "${profile}" ]]; then local paths_py="" if [[ -f "${ROOT}/stacks/daemon/paths.py" ]]; then paths_py="${ROOT}/stacks/daemon/paths.py" elif [[ -n "${PREFIX}" && -f "${PREFIX}/bin/paths.py" ]]; then paths_py="${PREFIX}/bin/paths.py" fi if [[ -n "${paths_py}" ]]; then local prefix_py="None" if [[ -n "${PREFIX}" ]]; then prefix_py="Path('${PREFIX}')" fi explicit="$(python3 -c " import sys from pathlib import Path sys.path.insert(0, '$(dirname "${paths_py}")') from paths import resolve_baycom_profile p = resolve_baycom_profile('${profile}', '${device}', Path('${ROOT}'), ${prefix_py}) print(p or '') " 2>/dev/null || true)" if [[ -z "${explicit}" ]]; then echo "baycom profile '${profile}' not found (use --baycom-ini PATH)" >&2 return 1 fi elif [[ "${profile}" == "dual" ]]; then for candidate in \ "${ROOT}/stacks/baycom-pr/config/examples/baycom-pr.dual.ini" \ "${PREFIX}/share/max25/stacks/baycom-pr/config/examples/baycom-pr.dual.ini"; do if [[ -f "${candidate}" ]]; then explicit="${candidate}" break fi done if [[ -z "${explicit}" ]]; then echo "dual baycom-pr.ini not found — copy stacks/baycom-pr/config/examples/baycom-pr.dual.ini" >&2 return 1 fi fi fi local paths_py="" if [[ -f "${ROOT}/stacks/daemon/paths.py" ]]; then paths_py="${ROOT}/stacks/daemon/paths.py" elif [[ -n "${PREFIX}" && -f "${PREFIX}/bin/paths.py" ]]; then paths_py="${PREFIX}/bin/paths.py" fi if [[ -n "${paths_py}" ]]; then local resolved="" local prefix_py="None" if [[ -n "${PREFIX}" ]]; then prefix_py="Path('${PREFIX}')" fi resolved="$(python3 -c " import sys from pathlib import Path sys.path.insert(0, '$(dirname "${paths_py}")') from paths import resolve_baycom_ini p = resolve_baycom_ini('${device}', Path('${ROOT}'), ${prefix_py}, '''${explicit}''') print(p or '') " 2>/dev/null || true)" if [[ -n "${resolved}" && -f "${resolved}" ]]; then echo "${resolved}" return 0 fi fi if [[ -n "${explicit}" ]]; then return 1 fi local candidate="" for candidate in \ "/etc/baycom/baycom-pr.ini" \ "/etc/baycom/baycom-pr-single.ini" \ "${ROOT}/local/baycom-pr.ini" \ "${ROOT}/share/baycom/baycom-pr.pccom-ttyS0-only.ini.example" \ "${PREFIX}/share/max25/baycom/baycom-pr.pccom-ttyS0-only.ini.example" \ "${ROOT}/stacks/baycom-pr/config/baycom-pr.ini"; do if [[ -f "${candidate}" ]]; then if [[ "${candidate}" == "/etc/baycom/baycom-pr.ini" ]]; then local dual="" dual="$(python3 -c " from pathlib import Path import sys sys.path.insert(0, '${ROOT}/stacks/daemon') try: from paths import is_dual_baycom_ini print('dual' if is_dual_baycom_ini(Path('/etc/baycom/baycom-pr.ini')) else 'single') except ImportError: print('single') " 2>/dev/null || echo "single")" if [[ "${dual}" == "dual" ]]; then continue fi fi echo "${candidate}" return 0 fi done return 1 } start_baycom_kernel() { # Kernel baycom-pr removed 2026-07-18 — product path is bcpr. echo "BayCom/based uses max25-bcpr (not old kernel BayCom)." >&2 echo "Start PC-COM first: sudo max25-bcpr-ctl start --ini /etc/max25/max25-bcpr.ini" >&2 echo "Then max25d — see stacks/max25-bcpr/README.md" >&2 echo "Frozen archive: MASTER vault archives/legacy/max25-stack-baycom-kernel-compa/" >&2 exit 1 } cmd_discover() { if [[ ! -f "${ROOT}/scripts/discover-plugins.sh" ]]; then echo "discover: source checkout required (set MAX25_ROOT)" >&2 exit 1 fi bash "${ROOT}/scripts/discover-plugins.sh" "$@" } cmd_build() { if [[ ! -f "${ROOT}/scripts/build.sh" ]]; then echo "build: source checkout required (set MAX25_ROOT)" >&2 exit 1 fi bash "${ROOT}/scripts/build.sh" } cmd_start() { parse_opts "$@" case "$HARDWARE" in tncs) case "$DEVICE" in tnc2c|"") local boot="" boot="$(stack_script "tncs/tnc2c-boot-wait.sh")" if [[ "${MAX25_TNC_PREP:-}" == "recover" ]]; then echo "TNC recover-only (software ladder, no power cycle)" exec "${boot}" --recover-only fi echo "TNC boot-wait rescue: power-cycle TNC while script runs if needed" exec "${boot}" ;; pktnc2) local boot="" boot="$(stack_script "tncs/pktnc2-boot-wait.sh")" if [[ "${MAX25_TNC_PREP:-}" == "recover" ]]; then echo "PK-TNC2 recover-only (software ladder, no power cycle)" exec "${boot}" --recover-only fi exec "${boot}" ;; *) echo "Unknown TNC device: $DEVICE" >&2; exit 1 ;; esac ;; modems) case "$DEVICE" in baycom-kiss|pccom-kiss) echo "baycom-kiss: USB/async KISS — no kernel stack start" echo "Configure max25d [devices] baycom-kiss and [serial.baycom-kiss]; max25d opens serial directly." exit 0 ;; bcpr|max25e0|max25e0:bc0|max25e0:bc1|"") echo "BayCom/based PC-COM: sudo max25-bcpr-ctl start -c /etc/max25/max25-bcpr.ini" >&2 echo "Then: ./scripts/run-max25d.sh — see stacks/max25-bcpr/README.md" >&2 echo "Canonical: scripts/run-max25d.sh" >&2 exit 1 ;; baycom-par96|baycom-ser12|baycom-a|baycom-b) start_baycom_kernel "${DEVICE}" ;; *) echo "Unknown modem device: $DEVICE (use max25e0 / baycom-kiss)" >&2 exit 1 ;; esac ;; soft-modems) case "$DEVICE" in soft-crdop|"") local crdop_bin="" crdop_bin="$(resolve_crdop_bin)" || { if [[ -f "${ROOT}/scripts/build.sh" ]]; then echo "Building CRDOP..." bash "${ROOT}/scripts/build.sh" crdop_bin="$(resolve_crdop_bin)" || true fi } if [[ -z "${crdop_bin}" ]]; then echo "crdopc not found in PATH or build/bin" >&2 exit 1 fi local launcher="" launcher="$(stack_script "crdop/scripts/crdopc" 2>/dev/null || true)" if [[ -z "${launcher}" ]]; then echo "Starting crdopc (HyBBX crdop plugin attaches to TCP :8515)" exec env CRDOP_BIN="${crdop_bin}" crdopc fi echo "Starting crdopc (HyBBX crdop plugin attaches to TCP :8515)" CRDOP_BIN="${crdop_bin}" exec "${launcher}" ;; *) echo "Unknown soft-modem device: $DEVICE" >&2 exit 1 ;; esac ;; "") echo "Specify --hardware modems|soft-modems" >&2 exit 1 ;; *) echo "Unknown hardware: $HARDWARE" >&2 exit 1 ;; esac } cmd_stop() { parse_opts "$@" if [[ "$HARDWARE" == "modems" || -z "$HARDWARE" ]]; then case "$DEVICE" in baycom-kiss) echo "baycom-kiss: no baycom-pr-ctl stop (USB serial owned by max25d)" ;; *) local baycom_ctl="" baycom_ctl="$(resolve_baycom_ctl 2>/dev/null || true)" if [[ -n "${baycom_ctl}" ]]; then local ini="" ini="$(resolve_baycom_ini_path "${DEVICE:-baycom-ser12}" "${BAYCOM_INI}" 2>/dev/null || true)" if [[ -n "${ini}" ]]; then sudo "${baycom_ctl}" -c "${ini}" stop 2>/dev/null || true else sudo "${baycom_ctl}" stop 2>/dev/null || true fi fi ;; esac fi echo "TNC serial: release port (stop other userspace serial owner; fuser on device)" } cmd_status() { parse_opts "$@" echo "MAX25 mode: $MODE" if [[ -f "${ROOT}/scripts/discover-plugins.sh" ]]; then bash "${ROOT}/scripts/discover-plugins.sh" --type device || true else echo "discover: source checkout required (set MAX25_ROOT)" fi local baycom_ctl="" baycom_ctl="$(resolve_baycom_ctl 2>/dev/null || true)" if [[ -n "${baycom_ctl}" && "${DEVICE:-}" != "baycom-kiss" ]]; then local ini="" ini="$(resolve_baycom_ini_path "${DEVICE:-baycom-ser12}" "${BAYCOM_INI}" 2>/dev/null || true)" if [[ -n "${ini}" ]]; then sudo "${baycom_ctl}" -c "${ini}" status 2>/dev/null || echo "baycom-pr: not running or no root" else sudo "${baycom_ctl}" status 2>/dev/null || echo "baycom-pr: not running or no root" fi fi } cmd_test() { if [[ ! -d "${ROOT}/build" ]]; then if [[ ! -f "${ROOT}/scripts/build.sh" ]]; then echo "test: source checkout with build/ required (set MAX25_ROOT)" >&2 exit 1 fi bash "${ROOT}/scripts/build.sh" fi cmake --build "${ROOT}/build" --target max25_test } case "$ACTION" in help|-h|--help) usage ;; discover) cmd_discover "$@" ;; build) cmd_build ;; start) cmd_start "$@" ;; stop) cmd_stop "$@" ;; status) cmd_status "$@" ;; test) cmd_test ;; *) echo "Unknown command: $ACTION" >&2; usage; exit 1 ;; esac