summaryrefslogtreecommitdiff
path: root/scripts/max25-ctl
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/max25-ctl')
-rwxr-xr-xscripts/max25-ctl413
1 files changed, 413 insertions, 0 deletions
diff --git a/scripts/max25-ctl b/scripts/max25-ctl
new file mode 100755
index 0000000..f17888b
--- /dev/null
+++ b/scripts/max25-ctl
@@ -0,0 +1,413 @@
+#!/usr/bin/env bash
+# MainAX25-Stack (MAX25-Stack) — unified control entry point for Packet Radio / AX.25 stacks.
+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 <<EOF
+MainAX25-Stack (MAX25-Stack) control — Packet Radio / AX.25 (HyBBX-compatible)
+
+Usage:
+ max25-ctl <command> [options]
+
+Commands:
+ help This help
+ discover List plugins
+ build Build all stacks
+ start Start hardware stack
+ stop Stop hardware stack
+ status Show stack 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):
+ --mode MODE standalone | service | hybbx-host (default: standalone)
+ --hardware HW tncs | modems | soft-modems
+ --device DEV tnc2c | pktnc2 | max25e0 | baycom-kiss | soft-crdop
+ (legacy aliases baycom-ser12/par96 redirect to bcpr docs)
+ --baycom-ini PATH legacy kernel baycom-pr.ini (removed — use bcpr.ini / max25e0)
+ --baycom-profile NAME single (default) | dual — legacy only (kernel path removed)
+
+Examples:
+ max25-ctl discover
+ max25-ctl build
+ max25-ctl start --hardware tncs --device tnc2c
+ 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
+
+Unified workflow (all devices): docs/PLUGINS-DEVICE-MODEL.md
+ SET DEVICE <id> → 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 "Kernel BayCom (baycom-pr / baycom_ser_fdx) removed from this tree." >&2
+ echo "Use BayCom/based via max25-bcpr: docs/BAYCOM.md · stacks/max25-bcpr/README.md" >&2
+ echo " [features] max25_bcpr=yes · max25e0 = max25-bcpr:bc0 · max25-bcpr-ctl / max25d auto_start" >&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 "max25-bcpr / max25e0: start via max25d [features] max25_bcpr=yes (auto_start) or max25-bcpr-ctl" >&2
+ echo "See docs/BAYCOM.md · 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 tncs|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 HyBBX or other userspace serial owner; fuser on device)"
+}
+
+cmd_status() {
+ parse_opts "$@"
+ echo "MainAX25 mode: $MODE"
+ if [[ -f "${ROOT}/scripts/discover-plugins.sh" ]]; then
+ bash "${ROOT}/scripts/discover-plugins.sh" --type device
+ 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
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com