diff options
| author | info@mode42.com <info@mode42.com> | 2026-08-07 18:25:13 +0000 |
|---|---|---|
| committer | info@mode42.com <info@mode42.com> | 2026-08-07 18:25:13 +0000 |
| commit | 04d965d67a7264a1c7c211494aebda1953df7603 (patch) | |
| tree | 0ebd700a6e219f84a26a656f4bee8778bc75da7b /scripts | |
Initial push
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/build-all.sh | 54 | ||||
| -rwxr-xr-x | scripts/build-amiga-terminal.sh | 17 | ||||
| -rwxr-xr-x | scripts/build-ax25-deps.sh | 146 | ||||
| -rwxr-xr-x | scripts/build-freebsd.sh | 31 | ||||
| -rwxr-xr-x | scripts/build.sh | 11 | ||||
| -rwxr-xr-x | scripts/clean.sh | 9 | ||||
| -rwxr-xr-x | scripts/discover-plugins.sh | 97 | ||||
| -rwxr-xr-x | scripts/install-freebsd.sh | 115 | ||||
| -rwxr-xr-x | scripts/install-max25.sh | 116 | ||||
| -rwxr-xr-x | scripts/max25-ctl | 413 | ||||
| -rwxr-xr-x | scripts/max25-terminal | 3 | ||||
| -rwxr-xr-x | scripts/max25d-session.sh | 160 | ||||
| -rwxr-xr-x | scripts/release-check.sh | 343 | ||||
| -rwxr-xr-x | scripts/run-max25-terminal.sh | 62 | ||||
| -rwxr-xr-x | scripts/run-max25d.sh | 99 | ||||
| -rwxr-xr-x | scripts/terminology-check.sh | 41 | ||||
| -rwxr-xr-x | scripts/test.sh | 12 | ||||
| -rwxr-xr-x | scripts/tx-rx-test.sh | 350 |
18 files changed, 2079 insertions, 0 deletions
diff --git a/scripts/build-all.sh b/scripts/build-all.sh new file mode 100755 index 0000000..571b466 --- /dev/null +++ b/scripts/build-all.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# MainAX25-Stack — build all merged stacks (max25-bcpr default ON). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" +BUILD_DIR="${BUILD_DIR:-build}" +BUILD_TYPE="${CMAKE_BUILD_TYPE:-Release}" +DO_INSTALL=0 +PREFIX="${PREFIX:-/usr/local}" + +usage() { + cat <<USAGE +Usage: $0 [--install] [--prefix DIR] [--build-dir DIR] + + Default: cmake configure + build (TERMINAL/DAEMON/BCPR ON). + --install also cmake --install to PREFIX +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --install) DO_INSTALL=1; shift ;; + --prefix) PREFIX="$2"; shift 2 ;; + --build-dir) BUILD_DIR="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown: $1" >&2; usage; exit 2 ;; + esac +done + +echo "== MainAX25-Stack build-all (${BUILD_DIR}, ${BUILD_TYPE}) ==" +cmake -B "${BUILD_DIR}" \ + -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ + -DMAX25_BUILD_MAX25_BCPR="${MAX25_BUILD_MAX25_BCPR:-ON}" \ + -DMAX25_BUILD_TERMINAL=ON \ + -DMAX25_BUILD_DAEMON=ON \ + -DMAX25_BUNDLE_AX25=OFF +cmake --build "${BUILD_DIR}" -j"$(nproc 2>/dev/null || echo 2)" + +echo "== plugin discovery ==" +bash scripts/discover-plugins.sh || true + +if [[ "$DO_INSTALL" -eq 1 ]]; then + echo "== install → ${PREFIX} ==" + if [[ "$(id -u)" -eq 0 ]]; then + cmake --install "${BUILD_DIR}" --prefix "${PREFIX}" + else + sudo cmake --install "${BUILD_DIR}" --prefix "${PREFIX}" + fi + echo "Launchers: ${PREFIX}/bin/run-max25d ${PREFIX}/bin/run-max25-terminal" + echo "Tests: ${PREFIX}/sbin/max25-tx-rx-test ${PREFIX}/sbin/max25-bcpr-rxtx-smoke.sh" +fi + +echo "== done ==" diff --git a/scripts/build-amiga-terminal.sh b/scripts/build-amiga-terminal.sh new file mode 100755 index 0000000..f6336b5 --- /dev/null +++ b/scripts/build-amiga-terminal.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh +# Cross-build max25-terminal for AmigaOS 3.9+ (bebbo-gcc / clib2). +set -eu + +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +SDK="${AMIGA_SDK_PATH:-/opt/amiga}" + +if [ ! -x "$SDK/bin/m68k-amigaos-gcc" ]; then + echo "Amiga SDK not found at $SDK (set AMIGA_SDK_PATH)" >&2 + exit 1 +fi + +make -C "$ROOT/stacks/terminal/amiga" AMIGA_SDK="$SDK" all +make -C "$ROOT/stacks/terminal/amiga" AMIGA_SDK="$SDK" test + +echo "AmigaOS client:" +echo " $ROOT/stacks/terminal/amiga/max25-terminal" diff --git a/scripts/build-ax25-deps.sh b/scripts/build-ax25-deps.sh new file mode 100755 index 0000000..d909cdc --- /dev/null +++ b/scripts/build-ax25-deps.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# DEPRECATED — manual build of vendored libax25 / ax25-tools / ax25-apps (Linux). +# +# MAX25 default build does NOT invoke this script. Tarballs in third_party/ax25/ +# are study reference only; max25d uses native ax25_codec.py. +# +# Use when you need host listen/call/kissattach without distro packages: +# scripts/build-ax25-deps.sh --prefix /opt/ax25 --need-apps --need-tools +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VENDOR="${ROOT}/third_party/ax25" +PATCHES="${VENDOR}/patches" + +LIBAX25_VER="0.0.12-rc5" +TOOLS_VER="0.0.10-rc5" +APPS_VER="0.0.8-rc5" + +usage() { + cat <<EOF +Usage: $0 --prefix DIR [options] + +Options: + --prefix DIR Install prefix (required) + --build-dir DIR Extract/build tree (default: <prefix>/../build) + --need-libax25 Build libax25 even if present on host + --need-tools Build ax25-tools + --need-apps Build ax25-apps + -h, --help This help + +Environment: + MAKE, CC, CXX Passed to upstream make/configure +EOF +} + +PREFIX="" +BUILD_DIR="" +NEED_LIBAX25=0 +NEED_TOOLS=0 +NEED_APPS=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --prefix) PREFIX="$2"; shift 2 ;; + --build-dir) BUILD_DIR="$2"; shift 2 ;; + --need-libax25) NEED_LIBAX25=1; shift ;; + --need-tools) NEED_TOOLS=1; shift ;; + --need-apps) NEED_APPS=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +[[ -n "$PREFIX" ]] || { echo "error: --prefix required" >&2; exit 1; } +PREFIX="$(cd "$(dirname "$PREFIX")" && pwd)/$(basename "$PREFIX")" +BUILD_DIR="${BUILD_DIR:-$(dirname "$PREFIX")/build}" +BUILD_DIR="$(mkdir -p "$BUILD_DIR" && cd "$BUILD_DIR" && pwd)" +PREFIX="$(mkdir -p "$PREFIX" && cd "$PREFIX" && pwd)" + +verify_tarball() { + local file="$1" + [[ -f "$file" ]] || { echo "error: missing tarball $file" >&2; exit 1; } + if [[ -f "${VENDOR}/SHA256SUMS" ]]; then + local base + base="$(basename "$file")" + (cd "$VENDOR" && sha256sum -c SHA256SUMS 2>/dev/null | grep -q "^${base}: OK") \ + || echo "WARN: SHA256 mismatch for ${base} — continuing" >&2 + fi +} + +extract() { + local tarball="$1" dir="$2" + verify_tarball "$tarball" + rm -rf "$dir" + tar xzf "$tarball" -C "$BUILD_DIR" +} + +apply_patches() { + local srcdir="$1" + shift + local patch failed=0 + for patch in "$@"; do + [[ -f "$patch" ]] || continue + echo "-- patch $(basename "$patch")" + if ! patch -d "$srcdir" -p1 -N -s -f < "$patch"; then + echo "error: patch failed: $(basename "$patch")" >&2 + failed=1 + fi + done + [[ "$failed" -eq 0 ]] +} + +ax25_link_flags() { + if [[ "$NEED_LIBAX25" -eq 1 ]]; then + export CPPFLAGS="-I${PREFIX}/include ${CPPFLAGS:-}" + export LDFLAGS="-L${PREFIX}/lib -Wl,-rpath,${PREFIX}/lib ${LDFLAGS:-}" + elif [[ -d /usr/include/netax25 ]]; then + export CPPFLAGS="-I/usr/include ${CPPFLAGS:-}" + export LDFLAGS="${LDFLAGS:-}" + fi +} + +build_libax25() { + local dir="${BUILD_DIR}/libax25-${LIBAX25_VER}" + echo "== libax25 ${LIBAX25_VER} -> ${PREFIX}" + extract "${VENDOR}/libax25-${LIBAX25_VER}.tar.gz" "$dir" + ( + cd "$dir" + ./configure --prefix="$PREFIX" + "${MAKE:-make}" -j"$(nproc 2>/dev/null || echo 2)" + "${MAKE:-make}" install + ) +} + +build_ax25_tools() { + local dir="${BUILD_DIR}/ax25-tools-${TOOLS_VER}" + echo "== ax25-tools ${TOOLS_VER} -> ${PREFIX}" + extract "${VENDOR}/ax25-tools-${TOOLS_VER}.tar.gz" "$dir" + ax25_link_flags + ( + cd "$dir" + ./configure --prefix="$PREFIX" + "${MAKE:-make}" -j"$(nproc 2>/dev/null || echo 2)" + "${MAKE:-make}" install + ) +} + +build_ax25_apps() { + local dir="${BUILD_DIR}/ax25-apps-${APPS_VER}" + echo "== ax25-apps ${APPS_VER} -> ${PREFIX}" + extract "${VENDOR}/ax25-apps-${APPS_VER}.tar.gz" "$dir" + apply_patches "$dir" "${PATCHES}/ax25-apps-0.0.8-termios.patch" + ax25_link_flags + ( + cd "$dir" + ./configure --prefix="$PREFIX" + "${MAKE:-make}" -j"$(nproc 2>/dev/null || echo 2)" + "${MAKE:-make}" install + ) +} + +[[ "$NEED_LIBAX25" -eq 1 ]] && build_libax25 +[[ "$NEED_TOOLS" -eq 1 ]] && build_ax25_tools +[[ "$NEED_APPS" -eq 1 ]] && build_ax25_apps + +echo "== AX.25 deps ready under ${PREFIX} ==" diff --git a/scripts/build-freebsd.sh b/scripts/build-freebsd.sh new file mode 100755 index 0000000..a226dad --- /dev/null +++ b/scripts/build-freebsd.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# MAX25-Stack — FreeBSD full build (max25d, bcpr, terminal, CRDOP/OSS, web) +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +BUILD_TYPE="${CMAKE_BUILD_TYPE:-Release}" +BUILD_DIR="${BUILD_DIR:-build}" + +if [[ "$(uname -s)" != "FreeBSD" ]]; then + echo "build-freebsd.sh: FreeBSD only" >&2 + exit 1 +fi + +echo "== MAX25 FreeBSD build → ${BUILD_DIR} (${BUILD_TYPE}) ==" + +cmake -B "${BUILD_DIR}" \ + -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ + -DMAX25_BUILD_TNCS=OFF \ + -DMAX25_BUILD_MAX25_BCPR=ON \ + -DMAX25_BUILD_CRDOP=ON \ + -DMAX25_BUILD_DAEMON=ON \ + -DMAX25_BUILD_TERMINAL=ON \ + -DMAX25_BUILD_WEB=ON \ + -DMAX25_BUNDLE_AX25=OFF \ + "$@" + +cmake --build "${BUILD_DIR}" -j"$(sysctl -n hw.ncpu 2>/dev/null || echo 2)" + +echo "== build done: ${BUILD_DIR}/bin ==" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..87c957d --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# MainAX25-Stack (MAX25-Stack) — configure and build via CMake. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +BUILD_TYPE="${CMAKE_BUILD_TYPE:-Release}" + +cmake -B build -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" +cmake --build build -j"$(nproc 2>/dev/null || echo 2)" diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100755 index 0000000..9447da2 --- /dev/null +++ b/scripts/clean.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# MainAX25-Stack (MAX25-Stack) — remove CMake build trees. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +rm -rf build stacks/crdop/build +echo "Removed build/ and stacks/crdop/build/" diff --git a/scripts/discover-plugins.sh b/scripts/discover-plugins.sh new file mode 100755 index 0000000..a8845b6 --- /dev/null +++ b/scripts/discover-plugins.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# MainAX25-Stack (MAX25-Stack) — list registered plugins from manifest.yaml + on-disk plugin.yaml files. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MANIFEST="${ROOT}/plugins/manifest.yaml" +JSON=0 + +usage() { + cat <<'EOF' +Usage: discover-plugins.sh [--json] [--type TYPE] [--id ID] + + --json Machine-readable output (one JSON object per line) + --type TYPE Filter: hardware | device + --id ID Show single plugin by id +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --json) JSON=1; shift ;; + --type) TYPE_FILTER="${2:-}"; shift 2 ;; + --id) ID_FILTER="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +if [[ ! -f "$MANIFEST" ]]; then + echo "manifest not found: $MANIFEST" >&2 + exit 1 +fi + +emit_plugin() { + local type="$1" id="$2" path="$3" extra="${4:-}" + local full="${ROOT}/plugins/${path}" + local yaml="${full}/plugin.yaml" + local label="" status="" hybbx="" + + if [[ -f "$yaml" ]]; then + label=$(grep -E '^label:' "$yaml" 2>/dev/null | head -1 | sed 's/^label:[[:space:]]*//' || true) + status=$(grep -E '^status:' "$yaml" 2>/dev/null | head -1 | sed 's/^status:[[:space:]]*//' || true) + hybbx=$(grep -E '^(hybbx_plugin|plugin):' "$yaml" 2>/dev/null | head -1 | sed 's/^[^:]*:[[:space:]]*//' || true) + fi + + if [[ -n "${TYPE_FILTER:-}" && "$type" != "$TYPE_FILTER" ]]; then return; fi + if [[ -n "${ID_FILTER:-}" && "$id" != "$ID_FILTER" ]]; then return; fi + + if [[ "$JSON" -eq 1 ]]; then + printf '{"type":"%s","id":"%s","path":"plugins/%s","label":"%s","status":"%s","hybbx":"%s"%s}\n' \ + "$type" "$id" "$path" "$label" "$status" "$hybbx" "$extra" + else + printf '%-14s %-18s %-30s %s\n' "$type" "$id" "plugins/$path" "${label:-—}" + fi +} + +if [[ "$JSON" -eq 0 && -z "${ID_FILTER:-}" ]]; then + printf '%-14s %-18s %-30s %s\n' "TYPE" "ID" "PATH" "LABEL" + printf '%s\n' "--------------------------------------------------------------------------------" +fi + +# Parse manifest sections (simple line-based — no PyYAML dependency) +section="" +while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%%#*}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z "$line" ]] && continue + + case "$line" in + betriebsform:) section=""; continue ;; + hardware:) section=hardware; continue ;; + devices:) section=device; continue ;; + schema_version:*|stack:*|description:*) continue ;; + esac + + [[ -z "$section" ]] && continue + + if [[ "$line" =~ ^[[:space:]]*-[[:space:]]+id:[[:space:]]*(.+)$ ]]; then + cur_id="${BASH_REMATCH[1]}" + cur_path="" + continue + fi + if [[ -n "${cur_id:-}" && "$line" =~ ^[[:space:]]+path:[[:space:]]*(.+)$ ]]; then + cur_path="${BASH_REMATCH[1]}" + emit_plugin "$section" "$cur_id" "$cur_path" + cur_id="" + fi +done < "$MANIFEST" + +# Also scan for plugin.yaml not in manifest (future auto-gen) +while IFS= read -r yaml; do + dir="$(dirname "$yaml")" + rel="${dir#${ROOT}/plugins/}" + id="$(grep -E '^id:' "$yaml" | head -1 | sed 's/^id:[[:space:]]*//' || basename "$dir")" + type="$(grep -E '^type:' "$yaml" | head -1 | sed 's/^type:[[:space:]]*//' || echo unknown)" + # Skip if already emitted via manifest for same id+type would duplicate — acceptable for scaffold +done < <(find "${ROOT}/plugins" -name plugin.yaml -not -path '*/betriebsform/*' 2>/dev/null | sort) diff --git a/scripts/install-freebsd.sh b/scripts/install-freebsd.sh new file mode 100755 index 0000000..01ff533 --- /dev/null +++ b/scripts/install-freebsd.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# MAX25-Stack — FreeBSD full build + install (daemon, bcpr, terminal, CRDOP, web) +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PREFIX="${PREFIX:-/usr/local}" +ETC="${ETC:-${PREFIX}/etc/max25}" +INSTALL_DEPS=0 +BUILD_ONLY=0 +BUILD_DIR="${BUILD_DIR:-build}" + +usage() { + cat <<EOF +MAX25-Stack install — FreeBSD + +Usage: $0 [options] + +Options: + --deps Install pkg build/runtime dependencies + --build-only Build only, do not install + --prefix DIR Install prefix (default: /usr/local) + --build-dir D CMake build dir (default: build) + -h, --help This help + +Full build (default): max25d · max25-bcpr · max25-terminal · CRDOP/OSS · web proxy/UI scaffold + +After install: + sudo mkdir -p /var/run/max25 ${ETC} + sudo cp ${PREFIX}/share/max25/max25d.freebsd.bcpr.ini.example ${ETC}/max25d.ini + sudo cp ${PREFIX}/share/max25-bcpr/max25-bcpr.ini.example ${ETC}/max25-bcpr.ini + sudo max25-bcpr-ctl start --ini ${ETC}/max25-bcpr.ini + sudo ${ROOT}/scripts/run-max25d.sh ${ETC}/max25d.ini + +See docs/FREEBSD-AX25.md · docs/WEBSOCKET.md · docs/BAYCOM.md +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --deps) INSTALL_DEPS=1; shift ;; + --build-only) BUILD_ONLY=1; shift ;; + --prefix) PREFIX="$2"; shift 2 ;; + --build-dir) BUILD_DIR="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +if [[ "$(uname -s)" != "FreeBSD" ]]; then + echo "install-freebsd.sh: FreeBSD only" >&2 + exit 1 +fi + +echo "== MAX25 install: $(uname -s) $(uname -m) → ${PREFIX} ==" + +if [[ "$INSTALL_DEPS" -eq 1 ]]; then + pkgs=(cmake python3 ncurses sox git gmake pkgconf) + echo "Installing packages: ${pkgs[*]}" + pkg install -y "${pkgs[@]}" +fi + +BUILD_DIR="${BUILD_DIR}" "${ROOT}/scripts/build-freebsd.sh" + +if [[ "$BUILD_ONLY" -eq 1 ]]; then + echo "== build done (no install) ==" + exit 0 +fi + +echo "-- install to ${PREFIX}" +if [[ "$(id -u)" -eq 0 ]]; then + cmake --install "${ROOT}/${BUILD_DIR}" --prefix "${PREFIX}" +else + sudo cmake --install "${ROOT}/${BUILD_DIR}" --prefix "${PREFIX}" +fi + +_install_ini() { + local src="$1" dst="$2" + if [[ ! -f "$dst" ]] && [[ -f "$src" ]]; then + mkdir -p "$(dirname "$dst")" + if [[ "$(id -u)" -eq 0 ]]; then + cp "$src" "$dst" + else + sudo cp "$src" "$dst" + fi + echo " installed $dst (from example)" + fi +} + +if [[ "$(id -u)" -eq 0 ]] || sudo -n true 2>/dev/null; then + _install_ini "${ROOT}/share/max25-bcpr/max25-bcpr.freebsd.ini.example" "${ETC}/max25-bcpr.ini" + _install_ini "${ROOT}/share/max25/max25d.freebsd.bcpr.ini.example" "${ETC}/max25d.ini" + _install_ini "${ROOT}/stacks/web/share/web-proxy.ini.example" "${ETC}/web-proxy.ini" + _install_ini "${ROOT}/stacks/web/share/web-terminal.ini.example" "${ETC}/web-terminal.ini" +fi + +echo "" +echo "== MAX25 installed to ${PREFIX} ==" +echo " max25d, max25-terminal, max25-ws-proxy → ${PREFIX}/bin" +echo " max25-bcpr-ctl, max25-bcprd-init → ${PREFIX}/sbin" +echo " examples → ${PREFIX}/share/max25/ · web UI → ${PREFIX}/share/max25/reverse-proxy/" +echo "" +echo "Next steps (PC-COM / ExSys cuau2 — edit INI before live RF):" +echo " pw groupmod dialer -m \$USER" +echo " sudo mkdir -p /var/run/max25 ${ETC}" +echo " sudo chown \$USER:dialer /var/run/max25" +echo " # ${ETC}/max25-bcpr.ini: serial=/dev/cuau2 iobase=0xd090 irq=36" +echo " # ${ETC}/max25d.ini: [daemon] user=… group=dialer" +echo " sudo max25-bcpr-ctl start --ini ${ETC}/max25-bcpr.ini" +echo " sudo ${ROOT}/scripts/run-max25d.sh ${ETC}/max25d.ini" +echo " max25-terminal -H 127.0.0.1 -p 7325 -d max25e0" +echo " MAX25_WEB_TERMINAL_INI=${ETC}/web-terminal.ini max25-ws-proxy -c ${ETC}/web-proxy.ini" +echo "" +echo "Optional: tmodem/c1224 → ${PREFIX}/share/max25/max25d.freebsd.tmodem.ini.example" +echo "Optional: CRDOP/OSS → ${PREFIX}/share/crdop/crdop.freebsd.ini.example" +echo "" diff --git a/scripts/install-max25.sh b/scripts/install-max25.sh new file mode 100755 index 0000000..c21f673 --- /dev/null +++ b/scripts/install-max25.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# MainAX25-Stack — build and install on Linux (daemon, bcpr, terminal, tests). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PREFIX="${PREFIX:-/usr/local}" +INSTALL_DEPS=0 +BUILD_ONLY=0 +BUILD_DIR="${BUILD_DIR:-build}" + +usage() { + cat <<EOF +MainAX25-Stack install — Linux + +Usage: $0 [options] + +Options: + --deps Install apt build dependencies + --build-only Build only, do not install + --prefix DIR Install prefix (default: /usr/local) + --build-dir D CMake build dir (default: build) + -h, --help This help + +After install: + run-max25d + max25-tx-rx-test --device modem --live + run-max25-terminal -U /run/max25/modem.sock + +See docs/LINUX-HOST-SETUP.md · docs/DEV/TNC-MODEM-DEV.md +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --deps) INSTALL_DEPS=1; shift ;; + --build-only) BUILD_ONLY=1; shift ;; + --prefix) PREFIX="$2"; shift 2 ;; + --build-dir) BUILD_DIR="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +if [[ "$(uname -s)" != "Linux" ]]; then + echo "install-max25.sh: Linux only" >&2 + exit 1 +fi + +echo "== MAX25 install: $(uname -s) $(uname -m) → ${PREFIX} ==" + +if [[ "$INSTALL_DEPS" -eq 1 ]]; then + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update + pkgs=(build-essential make cmake pkg-config git python3 libncurses-dev libasound2-dev) + sudo apt-get install -y "${pkgs[@]}" || true + fi +fi + +cd "$ROOT" +echo "-- cmake (BCPR+TERMINAL+DAEMON ON — max25-bcpr default)" +cmake -B "${BUILD_DIR}" \ + -DCMAKE_BUILD_TYPE=Release \ + -DMAX25_BUILD_MAX25_BCPR=ON \ + -DMAX25_BUILD_TERMINAL=ON \ + -DMAX25_BUILD_DAEMON=ON \ + -DMAX25_BUNDLE_AX25=OFF +cmake --build "${BUILD_DIR}" -j"$(nproc 2>/dev/null || echo 2)" + +if [[ "$BUILD_ONLY" -eq 1 ]]; then + echo "== build done (no install) ==" + exit 0 +fi + +echo "-- install to ${PREFIX}" +if [[ "$(id -u)" -eq 0 ]]; then + cmake --install "${BUILD_DIR}" --prefix "$PREFIX" +else + sudo cmake --install "${BUILD_DIR}" --prefix "$PREFIX" +fi + +echo "" +echo "== MAX25 installed to ${PREFIX} ==" +echo " max25d, kiss_bridge.py, max25-terminal, max25-ctl -> ${PREFIX}/bin" +echo " max25-bcpr-ctl -> ${PREFIX}/sbin (when MAX25_BUILD_MAX25_BCPR=ON)" +echo " examples -> ${PREFIX}/share/max25/" +echo "" + +echo "" +echo "Next steps (TNC host):" +echo " sudo usermod -aG dialout \$USER # serial access" +echo " sudo cp share/max25/max25d.ini.host.example /etc/max25/max25d.ini" +echo " sudo max25d -c /etc/max25/max25d.ini" +echo " max25-terminal -U /run/max25/modem.sock" +echo "" +echo "BayCom/based (max25-bcpr): built by default (MAX25_BUILD_MAX25_BCPR=ON)." +echo " See docs/BAYCOM.md · share/max25-bcpr/max25-bcpr.ini.example" +echo "" +ETC="${ETC:-/etc/max25}" +if [[ "$(id -u)" -eq 0 ]] || sudo -n true 2>/dev/null; then + _install_ini() { + local src="$1" dst="$2" + if [[ ! -f "$dst" ]] && [[ -f "$src" ]]; then + mkdir -p "$ETC" + if [[ "$(id -u)" -eq 0 ]]; then + cp "$src" "$dst" + else + sudo cp "$src" "$dst" + fi + echo " installed $dst (from example)" + fi + } + _install_ini "$ROOT/share/max25-bcpr/max25-bcpr.ini.example" "$ETC/max25-bcpr.ini" + if [[ -f "$ETC/bcpr.ini" ]] && [[ ! -L "$ETC/max25-bcpr.ini" ]]; then + echo " note: legacy $ETC/bcpr.ini kept — paths /tmp/bcpr auto-map to /tmp/max25-bcpr at runtime" + fi +fi 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 diff --git a/scripts/max25-terminal b/scripts/max25-terminal new file mode 100755 index 0000000..71a689e --- /dev/null +++ b/scripts/max25-terminal @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +# Alias → canonical launcher (stale stacks/terminal ELF avoided). +exec "$(cd "$(dirname "$0")" && pwd)/run-max25-terminal.sh" "$@" diff --git a/scripts/max25d-session.sh b/scripts/max25d-session.sh new file mode 100755 index 0000000..701d23e --- /dev/null +++ b/scripts/max25d-session.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# max25d-session — run max25d inside tmux or GNU screen (operator attach/detach). +# +# max25d-session start [--tmux|--screen] [-c /etc/max25/max25d.ini] +# max25d-session attach [--tmux|--screen] +# max25d-session status +# max25d-session stop +# +# Environment overrides: +# MAX25D_SESSION session name (default: max25d) +# MAX25D_SESSION_BACKEND tmux | screen (default: tmux) +# MAX25D_INI config path +set -euo pipefail + +SESSION_NAME="${MAX25D_SESSION:-max25d}" +BACKEND="${MAX25D_SESSION_BACKEND:-tmux}" +CONFIG="${MAX25D_INI:-/etc/max25/max25d.ini}" +MAX25D_BIN="${MAX25D_BIN:-max25d}" + +usage() { + cat <<'EOF' +max25d-session — persistent max25d in tmux or screen + +Commands: + start [--tmux|--screen] [-c ini] start detached session + attach [--tmux|--screen] attach to running session + status show session + max25d hint + stop kill session + +Examples: + max25d-session start -c /etc/max25/max25d.ini + max25d-session attach + MAX25D_SESSION_BACKEND=screen max25d-session start + +EOF +} + +have_tmux() { command -v tmux >/dev/null 2>&1; } +have_screen() { command -v screen >/dev/null 2>&1; } + +pick_backend() { + case "${1:-$BACKEND}" in + tmux) + if ! have_tmux; then + echo "max25d-session: tmux not installed" >&2 + exit 1 + fi + echo tmux + ;; + screen) + if ! have_screen; then + echo "max25d-session: screen not installed" >&2 + exit 1 + fi + echo screen + ;; + *) + echo "max25d-session: unknown backend '$1' (use tmux or screen)" >&2 + exit 1 + ;; + esac +} + +session_running() { + local b="$1" + case "$b" in + tmux) tmux has-session -t "$SESSION_NAME" 2>/dev/null ;; + screen) screen -ls | grep -q "[[:space:]]*[0-9]*\.${SESSION_NAME}[[:space:]]" ;; + esac +} + +cmd_start() { + local b + b="$(pick_backend "$BACKEND")" + if session_running "$b"; then + echo "max25d-session: session '$SESSION_NAME' already running ($b)" >&2 + echo " attach: max25d-session attach --$b" >&2 + exit 1 + fi + local cmd="${MAX25D_BIN} -c ${CONFIG}" + case "$b" in + tmux) + tmux new-session -d -s "$SESSION_NAME" \ + "${MAX25D_BIN} -c ${CONFIG}; echo; echo max25d exited \$?; read -r _" + ;; + screen) + screen -dmS "$SESSION_NAME" bash -lc "${cmd}" + ;; + esac + echo "max25d-session: started ($b) name=$SESSION_NAME config=$CONFIG" + echo "max25d-session: attach with: max25d-session attach --$b" +} + +cmd_attach() { + local b + b="$(pick_backend "$BACKEND")" + if ! session_running "$b"; then + echo "max25d-session: no session '$SESSION_NAME' ($b)" >&2 + exit 1 + fi + case "$b" in + tmux) exec tmux attach -t "$SESSION_NAME" ;; + screen) exec screen -r "$SESSION_NAME" ;; + esac +} + +cmd_status() { + local b + b="$(pick_backend "$BACKEND")" + if session_running "$b"; then + echo "max25d-session: running backend=$b name=$SESSION_NAME" + case "$b" in + tmux) tmux list-panes -t "$SESSION_NAME" -F 'pane #{pane_index}: #{pane_current_command}' 2>/dev/null || true ;; + screen) screen -ls | grep "${SESSION_NAME}" || true ;; + esac + else + echo "max25d-session: not running (backend=$b name=$SESSION_NAME)" + fi + if command -v pgrep >/dev/null 2>&1 && pgrep -x max25d >/dev/null 2>&1; then + echo "max25d-session: max25d process: $(pgrep -xa max25d || true)" + fi +} + +cmd_stop() { + local b + b="$(pick_backend "$BACKEND")" + if ! session_running "$b"; then + echo "max25d-session: not running" >&2 + exit 0 + fi + case "$b" in + tmux) tmux kill-session -t "$SESSION_NAME" ;; + screen) screen -S "$SESSION_NAME" -X quit ;; + esac + echo "max25d-session: stopped ($b) name=$SESSION_NAME" +} + +CMD="${1:-}" +shift || true +while [[ $# -gt 0 ]]; do + case "$1" in + --tmux) BACKEND=tmux; shift ;; + --screen) BACKEND=screen; shift ;; + -c|--config) + CONFIG="${2:?config path required}" + shift 2 + ;; + -h|--help) usage; exit 0 ;; + *) echo "max25d-session: unknown option: $1" >&2; usage >&2; exit 1 ;; + esac +done + +case "$CMD" in + start) cmd_start ;; + attach) cmd_attach ;; + status) cmd_status ;; + stop) cmd_stop ;; + ""|-h|--help) usage ;; + *) echo "max25d-session: unknown command: $CMD" >&2; usage >&2; exit 1 ;; +esac diff --git a/scripts/release-check.sh b/scripts/release-check.sh new file mode 100755 index 0000000..53f6c61 --- /dev/null +++ b/scripts/release-check.sh @@ -0,0 +1,343 @@ +#!/usr/bin/env bash +# MainAX25-Stack (MAX25-Stack-v1.5.0) — release gates (offline, no root). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +PASS=0 +FAIL=0 +WARN=0 + +ok() { echo "OK: $*"; PASS=$((PASS + 1)); } +fail() { echo "FAIL: $*" >&2; FAIL=$((FAIL + 1)); } +warn() { echo "WARN: $*"; WARN=$((WARN + 1)); } + +echo "======== MainAX25-Stack release-check ($(tr -d '[:space:]' < VERSION)) ========" + +# --- version & manifest --- +if [[ -f VERSION ]]; then + VER="$(tr -d '[:space:]' < VERSION)" + [[ -n "$VER" ]] && ok "VERSION=${VER}" || fail "VERSION file empty" +else + fail "VERSION file missing" +fi + +if [[ -f plugins/manifest.yaml ]]; then + ok "plugins/manifest.yaml" + grep -q 'default_betriebsform: standalone' plugins/manifest.yaml \ + && ok "default operating mode: standalone" \ + || fail "manifest missing default operating mode (default_betriebsform: standalone)" +else + fail "plugins/manifest.yaml missing" +fi + +# --- required docs --- +for doc in README.md CONTRIBUTING.md docs/README.md docs/ARCHITECTURE.md docs/DEVELOPMENT.md docs/HYBBX.md docs/PLATFORMS.md docs/V1.0.0-SCOPE.md docs/BAYCOM.md docs/CRDOP.md docs/MERGE-REPORT.md docs/MAX25-TERMINAL.md docs/MAX25-CLIENT.md docs/LINUX-HOST-SETUP.md docs/PACKET-RADIO.md stacks/daemon/README.md; do + [[ -f "$doc" ]] && ok "doc $doc" || fail "missing $doc" +done + +# --- scripts executable --- +for script in scripts/discover-plugins.sh scripts/build-all.sh scripts/build.sh scripts/test.sh scripts/clean.sh scripts/release-check.sh scripts/max25-ctl scripts/install-max25.sh scripts/tx-rx-test.sh scripts/terminology-check.sh; do + if [[ -x "$script" ]]; then + ok "executable $script" + else + fail "not executable: $script" + fi +done + +# --- public terminology gate (§0.16 / §0.19) --- +if [[ -x scripts/terminology-check.sh ]]; then + if scripts/terminology-check.sh; then + ok "terminology-check" + else + fail "terminology-check" + fi +else + fail "scripts/terminology-check.sh missing or not executable" +fi + +if [[ -x stacks/daemon/max25d ]] && [[ -f stacks/daemon/max25d.py ]]; then + ok "executable stacks/daemon/max25d launcher + max25d.py" +else + fail "stacks/daemon/max25d launcher or max25d.py missing" +fi + +if [[ -f stacks/daemon/kiss_bridge.py ]]; then + ok "stacks/daemon/kiss_bridge.py" +else + fail "missing stacks/daemon/kiss_bridge.py" +fi + +if [[ -f stacks/daemon/ax25_codec.py ]]; then + ok "stacks/daemon/ax25_codec.py (native AX.25 codec)" +else + fail "missing stacks/daemon/ax25_codec.py" +fi + +if [[ -f stacks/daemon/privilege_drop.py ]]; then + ok "stacks/daemon/privilege_drop.py" +else + fail "missing stacks/daemon/privilege_drop.py" +fi + +if python3 stacks/daemon/test_ax25_codec.py >/dev/null 2>&1; then + ok "ax25_codec unit tests" +else + fail "ax25_codec unit tests" +fi + +if [[ -f share/max25/max25d.ini.example ]]; then + ok "share/max25/max25d.ini.example" +else + fail "missing share/max25/max25d.ini.example" +fi + +if [[ -f share/max25/max25d.ini.host.example ]]; then + ok "share/max25/max25d.ini.host.example" +else + fail "missing share/max25/max25d.ini.host.example" +fi + +if [[ -f share/baycom/README.md ]] && { [[ -f stacks/max25-bcpr/share/max25-bcpr.ini.example ]] || [[ -f share/max25-bcpr/max25-bcpr.ini.example ]]; }; then + ok "BayCom/based bcpr example + share/baycom README" +else + fail "missing max25-bcpr.ini.example or share/baycom/README.md" +fi + +if [[ -f share/clients/index.yaml ]] && [[ -f share/clients/tnc2c.yaml ]]; then + ok "share/clients registry" +else + fail "missing share/clients/ (index.yaml or tnc2c.yaml)" +fi + +if [[ -f local/README.md ]]; then + ok "local/README.md (gitignored site overrides)" +else + fail "missing local/README.md" +fi + +if [[ -f include/max25/protocol.md ]]; then + ok "include/max25/protocol.md" +else + fail "missing include/max25/protocol.md" +fi + +if [[ -f include/max25/packet-radio.md ]]; then + ok "include/max25/packet-radio.md" +else + fail "missing include/max25/packet-radio.md" +fi + +# --- plugin count --- +YAML_COUNT="$(find plugins -name plugin.yaml | wc -l | tr -d ' ')" +if [[ "$YAML_COUNT" -ge 10 ]]; then + ok "plugin.yaml count: ${YAML_COUNT}" +else + fail "expected >=10 plugin.yaml, got ${YAML_COUNT}" +fi + +# --- v1 active devices in manifest --- +for dev in tnc2c soft-crdop; do + if grep -A6 "id: ${dev}" plugins/manifest.yaml | grep -q 'status: active'; then + ok "device ${dev} status: active" + else + fail "device ${dev} not active in manifest" + fi +done + +if grep -A6 "id: soft-modems" plugins/manifest.yaml | grep -q 'optional: true'; then + fail "hardware soft-modems should not be optional-only" +else + ok "hardware soft-modems is MAX25-SoftModem standard group" +fi + +# --- planned/scaffold devices stay out of v1 active set --- +for dev in pktnc2; do + if grep -A6 "id: ${dev}" plugins/manifest.yaml | grep -Eq 'status: (planned|scaffold)'; then + ok "device ${dev} correctly non-active" + else + warn "device ${dev} status unexpected (should be planned/scaffold)" + fi +done + +if grep -A6 "id: baycom-kiss" plugins/manifest.yaml | grep -q 'status: active'; then + ok "device baycom-kiss status: active (USB/async KISS)" +else + warn "device baycom-kiss status unexpected" +fi + +# kernel baycom-ser12 / baycom-par96 removed 2026-07-18 +if grep -q 'id: baycom-ser12' plugins/manifest.yaml || grep -q 'id: baycom-par96' plugins/manifest.yaml; then + fail "legacy kernel baycom-ser12/par96 still in manifest (use bcpr)" +else + ok "manifest free of kernel baycom-ser12/par96" +fi + +# --- HyBBX INI templates (stack-local) --- +for ini in stacks/tncs/hybbx-tnc2c.ini \ + stacks/tncs/hybbx-dual.ini \ + share/hybbx/tnc2c-host.ini.example \ + share/hybbx/crdop-host.ini.example; do + [[ -f "$ini" ]] && ok "ini $ini" || fail "missing $ini" +done +[[ -d stacks/max25-bcpr ]] && ok "stacks/max25-bcpr (BayCom/based)" || fail "missing stacks/max25-bcpr" + +# --- build artifacts (CMake) --- +BUILD_DIR="build" +if ! cmake -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE=Release \ + -DMAX25_BUNDLE_AX25=OFF >/dev/null 2>&1; then + BUILD_DIR="build-default" + cmake -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE=Release \ + -DMAX25_BUNDLE_AX25=OFF >/dev/null 2>&1 \ + || fail "cmake configure" +fi +if cmake --build "${BUILD_DIR}" -j"$(nproc 2>/dev/null || echo 2)" >/dev/null 2>&1; then + ok "cmake build (${BUILD_DIR}/bin/, CRDOP default ON)" +else + fail "cmake build" +fi +[[ -x "${BUILD_DIR}/bin/tnc2c-probe" ]] && ok "tnc2c-probe built" || fail "tnc2c-probe build" +if [[ -x "${BUILD_DIR}/bin/max25-bcprd" ]]; then + ok "max25-bcprd built" +else + ok "max25-bcprd not built (check cmake MAX25_BUILD_MAX25_BCPR)" +fi +[[ -x "${BUILD_DIR}/bin/max25-terminal" ]] && ok "max25-terminal built" || fail "max25-terminal build" +[[ -f stacks/crdop/share/crdop.ini.example ]] && ok "CRDOP scaffold (MAX25-SoftModem)" || fail "CRDOP scaffold missing" +[[ -f plugins/external/ardop/plugin.yaml ]] && ok "ARDOP-plugin metadata" || fail "missing plugins/external/ardop/plugin.yaml" +if [[ -x "${BUILD_DIR}/bin/crdopc" ]]; then + warn "vendor crdopc binary present (dev-only CRDOP_VENDOR_ARDOPCF build — not for release)" +else + ok "no vendor crdopc binary (native SoftModem default)" +fi +if [[ -x "${BUILD_DIR}/bin/audio-dummyd" ]]; then + ok "audio-dummyd binary (native SoftModem host)" +else + ok "audio-dummyd not in build dir (launcher uses source tree path)" +fi + +# --- ARDOP must not appear in default install tree --- +INSTALL_DIR="${BUILD_DIR}/install-release-check" +rm -rf "${INSTALL_DIR}" +if cmake --install "${BUILD_DIR}" --prefix "${INSTALL_DIR}" >/dev/null 2>&1; then + ok "cmake --install prefix" + if find "${INSTALL_DIR}" \( -path '*/vendor/ardopcf/*' -o -name 'ardopcf' -o -name 'crdopc' \) 2>/dev/null | grep -q .; then + fail "ARDOP vendor artifacts in install tree (forbidden in release)" + else + ok "install tree free of ARDOP vendor binaries/sources" + fi + if find "${INSTALL_DIR}" -type f -exec grep -l 'ardopcf' {} + 2>/dev/null | grep -q .; then + warn "install tree contains files mentioning ardopcf (review)" + else + ok "install tree has no ardopcf references" + fi +else + warn "cmake --install skipped (non-fatal for dev trees)" +fi + +# --- offline tests --- +if [[ -x "${BUILD_DIR}/bin/max25-bcprd" ]]; then + if "${BUILD_DIR}/bin/max25-bcprd" -c stacks/max25-bcpr/share/max25-bcpr.ini.example --dry-run --once >/dev/null 2>&1; then + ok "max25-bcpr dry-run once" + else + fail "max25-bcpr dry-run once" + fi +else + ok "max25-bcpr dry-run skipped (not built)" +fi + +if [[ -x "${BUILD_DIR}/bin/crdopc" ]] && CRDOP_BIN="${PWD}/${BUILD_DIR}/bin/crdopc" bash stacks/crdop/scripts/test-smoke.sh >/dev/null 2>&1; then + ok "crdop smoke (dev vendor build only)" +elif [[ -x "${BUILD_DIR}/bin/crdopc" ]]; then + fail "crdop smoke (vendor crdopc present but smoke failed)" +else + ok "crdop vendor smoke skipped — native SoftModem default" +fi + +# --- discovery --- +if bash scripts/discover-plugins.sh --json >/dev/null 2>&1; then + ok "discover-plugins --json" +else + fail "discover-plugins --json" +fi + +# --- max25d + terminal --- +chmod +x stacks/daemon/max25d stacks/daemon/max25d.py 2>/dev/null || true +[[ -x stacks/daemon/max25d ]] && [[ -f stacks/daemon/max25d.py ]] && ok "max25d ready" || fail "max25d" +if [[ -L "${BUILD_DIR}/bin/max25-client" ]] || [[ -x "${BUILD_DIR}/bin/max25-client" ]]; then + ok "max25-client symlink" +else + warn "max25-client symlink missing until cmake --install" +fi +if MAX25_TERMINAL="${PWD}/${BUILD_DIR}/bin/max25-terminal" \ + bash stacks/terminal/test-terminal.sh >/dev/null 2>&1; then + ok "max25-terminal TCP probe" +else + fail "max25-terminal TCP probe" +fi + +if python3 stacks/daemon/test_proto.py >/dev/null 2>&1; then + ok "max25d protocol smoke" +else + fail "max25d protocol smoke" +fi + +if python3 stacks/daemon/test_auth.py >/dev/null 2>&1; then + ok "max25d TCP auth smoke" +else + fail "max25d TCP auth smoke" +fi + +if bash scripts/tx-rx-test.sh >/dev/null 2>&1; then + ok "tx-rx-test L0 (TNC + BayCom/based)" +else + fail "tx-rx-test L0 (TNC + BayCom/based)" +fi + +if [[ -f docs/TX-RX-TEST.md ]]; then + ok "doc docs/TX-RX-TEST.md" +else + fail "missing docs/TX-RX-TEST.md" +fi + +if python3 stacks/tncs/test_tnc_serial_recovery.py >/dev/null 2>&1; then + ok "tnc serial recovery unit tests" +else + fail "tnc serial recovery unit tests" +fi + +if python3 stacks/daemon/test_serial_watch.py >/dev/null 2>&1; then + ok "max25d serial watch tests" +else + fail "max25d serial watch tests" +fi + +[[ -x stacks/tncs/tnc2c-host-reset.sh ]] && ok "tnc2c-host-reset.sh" || fail "tnc2c-host-reset.sh" + +# --- AmigaOS terminal (optional cross-build) --- +if [[ -x /opt/amiga/bin/m68k-amigaos-gcc ]]; then + if bash scripts/build-amiga-terminal.sh >/dev/null 2>&1; then + ok "max25-terminal AmigaOS cross-build" + else + fail "max25-terminal AmigaOS cross-build" + fi +else + warn "Amiga SDK not at /opt/amiga; skip Amiga terminal build" +fi + +# --- CI workflow --- +[[ -f .github/workflows/ci.yml ]] && ok "ci workflow" || fail "missing .github/workflows/ci.yml" + +# --- tncs probe (warn without hardware) --- +if "${BUILD_DIR}/bin/tnc2c-probe" >/dev/null 2>&1; then + ok "tnc2c-probe (serial found)" +else + warn "tnc2c-probe: no serial devices (OK for CI without hardware)" +fi + +echo "======== release-check: ${PASS} passed, ${FAIL} failed, ${WARN} warnings ========" +if [[ "$FAIL" -gt 0 ]]; then + exit 1 +fi +exit 0 diff --git a/scripts/run-max25-terminal.sh b/scripts/run-max25-terminal.sh new file mode 100755 index 0000000..adb0cca --- /dev/null +++ b/scripts/run-max25-terminal.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Canonical max25-terminal launcher — tree or PREFIX/bin. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +pick() { + local c + # Prefer installed binary beside this script + if [[ -x "${SCRIPT_DIR}/max25-terminal" && ! -d "${SCRIPT_DIR}/../stacks/terminal" ]]; then + c="${SCRIPT_DIR}/max25-terminal" + if "$c" -h 2>&1 | grep -q -- '-d, --device'; then + echo "$c" + return 0 + fi + fi + local TREE="" + if [[ -d "${SCRIPT_DIR}/../stacks/terminal" ]]; then + TREE="$(cd "${SCRIPT_DIR}/.." && pwd)" + fi + for c in \ + ${TREE:+"${TREE}/build/bin/max25-terminal"} \ + ${TREE:+"${TREE}/build-max25-bcpr/bin/max25-terminal"} \ + "${ROOT}/build-max25-bcpr/bin/max25-terminal" \ + "${ROOT}/build/bin/max25-terminal" \ + /usr/local/bin/max25-terminal + do + [[ -n "$c" && -x "$c" ]] || continue + if "$c" -h 2>&1 | grep -q -- '-d, --device'; then + echo "$c" + return 0 + fi + done + if command -v max25-terminal >/dev/null 2>&1; then + c="$(command -v max25-terminal)" + if "$c" -h 2>&1 | grep -q -- '-d, --device'; then + echo "$c" + return 0 + fi + fi + return 1 +} + +BIN="$(pick)" || { + echo "run-max25-terminal: no current max25-terminal with -d/--device" >&2 + echo " cmake -B build-max25-bcpr -DMAX25_BUILD_MAX25_BCPR=ON && cmake --build build-max25-bcpr" >&2 + echo " install: scripts/install-max25.sh or cmake --install build" >&2 + echo " Do not run stacks/terminal/max25-terminal leftovers." >&2 + exit 127 +} + +SOCK="${MAX25_UNIX:-/run/max25/modem.sock}" +if [[ ! -S "$SOCK" ]] && [[ "${1:-}" != "-h" ]] && [[ "${1:-}" != "--help" ]]; then + if [[ " $* " != *" -T "* ]] && [[ " $* " != *" --tcp-only "* ]]; then + echo "run-max25-terminal: no socket $SOCK — start max25d first:" >&2 + echo " run-max25d # or: ${SCRIPT_DIR}/run-max25d.sh" >&2 + exit 1 + fi +fi + +exec "$BIN" "$@" diff --git a/scripts/run-max25d.sh b/scripts/run-max25d.sh new file mode 100755 index 0000000..c3e1ca9 --- /dev/null +++ b/scripts/run-max25d.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Canonical max25d start — escalate to root only when hardware needs it. +# Works from source tree (scripts/) and from PREFIX/bin after install. +# Root needed: ttyS / USB serial / ioperm / /dev/port / max25-bcpr SER12 (max25e0). +# Escalation: already root → sudo -n → sudo → su → pkexec. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [[ -x "${SCRIPT_DIR}/max25d" && ! -d "${SCRIPT_DIR}/../stacks/daemon" ]]; then + DAEMON="${SCRIPT_DIR}/max25d" + TREE="" +else + TREE="$(cd "${SCRIPT_DIR}/.." && pwd)" + DAEMON="${TREE}/stacks/daemon/max25d" +fi + +if [[ ! -x "$DAEMON" ]]; then + echo "run-max25d: missing $DAEMON" >&2 + exit 1 +fi + +# Match real daemon only (not agent shells whose argv text mentions max25d). +if pgrep -x max25d >/dev/null 2>&1 \ + || pgrep -f -- '/max25d(\.py)?[[:space:]]+-c[[:space:]]' >/dev/null 2>&1; then + echo "run-max25d: max25d already running" >&2 + pgrep -af -- '/max25d(\.py)?[[:space:]]+-c[[:space:]]' | head -5 || true + pgrep -ax max25d | head -5 || true + ls -la /run/max25/modem.sock /var/run/max25/modem.sock 2>/dev/null \ + || echo "run-max25d: WARNING no modem.sock under /run/max25 or /var/run/max25" >&2 + exit 0 +fi + +# Accept path or max25d-style "-c path". +INI="${1:-}" +if [[ "$INI" == "-c" || "$INI" == "--config" ]]; then + INI="${2:-}" +fi +if [[ -z "$INI" ]]; then + if [[ -n "$TREE" && -f "${TREE}/local/max25d.ini" ]]; then + INI="${TREE}/local/max25d.ini" + elif [[ -f "${PWD}/local/max25d.ini" ]]; then + INI="${PWD}/local/max25d.ini" + elif [[ -f /etc/max25/max25d.ini ]]; then + INI=/etc/max25/max25d.ini + elif [[ -f /usr/local/etc/max25/max25d.ini ]]; then + INI=/usr/local/etc/max25/max25d.ini + else + echo "run-max25d: no INI (pass path or create local/max25d.ini / /etc/max25/max25d.ini / /usr/local/etc/max25/max25d.ini)" >&2 + exit 1 + fi +fi +if [[ ! -f "$INI" ]]; then + echo "run-max25d: INI not found: $INI" >&2 + exit 1 +fi + +needs_root() { + local f="$1" + [[ -f "$f" ]] || return 1 + if grep -Eiq '^[[:space:]]*(max25[-_]bcpr|bcpr|baycom|pccom)[[:space:]]*=[[:space:]]*yes' "$f"; then return 0; fi + if grep -Eiq '^[[:space:]]*(max25e0|max25e0:bc[01]|tnc2c|pktnc2|baycom-kiss|pccom-kiss)[[:space:]]*=' "$f"; then return 0; fi + if grep -Eiq '^[[:space:]]*device[[:space:]]*=[[:space:]]*(max25e0|tnc2c|pktnc2)' "$f"; then return 0; fi + if grep -Eqi '/dev/(ttyS|ttyUSB|ttyACM|port)\b' "$f"; then return 0; fi + if grep -Eiq '^[[:space:]]*unix_socket[[:space:]]*=[[:space:]]*/(var/)?run/max25/' "$f"; then return 0; fi + return 1 +} + +if [[ "$(id -u)" -ne 0 ]]; then + if needs_root "$INI"; then + if command -v sudo >/dev/null 2>&1; then + if sudo -n true 2>/dev/null; then + echo "run-max25d: hardware path — escalating via sudo -n" >&2 + exec sudo -n "$0" "$INI" + fi + echo "run-max25d: hardware path — escalating via sudo (password may be required)" >&2 + exec sudo "$0" "$INI" + fi + if command -v su >/dev/null 2>&1; then + echo "run-max25d: hardware path — escalating via su" >&2 + exec su -c "$(printf '%q ' "$0" "$INI")" + fi + if command -v pkexec >/dev/null 2>&1; then + echo "run-max25d: hardware path — escalating via pkexec" >&2 + exec pkexec "$0" "$INI" + fi + echo "run-max25d: ERROR need root for SER12/UART/ioperm" >&2 + exit 1 + fi + echo "run-max25d: starting unprivileged (uid=$(id -u))" >&2 +else + echo "run-max25d: running as root (uid=0)" >&2 +fi + +if [[ "$(id -u)" -eq 0 ]]; then + mkdir -p /run/max25 /var/run/max25 +fi +echo "run-max25d: ini=$INI" +exec "$DAEMON" -c "$INI" diff --git a/scripts/terminology-check.sh b/scripts/terminology-check.sh new file mode 100755 index 0000000..96c01fb --- /dev/null +++ b/scripts/terminology-check.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# terminology-check.sh — release gate for AGENT-INDEX §0.16 / §0.19 +# Fail on forbidden product terms in shipped markdown (not tests/archives). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +ERR=0 + +check() { + local pat="$1" msg="$2" + local hits + hits="$(grep -RInE --include='*.md' "$pat" README.md CHANGELOG.md RELEASE-READINESS.md docs share/clients plugins 2>/dev/null \ + | grep -viE 'Never |never |forbidden|LEGACY|removed|prefer bcpr|BayCom/based' || true)" + if [[ -n "$hits" ]]; then + echo "FAIL: $msg" + echo "$hits" | head -40 + ERR=1 + else + echo "OK: $msg" + fi +} + +check '\bbcpr-bc0\b' 'product device id bcpr-bc0 (use max25e0)' +check '\bbcpr-bc1\b' 'product device id bcpr-bc1 (use max25e0:bc1)' +# product device face for SER12 — allow historical "removed" lines +if grep -RInE --include='*.md' 'device = baycom-ser12|`baycom-ser12` \|' README.md docs 2>/dev/null \ + | grep -vE 'removed|LEGACY|kernel baycom|not a kernel|Never' | grep -q .; then + echo "FAIL: product baycom-ser12 as active device face" + grep -RInE --include='*.md' 'device = baycom-ser12|`baycom-ser12` \|' README.md docs 2>/dev/null | head -20 || true + ERR=1 +else + echo "OK: no active baycom-ser12 product device face in README/docs" +fi +check '\bKonverter\b' 'Konverter in shipped docs' +check '\bAX25SRV\b|\bax25srv\b' 'AX25SRV in shipped docs' + +if [[ "$ERR" -ne 0 ]]; then + exit 1 +fi +echo "terminology-check: PASS" +exit 0 diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..bc52a97 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# MainAX25-Stack (MAX25-Stack) — offline tests via CMake targets. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +if [[ ! -d build ]]; then + bash "${ROOT}/scripts/build.sh" +fi + +cmake --build build --target max25_test diff --git a/scripts/tx-rx-test.sh b/scripts/tx-rx-test.sh new file mode 100755 index 0000000..31b4435 --- /dev/null +++ b/scripts/tx-rx-test.sh @@ -0,0 +1,350 @@ +#!/usr/bin/env bash +# tx-rx-test.sh — unified TX/RX release test (BayCom/based max25-bcpr + classic TNC). +# Default: L0 offline only (CI / make test). Live/TX are operator-gated. +# Public: BayCom/based / max25-bcpr. Never Konverter/converter. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Tree: scripts/ → repo root. Install: PREFIX/bin or PREFIX/sbin → no stacks/. +if [[ -d "${SCRIPT_DIR}/../stacks/max25-bcpr" ]]; then + ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + cd "$ROOT" + _BCPR_CTL_DEFAULT="${ROOT}/stacks/max25-bcpr/tools/max25-bcpr-ctl" + _BCPR_INI_DEFAULT="${ROOT}/stacks/max25-bcpr/share/max25-bcpr.ini.example" +else + ROOT="" + cd "${PWD:-/}" + _BCPR_CTL_DEFAULT="$(command -v max25-bcpr-ctl 2>/dev/null || true)" + _BCPR_INI_DEFAULT="/etc/max25/max25-bcpr.ini" +fi + +LEVEL=0 +DEVICE=all # all | modem | tnc +DO_LIVE=0 +DO_TX=0 +FORCE_TX=0 +SECONDS_LIVE=15 +TX_SECONDS=3 +BCPR_INI="${BCPR_INI:-$_BCPR_INI_DEFAULT}" +SOCK="${MAX25_SOCK:-/run/max25/modem.sock}" +ERR=0 +BCPR_CTL="${BCPR_CTL:-$_BCPR_CTL_DEFAULT}" +if [[ -z "$BCPR_CTL" ]] || [[ ! -x "$BCPR_CTL" ]]; then + if command -v max25-bcpr-ctl >/dev/null 2>&1; then + BCPR_CTL="$(command -v max25-bcpr-ctl)" + elif [[ -n "$ROOT" && -x "$ROOT/stacks/max25-bcpr/tools/max25-bcpr-ctl" ]]; then + BCPR_CTL="$ROOT/stacks/max25-bcpr/tools/max25-bcpr-ctl" + fi +fi +BCPR_SMOKE="${BCPR_SMOKE:-}" +if [[ -z "$BCPR_SMOKE" ]]; then + if [[ -n "$ROOT" && -x "$ROOT/stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh" ]]; then + BCPR_SMOKE="$ROOT/stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh" + elif command -v max25-bcpr-rxtx-smoke.sh >/dev/null 2>&1; then + BCPR_SMOKE="$(command -v max25-bcpr-rxtx-smoke.sh)" + elif [[ -x /usr/local/sbin/max25-bcpr-rxtx-smoke.sh ]]; then + BCPR_SMOKE=/usr/local/sbin/max25-bcpr-rxtx-smoke.sh + fi +fi + +usage() { + cat <<'USAGE' +Usage: tx-rx-test.sh [--level N] [--device all|modem|tnc] [--live] [--tx] [--seconds N] [--tx-seconds N] [-c bcpr.ini] + + L0 offline TX/RX proofs (default) — CI / release-check / ctest + L1 soft preflight (no attach / no RF) + L2 live attach (modem: max25-bcprd timed; tnc: max25d sock status) + L3 live RX listen (same as L2 + RX readiness) + L4 TX (--tx required; PTT / RF risk; default ~3s MCR key) + + --live implies at least L2. --tx requires --live (L4). + --tx-seconds: modem PTT key target (default 3; proven ≈376B info). + Default: NO TX. Hard time caps. Safe for make test / CI. + + Static rule (§0.20): live --tx is refused unless RX was detected in this + run (modem: Soft-DCD/noise → state_dir/dcd-bc* or rx-activity-bc*; + TNC: live CONNECT/RX path OK first). Override only with --force-tx + (against policy — debug only). +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --level) LEVEL="$2"; shift 2 ;; + --device) DEVICE="$2"; shift 2 ;; + --live) DO_LIVE=1; shift ;; + --tx) DO_TX=1; shift ;; + --force-tx) FORCE_TX=1; shift ;; + --seconds) SECONDS_LIVE="$2"; shift 2 ;; + --tx-seconds) TX_SECONDS="$2"; shift 2 ;; + -c) BCPR_INI="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "ERROR: unknown arg: $1"; usage; exit 2 ;; + esac +done + +case "$DEVICE" in + all|modem|tnc) ;; + *) echo "ERROR: --device must be all|modem|tnc"; exit 2 ;; +esac + +if [[ "$DO_TX" -eq 1 && "$DO_LIVE" -eq 0 ]]; then + echo "ERROR: --tx requires --live" + exit 2 +fi + +if [[ "$DO_LIVE" -eq 1 && "$LEVEL" -lt 2 ]]; then + LEVEL=2 +fi +if [[ "$DO_TX" -eq 1 ]]; then + LEVEL=4 +fi + +if [[ "$SECONDS_LIVE" -lt 1 ]]; then + SECONDS_LIVE=1 +fi +if [[ "$SECONDS_LIVE" -gt 60 ]]; then + echo "WARN: capping --seconds from $SECONDS_LIVE to 60" + SECONDS_LIVE=60 +fi +if [[ "$TX_SECONDS" -lt 1 ]]; then + TX_SECONDS=1 +fi +if [[ "$TX_SECONDS" -gt 12 ]]; then + echo "WARN: capping --tx-seconds from $TX_SECONDS to 12" + TX_SECONDS=12 +fi +need_live=$((TX_SECONDS + 5)) +if [[ "$DO_TX" -eq 1 && "$SECONDS_LIVE" -lt "$need_live" ]]; then + echo "NOTE: raising --seconds from $SECONDS_LIVE to $need_live for --tx-seconds $TX_SECONDS" + SECONDS_LIVE=$need_live +fi + +log() { printf '%s\n' "$*"; } +ok() { log "PASS: $*"; } +fail() { log "FAIL: $*"; ERR=1; } +stage() { log ""; log "=== $* ==="; } +want_modem() { [[ "$DEVICE" == "all" || "$DEVICE" == "modem" ]]; } +want_tnc() { [[ "$DEVICE" == "all" || "$DEVICE" == "tnc" ]]; } + +run_l0() { + stage "L0 offline TX/RX" + if [[ -n "$ROOT" && -f "$ROOT/stacks/daemon/test_tx_rx_offline.py" ]]; then + if python3 "$ROOT/stacks/daemon/test_tx_rx_offline.py"; then + ok "test_tx_rx_offline (TNC + bcpr host)" + else + fail "test_tx_rx_offline" + fi + else + ok "test_tx_rx_offline skipped (installed layout — use bcpr-rxtx-smoke)" + fi + + if want_tnc; then + if python3 stacks/tncs/test_tnc_serial_recovery.py >/dev/null; then + ok "tnc serial recovery unit" + else + fail "tnc serial recovery unit" + fi + if python3 stacks/daemon/test_kiss_bridge.py >/dev/null; then + ok "kiss_bridge unit (TNC KISS)" + else + fail "kiss_bridge unit" + fi + fi + + if want_modem; then + # Default tree builds max25-bcpr (ON). Smoke when binaries exist. + bcprd="" + for cand in \ + "${BCPR_BUILD_DIR:-}/bin/max25-bcprd" \ + "${BUILD_DIR:-}/bin/max25-bcprd" \ + build/bin/max25-bcprd \ + build-default/bin/max25-bcprd \ + "build-max25-bcpr/bin/max25-bcprd" \ + "/tmp/max25-build-max25-bcpr-${USER:-user}/bin/max25-bcprd"; do + if [[ -n "$cand" && -x "$cand" ]]; then + bcprd="$cand" + break + fi + done + if [[ -z "$bcprd" ]]; then + warn "max25-bcpr L0 skipped (binary not found; default ON — expected built)" + elif [[ -x stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh ]]; then + if [[ -n "$bcprd" ]]; then + if MAX25_BCPR_NO_AUTOBUILD=1 bash stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh -c "$BCPR_INI"; then + ok "max25-bcpr-rxtx-smoke L0 (BayCom/based)" + else + fail "max25-bcpr-rxtx-smoke L0" + fi + else + if bash stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh -c "$BCPR_INI"; then + ok "max25-bcpr-rxtx-smoke L0 (BayCom/based)" + else + fail "max25-bcpr-rxtx-smoke L0" + fi + fi + else + fail "max25-bcpr-rxtx-smoke.sh missing" + fi + fi +} + +run_l1() { + stage "L1 soft preflight" + if want_modem; then + if [[ -n "${BCPR_CTL:-}" && -x "$BCPR_CTL" ]]; then + set +e + "$BCPR_CTL" -c "$BCPR_INI" preflight + pf_rc=$? + set -e + if [[ "$pf_rc" -eq 0 ]]; then + ok "max25-bcpr-ctl preflight" + else + if [[ "$DO_LIVE" -eq 1 ]]; then + fail "max25-bcpr-ctl preflight (required for --live modem)" + else + log "NOTE: bcpr preflight rc=$pf_rc — soft at L1 without --live" + fi + fi + else + fail "max25-bcpr-ctl missing (install max25-bcpr or set BCPR_CTL)" + fi + fi + if want_tnc; then + if [[ -f share/clients/tnc2c.yaml ]] && [[ -f stacks/tncs/tnc_serial_recovery.py ]]; then + ok "tnc client profile + recovery module present" + else + fail "tnc profile/recovery missing" + fi + fi +} + +live_tnc_sock() { + local mode="$1" # rx|tx + local sock="$SOCK" + if [[ ! -S "$sock" ]]; then + if [[ -S /tmp/max25/modem.sock ]]; then + sock=/tmp/max25/modem.sock + else + fail "no max25d unix socket ($SOCK) — start max25d with TNC first" + return 1 + fi + fi + log "using unix socket $sock (mode=$mode)" + python3 - "$sock" "$mode" <<'PY' || return 1 +import socket, sys, time + +sock_path, mode = sys.argv[1], sys.argv[2] + +class LineReader: + def __init__(self, s): + self.s = s + self.buf = b"" + def read(self, timeout=8.0): + self.s.settimeout(timeout) + while b"\n" not in self.buf: + chunk = self.s.recv(4096) + if not chunk: + raise RuntimeError("connection closed") + self.buf += chunk + line, self.buf = self.buf.split(b"\n", 1) + return line.decode("utf-8", errors="replace") + +s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +s.connect(sock_path) +r = LineReader(s) +hello = r.read() +assert hello == "OK", hello +status = r.read() +assert status.startswith("STATUS "), status +s.sendall(b"CONNECT\n") +ev = r.read() +assert ev == "EVENT connected", ev +assert r.read() == "OK" +if mode == "tx": + s.sendall(b"SEND TXRXTEST\n") + rx = r.read() + assert rx.startswith("RX "), rx + assert r.read() == "OK" + print("TNC live TX: SEND OK (host path)") +else: + # RX readiness: connected + status shows device + s.sendall(b"GET STATUS\n") + st = r.read() + assert "device=" in st, st + assert r.read() == "OK" + print("TNC live RX: CONNECT + STATUS OK (listen ready)") +s.close() +PY +} + +run_live() { + stage "L2/L3 live (${SECONDS_LIVE}s) device=$DEVICE" + if want_modem; then + local smoke_args=(-c "$BCPR_INI" --live --seconds "$SECONDS_LIVE") + if [[ "$DO_TX" -eq 1 ]]; then + smoke_args+=(--tx --tx-seconds "$TX_SECONDS") + if [[ "${FORCE_TX:-0}" -eq 1 ]]; then + smoke_args+=(--force-tx) + fi + log "WARN: modem --tx asserts PTT (~${TX_SECONDS}s; watch LED/wattmeter) — only after RX (§0.20)" + fi + _smoke="${BCPR_SMOKE:-stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh}" + if [[ ! -x "$_smoke" ]]; then + fail "max25-bcpr-rxtx-smoke.sh missing (install max25-bcpr or set BCPR_SMOKE)" + elif bash "$_smoke" "${smoke_args[@]}"; then + ok "bcpr live smoke (BayCom/based)" + else + fail "bcpr live smoke" + fi + fi + if want_tnc; then + local TNC_RX_OK=0 + if live_tnc_sock rx; then + ok "tnc live CONNECT (RX ready)" + TNC_RX_OK=1 + else + fail "tnc live CONNECT" + fi + if [[ "$DO_TX" -eq 1 ]]; then + if [[ "$TNC_RX_OK" -ne 1 && "${FORCE_TX:-0}" -eq 0 ]]; then + fail "TNC TX blocked (§0.20): RX/CONNECT failed" + else + log "WARN: TNC --tx sends one UI frame over air if radio keyed" + if live_tnc_sock tx; then + ok "tnc live CONNECT+SEND" + else + fail "tnc live CONNECT+SEND" + fi + fi + fi + fi +} + +# --- main --- +log "tx-rx-test level=$LEVEL device=$DEVICE live=$DO_LIVE tx=$DO_TX tx_seconds=$TX_SECONDS" +run_l0 + +if [[ "$LEVEL" -ge 1 ]]; then + run_l1 +fi + +if [[ "$LEVEL" -ge 2 || "$DO_LIVE" -eq 1 ]]; then + run_live +fi + +stage "summary" +if [[ "$ERR" -eq 0 ]]; then + if [[ "$DO_TX" -eq 1 ]]; then + ok "TX/RX complete through L4 (live + TX)" + elif [[ "$DO_LIVE" -eq 1 ]]; then + ok "TX/RX complete through L2/L3 (live RX, no TX)" + elif [[ "$LEVEL" -ge 1 ]]; then + ok "TX/RX L0+L1 complete" + else + ok "TX/RX L0 offline complete — use --live / --tx for hardware" + fi + exit 0 +fi +fail "one or more stages failed" +exit 1 |
