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 --- scripts/max25-ctl | 408 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100755 scripts/max25-ctl (limited to 'scripts/max25-ctl') diff --git a/scripts/max25-ctl b/scripts/max25-ctl new file mode 100755 index 0000000..473b4a2 --- /dev/null +++ b/scripts/max25-ctl @@ -0,0 +1,408 @@ +#!/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 -- cgit v1.3.1