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 --- stacks/max25-bcpr/tools/NEBENBEI-MOVED.md | 5 + stacks/max25-bcpr/tools/bcpr-ultimate-diag.sh | 916 +++++++++++++++++++++ stacks/max25-bcpr/tools/max25-bcpr-ctl | 379 +++++++++ stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh | 566 +++++++++++++ .../max25-bcpr/tools/max25-bcpr-ultimate-diag.sh | 914 ++++++++++++++++++++ 5 files changed, 2780 insertions(+) create mode 100644 stacks/max25-bcpr/tools/NEBENBEI-MOVED.md create mode 100755 stacks/max25-bcpr/tools/bcpr-ultimate-diag.sh create mode 100755 stacks/max25-bcpr/tools/max25-bcpr-ctl create mode 100755 stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh create mode 100755 stacks/max25-bcpr/tools/max25-bcpr-ultimate-diag.sh (limited to 'stacks/max25-bcpr/tools') diff --git a/stacks/max25-bcpr/tools/NEBENBEI-MOVED.md b/stacks/max25-bcpr/tools/NEBENBEI-MOVED.md new file mode 100644 index 0000000..8a4ac20 --- /dev/null +++ b/stacks/max25-bcpr/tools/NEBENBEI-MOVED.md @@ -0,0 +1,5 @@ +# Dauerlauf helpers — not shipped here + +Experimental endurance / orch helpers are **not** part of this freigegeben product tree. + +Product L1 remains: `max25-bcpr-ctl`, smoke, ultimate-diag · host face **`max25e0`** · public mark **BayCom/based**. diff --git a/stacks/max25-bcpr/tools/bcpr-ultimate-diag.sh b/stacks/max25-bcpr/tools/bcpr-ultimate-diag.sh new file mode 100755 index 0000000..573585c --- /dev/null +++ b/stacks/max25-bcpr/tools/bcpr-ultimate-diag.sh @@ -0,0 +1,916 @@ +#!/usr/bin/env bash +# bcpr-ultimate-diag.sh — interactive BayCom/based (bcpr) TX/RX diagnostic ladder +# Public mark: BayCom/based. Internal path: bcpr. Never Konverter/converter. +# Target: AX25WRK1 intermittent "host keys, RF sometimes" (mic OK 4W; host MCR OK). +# +# Soft-TNC RE (2026-07-19) — operator warnings only (no MCR/code patch here): +# • FlexNet SER12 cal PTT watchdog: ~14.5 s keyed → ~500 ms unkey (discharge). +# Long continuous force-tx/cal: bcprd ptt_wd drops RTS ~500 ms every ~14.5 s +# (FlexNet SER12 mirror; disable with ptt_wd=no / --no-ptt-wd). +# • TXD: default pulse THR 0x00; experiment txd_bias=steady (UART break ≈ TFPCX). +# • Keep bcprd MCR Sailer 0x0e|bit / 0x0d (4PC-COM 0x0A is outlier — do not switch). +# Vault RE: 0-RESEARCHES/projects/max25-stack/2026-07-19-baycom-soft-tnc-serial-ptt-re.md +# +# Usage (Cursor IDE terminal, as operator): +# sudo -n /home/akb/Code/10-PROJECTS/MAX25-Stack/stacks/bcpr/tools/bcpr-ultimate-diag.sh +# BCPR_INI=/etc/max25/bcpr.ini ./bcpr-ultimate-diag.sh -c /etc/max25/bcpr.ini +# ./bcpr-ultimate-diag.sh --help +# ./bcpr-ultimate-diag.sh --all # run phases 1–9 with pauses +# ./bcpr-ultimate-diag.sh --menu # interactive menu (default) +# +# Needs: /usr/bin/sudo -n for ioport / live smoke / cal. Does NOT change MCR code. +# Does NOT use USB as product TX path. Does NOT kill max25d carelessly. +# +# Vault: 0-RESEARCHES/projects/max25-stack/2026-07-19-bcpr-ultimate-diag-script.md +# Relies on: bcpr-ctl, bcpr-rxtx-smoke.sh (same directory). + +# Note: interactive prompts use read; keep pipefail but relax -e around optional probes. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../../.." 2>/dev/null && pwd || true)" +CTL="${SCRIPT_DIR}/bcpr-ctl" +SMOKE_SH="${SCRIPT_DIR}/bcpr-rxtx-smoke.sh" +INI="${BCPR_INI:-/etc/max25/bcpr.ini}" +SUDO="/usr/bin/sudo" +LOG="" +MODE="menu" # menu | all | phaseN +PHASE_ONLY="" +RUN_TS="" + +# Result arrays for summary (parallel indices) +declare -a RES_PHASE=() +declare -a RES_HOST=() +declare -a RES_WATT=() +declare -a RES_BLED=() +declare -a RES_RDISP=() +declare -a RES_NOTE=() + +usage() { + cat <<'USAGE' +bcpr-ultimate-diag.sh — BayCom/based (bcpr) interactive diagnostic / force-TX ladder + +Usage: + bcpr-ultimate-diag.sh [-c INI] [--menu|--all|--phase N] [--help] + +Options: + -c INI bcpr.ini (default: $BCPR_INI or /etc/max25/bcpr.ini) + --menu interactive German menu (default) + --all run phases 1–9 linearly with pauses + RF prompts + --phase N run a single phase (1–9, or 0=restart menu) + -h, --help this help + +Environment: + BCPR_INI same as -c + BCPRD optional path to bcprd binary + +Phases: + 1 Preflight (ttyS0, fuser, dosbox-x, bcprd/max25d, INI, locks) + 2 Idle MCR sample via /dev/port + 3 bcpr-ctl status + telem (tx-last / dcd / rx-activity) + 4 RX listen (soft-DCD) — open SQ reminder + 5 Force-TX ladder 1s / 3s / 5s / 8s + wattmeter/LED/display prompts + 6 Cal/high (bcprd --cal high) if binary supports it + 7 Double-burst / back-to-back TX + 8 Contact hunt (DE-9 + 3.5mm) + 5s force-tx + wiggle + 9 Summary table + verdict hints + 0 Optional safe bcprd restart (bcpr-ctl stop/start only) + U USB note (SER12 unsupported on USB — not product TX) + +Log: /tmp/bcpr-ultimate-diag-YYYYMMDD-HHMMSS.log + +Examples: + sudo -n stacks/bcpr/tools/bcpr-ultimate-diag.sh --all + BCPR_INI=/etc/max25/bcpr.ini sudo -n ./bcpr-ultimate-diag.sh --menu +USAGE +} + +# ---------- logging / UI ---------- + +log() { + local line + line="$(printf '%s' "$*")" + printf '%s\n' "$line" + if [[ -n "${LOG:-}" ]]; then + printf '%s\n' "$line" >>"$LOG" + fi +} + +log_raw() { + # stdin → stdout + log + if [[ -n "${LOG:-}" ]]; then + tee -a "$LOG" + else + cat + fi +} + +banner() { + log "" + log "════════════════════════════════════════════════════════════" + log "$*" + log "════════════════════════════════════════════════════════════" +} + +pause_enter() { + local msg="${1:-Weiter mit Enter …}" + printf '\n%s ' "$msg" >/dev/tty + # shellcheck disable=SC2034 + local _dummy + read -r _dummy /dev/tty + read -r __ans /dev/tty + read -r __ans /dev/tty ;; + esac + done +} + +record_result() { + local phase="$1" host="$2" watt="$3" bled="$4" rdisp="$5" note="${6:-}" + RES_PHASE+=("$phase") + RES_HOST+=("$host") + RES_WATT+=("$watt") + RES_BLED+=("$bled") + RES_RDISP+=("$rdisp") + RES_NOTE+=("$note") +} + +need_sudo() { + if [[ ! -x "$SUDO" ]]; then + log "FEHLER: $SUDO fehlt — Live-Phasen brauchen sudo." + return 1 + fi + if ! "$SUDO" -n true 2>/dev/null; then + log "FEHLER: sudo -n fehlgeschlagen." + log " Bitte einmalig: sudo -v (oder NOPASSWD für diesen User)" + log " Dann Skript erneut starten." + return 1 + fi + return 0 +} + +run_sudo() { + need_sudo || return 1 + "$SUDO" -n "$@" +} + +ini_get() { + local section="$1" key="$2" + [[ -f "$INI" ]] || return 0 + awk -F= -v s="[$section]" -v k="$key" ' + $0 ~ /^\[/ { cur=$0; gsub(/[[:space:]]/,"",cur) } + cur==s && $1 ~ "^[[:space:]]*"k"[[:space:]]*$" { + v=$2; gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); print v; exit + }' "$INI" 2>/dev/null || true +} + +resolve_bcprd() { + if [[ -n "${BCPRD:-}" && "$BCPRD" != "bcprd" && -x "$BCPRD" ]]; then + printf '%s\n' "$BCPRD"; return 0 + fi + if command -v bcprd >/dev/null 2>&1; then + command -v bcprd; return 0 + fi + local cand + for cand in \ + "${BCPR_BUILD_DIR:-}/bin/bcprd" \ + "${ROOT}/build-bcpr/bin/bcprd" \ + "${ROOT}/build-bcpr-${USER:-user}/bin/bcprd" \ + "/tmp/max25-build-bcpr-${USER:-user}/bin/bcprd" \ + "${ROOT}/build/bin/bcprd" \ + /usr/local/bin/bcprd /usr/bin/bcprd; do + [[ -n "$cand" && -x "$cand" ]] && { printf '%s\n' "$cand"; return 0; } + done + return 1 +} + +serial_dev() { + local s + s="$(ini_get bc0 serial)" + printf '%s\n' "${s:-/dev/ttyS0}" +} + +state_dir() { + local sd + sd="$(ini_get bcpr state_dir)" + printf '%s\n' "${sd:-/tmp/bcpr}" +} + +iobase_val() { + local b + b="$(ini_get bc0 iobase)" + printf '%s\n' "${b:-0x3f8}" +} + +ensure_tools() { + local err=0 + if [[ ! -x "$CTL" ]]; then + log "FEHLER: bcpr-ctl fehlt: $CTL"; err=1 + fi + if [[ ! -x "$SMOKE_SH" ]]; then + log "FEHLER: bcpr-rxtx-smoke.sh fehlt: $SMOKE_SH"; err=1 + fi + return "$err" +} + +# ---------- Phase helpers ---------- + +# Last smoke host result (PASS|FAIL) — do not capture function stdout (log noise). +_LAST_HOST="—" + +smoke_force_tx() { + # smoke_force_tx SECONDS → sets _LAST_HOST + local secs="$1" + local listen=$((secs + 8)) + local rc=0 + [[ "$listen" -lt 12 ]] && listen=12 + log "→ L4 force-tx ${secs}s (smoke --live --tx --force-tx --tx-seconds ${secs})" + run_sudo "$CTL" -c "$INI" smoke --live --tx --force-tx --seconds "$listen" --tx-seconds "$secs" 2>&1 | log_raw + rc=${PIPESTATUS[0]} + if [[ "$rc" -eq 0 ]]; then + _LAST_HOST="PASS" + log "HOST: PASS (MCR keyed ~${secs}s target)" + else + _LAST_HOST="FAIL" + log "HOST: FAIL (smoke rc=$rc)" + fi +} + +prompt_rf_obs() { + # prompt_rf_obs PHASE_LABEL HOST_RESULT + # sets globals: _watt _bled _rdisp _extra + local label="$1" host="$2" + local watt bled rdisp extra + log "" + log "--- RF-Beobachtung: $label (Host=$host) ---" + log "Bitte Wattmeter / Board-LED / Radio-Display während des Keys prüfen." + ask watt "Wattmeter W (Zahl oder 0 / ?):" + ask_yn bled "Board-LED (PC-COM) an während Key?" + ask_yn rdisp "Radio-Display / TX-Anzeige an?" + ask extra "Kurznotiz (Enter = keine):" + _watt="$watt" + _bled="$bled" + _rdisp="$rdisp" + _extra="$extra" + record_result "$label" "$host" "$watt" "$bled" "$rdisp" "$extra" +} + +# ---------- Phases ---------- + +phase_preflight() { + banner "Phase 1 — Preflight" + local serial sd iobase irq fulldup dry + serial="$(serial_dev)" + sd="$(state_dir)" + iobase="$(iobase_val)" + irq="$(ini_get bc0 irq)" + fulldup="$(ini_get bc0 fulldup)" + dry="$(ini_get bcpr dry_run)" + + log "INI: $INI" + if [[ ! -f "$INI" ]]; then + log "FEHLER: INI fehlt: $INI" + log " Hinweis: Beispiel → stacks/bcpr/share/bcpr.ini.example → /etc/max25/bcpr.ini" + return 1 + fi + + log "--- INI Auszug [bcpr]/[bc0] ---" + log " dry_run=${dry:-?} state_dir=${sd}" + log " serial=${serial} iobase=${iobase} irq=${irq:-?} fulldup=${fulldup:-?}" + log " mode=$(ini_get bc0 mode) kiss_link=$(ini_get bc0 kiss_link)" + log " baud=$(ini_get bc0 baud) tx_delay=$(ini_get bc0 tx_delay)" + + log "--- Seriell ---" + if [[ -e "$serial" ]]; then + log "OK: $serial existiert" + ls -l "$serial" 2>&1 | log_raw || true + else + log "FEHLER: $serial fehlt" + fi + + log "--- fuser / lsof (wer hält Port?) ---" + if command -v fuser >/dev/null 2>&1; then + + run_sudo fuser -v "$serial" 2>&1 | log_raw + + else + log "WARN: fuser nicht installiert" + fi + if command -v lsof >/dev/null 2>&1; then + + run_sudo lsof "$serial" 2>&1 | log_raw + + fi + + log "--- dosbox-x / DOS-Gast ---" + if pgrep -a -f 'dosbox-x|dosbox' 2>/dev/null | log_raw; then + log "WARN: dosbox läuft — kann ttyS0/USB belegen (sniff/passthrough)." + else + log "OK: kein dosbox/dosbox-x Prozess" + fi + + log "--- Prozesse bcprd / max25d ---" + pgrep -a -x bcprd 2>/dev/null | log_raw || log " bcprd: nicht laufend" + pgrep -a -x max25d 2>/dev/null | log_raw || log " max25d: nicht laufend" + # wrapper scripts + pgrep -a -f 'run-max25d|max25d' 2>/dev/null | head -20 | log_raw || true + + log "--- Lock / State ($sd) ---" + if [[ -d "$sd" ]]; then + ls -la "$sd" 2>&1 | log_raw || true + for f in "$sd"/lock* "$sd"/*.lock "$sd"/bcprd.pid; do + [[ -e "$f" ]] || continue + log " lock/pid: $f" + [[ -f "$f" ]] && { log " content:"; cat "$f" 2>&1 | log_raw || true; } + done + else + log "WARN: state_dir fehlt: $sd" + fi + + log "--- Kernel baycom_* Module (sollten NICHT geladen sein) ---" + if command -v lsmod >/dev/null 2>&1; then + lsmod 2>/dev/null | awk '/^baycom_/ {print}' | log_raw || log "OK: keine baycom_* Module" + fi + + log "--- setserial (falls vorhanden) ---" + local ss + for ss in /usr/bin/setserial /bin/setserial /sbin/setserial /usr/sbin/setserial; do + if [[ -x "$ss" ]]; then + + run_sudo "$ss" -g "$serial" 2>&1 | log_raw + + break + fi + done + + log "--- bcpr-ctl preflight ---" + + run_sudo "$CTL" -c "$INI" preflight 2>&1 | log_raw + local pf=$? + + if [[ "$pf" -eq 0 ]]; then + log "OK: preflight PASS" + else + log "WARN: preflight rc=$pf (Port busy wenn Stack läuft — normal bei live max25d/bcprd)" + fi + + log "--- Tools ---" + log " CTL=$CTL" + log " SMOKE=$SMOKE_SH" + local bin + if bin="$(resolve_bcprd)"; then + log " bcprd=$bin" + else + log " WARN: bcprd Binary nicht gefunden (Build: -DMAX25_BUILD_BCPR=ON)" + fi + + pause_enter "Phase 1 fertig — Enter für weiter …" + return 0 +} + +phase_idle_mcr() { + banner "Phase 2 — Idle MCR Sample (/dev/port)" + local iobase mcr_off + iobase="$(iobase_val)" + # MCR = iobase+4 + mcr_off=$((iobase + 4)) + log "iobase=$iobase MCR=$(printf '0x%x' "$mcr_off") (iobase+4)" + log "Erwartung Idle (Sailer/bcpr): oft 0x0d (DTR+OUT2+RTS-clear) — Werte nur Info." + + if ! need_sudo; then + log "überspringe MCR-Sample (kein sudo)" + pause_enter + return 0 + fi + + run_sudo python3 - "$iobase" <<'PY' 2>&1 | log_raw +import os, sys, time +iobase = int(sys.argv[1], 0) +mcr = iobase + 4 +fd = os.open("/dev/port", os.O_RDONLY) +try: + vals = [] + for i in range(20): + os.lseek(fd, mcr, os.SEEK_SET) + v = ord(os.read(fd, 1)) + vals.append(v) + time.sleep(0.05) + uniq = sorted(set(vals)) + print("Idle MCR samples (20×50ms): " + ",".join("0x%02x" % v for v in vals)) + print("Unique: " + ",".join("0x%02x" % v for v in uniq)) + rts = [(v & 0x02) != 0 for v in vals] + print("RTS asserted in any sample: %s" % ("YES" if any(rts) else "no")) + if all(v == 0 for v in vals): + print("WARN: all-zero — /dev/port may be inaccessible or wrong iobase") +finally: + os.close(fd) +PY + local rc=$? + + if [[ "$rc" -ne 0 ]]; then + log "WARN: Idle-MCR Sample fehlgeschlagen (rc=$rc) — CAP_SYS_RAWIO / iobase prüfen" + fi + pause_enter "Phase 2 fertig — Enter …" + return 0 +} + +phase_status() { + banner "Phase 3 — Status + Telemetrie" + + run_sudo "$CTL" -c "$INI" status 2>&1 | log_raw + + local sd + sd="$(state_dir)" + log "--- Telemetrie unter $sd ---" + for f in "$sd"/tx-last-bc* "$sd"/dcd-bc* "$sd"/rx-activity-bc* "$sd"/kiss-bc*; do + [[ -e "$f" ]] || continue + log "FILE: $f" + if [[ -L "$f" ]]; then + log " symlink → $(readlink -f "$f" 2>/dev/null || readlink "$f")" + elif [[ -f "$f" ]]; then + cat "$f" 2>&1 | log_raw || true + fi + done + pause_enter "Phase 3 fertig — Enter …" + return 0 +} + +phase_rx_listen() { + banner "Phase 4 — RX Listen (soft-DCD)" + log "OPERATOR: Squellch öffnen / Rauschen/SQ so dass Soft-DCD aktiv werden kann." + log " (§0.20 RX before TX — hier nur Listen, kein TX)" + pause_enter "SQ bereit? Enter startet ~12s RX listen …" + + run_sudo "$CTL" -c "$INI" smoke --live --seconds 12 2>&1 | log_raw + local rc=${PIPESTATUS[0]} + + if [[ "$rc" -eq 0 ]]; then + log "OK: RX-Listen smoke beendet (rc=0)" + else + log "WARN: RX-Listen smoke rc=$rc (siehe Log; Stack/INI prüfen)" + fi + + local sd + sd="$(state_dir)" + for f in "$sd"/dcd-bc0 "$sd"/rx-activity-bc0; do + [[ -f "$f" ]] || continue + log "Nach RX: $f" + cat "$f" 2>&1 | log_raw || true + done + pause_enter "Phase 4 fertig — Enter …" + return 0 +} + +phase_force_tx_ladder() { + banner "Phase 5 — Force-TX Ladder (1 / 3 / 5 / 8 s)" + log "WARNUNG: --force-tx ohne RX-Nachweis (§0.20 Override) — Debug / Intermittent-Hunt." + log "Wattmeter bereithalten. USB-Pfad ist KEIN Produkt-TX." + log "" + log "PTT-WATCHDOG (FlexNet SER12 / WD-Boards):" + log " Hardware kann PTT nach ~14,5 s Dauer-Key abwerfen (Discharge ~500 ms)." + log " Bei langen Läufen / vielen Keys hintereinander: zwischendurch UNKEY / Pause ~14 s." + log " Sonst: RF fällt trotz Host-MCR PASS (Watchdog, kein UART-Fehler)." + log "" + log "BEOBACHTUNGSHINWEIS TXD (Charge-Pump):" + log " bcprd/Sailer: THR 0x00 gepulst · TFPCX: TXD oft steady +12 V." + log " Notiz wenn RF mitten im Key einbricht / flackert (Pump/Bias-Kandidat)." + pause_enter "Bereit für Ladder? Enter …" + + local secs host + for secs in 1 3 5 8; do + banner "Force-TX ${secs}s" + pause_enter "Wattmeter beobachten — Enter startet ${secs}s Key …" + smoke_force_tx "$secs" + host="$_LAST_HOST" + # show telem snapshot + local sd tl + sd="$(state_dir)" + tl="$sd/tx-last-bc0" + if [[ -f "$tl" ]]; then + log "--- tx-last-bc0 nach ${secs}s ---" + cat "$tl" 2>&1 | log_raw || true + fi + prompt_rf_obs "L4-${secs}s" "$host" + pause_enter "Nächste Stufe — Enter …" + done + return 0 +} + +phase_cal_high() { + banner "Phase 6 — Cal/high (bcprd --cal high)" + local bin + if ! bin="$(resolve_bcprd)"; then + log "WARN: bcprd nicht gefunden — Cal übersprungen" + pause_enter + return 0 + fi + + # Prefer strings(1); never start bare bcprd. Source contract: --cal high|low|alt. + if command -v strings >/dev/null 2>&1 && ! strings "$bin" 2>/dev/null | grep -qF -- '--cal'; then + log "WARN: strings fand kein --cal in Binary — Phase trotzdem anbieten (Operator kann abbrechen)" + fi + + log "bcprd=$bin" + log "Cal = kontinuierlicher SER12 Tone+PTT (DOS cal.exe Stil), kein KISS." + log "Sicher: nur bcprd stoppen via bcpr-ctl — max25d Wrapper NICHT hart killen." + log "" + log "PTT-WATCHDOG: FlexNet cal pausiert alle ~14,5 s für ~500 ms (PTT-Discharge)." + log " Dieses Skript: --cal high ~10 s (unter WD). Längere manuelle Cal/" + log " Dauer-TX: UNKEY/Pause ~14 s einplanen — sonst RF-Tot auf WD-Boards." + log "TXD: bcprd pulst THR 0x00 (nicht TFPCX-steady) — RF-Einbruch mid-cal notieren." + log "" + local do_cal + ask_yn do_cal "bcprd stoppen und --cal high ~10s starten?" + if [[ "$do_cal" != "j" ]]; then + log "Cal übersprungen (Operator)" + pause_enter + return 0 + fi + + if ! need_sudo; then + pause_enter + return 0 + fi + + log "→ bcpr-ctl stop" + + run_sudo "$CTL" -c "$INI" stop 2>&1 | log_raw + + sleep 1 + + # Ensure no stray bcprd + if pgrep -x bcprd >/dev/null 2>&1; then + log "WARN: bcprd noch aktiv nach stop — pkill -x bcprd (kein max25d)" + + run_sudo pkill -x bcprd 2>&1 | log_raw + + sleep 1 + fi + + log "→ $bin -c $INI --cal high --seconds 10" + pause_enter "Wattmeter bereit — Enter startet cal high 10s …" + + run_sudo "$bin" -c "$INI" --cal high --seconds 10 2>&1 | log_raw + local rc=$? + + local host="PASS" + [[ "$rc" -eq 0 ]] || host="FAIL(rc=$rc)" + prompt_rf_obs "CAL-high-10s" "$host" + + local restart + ask_yn restart "bcprd danach wieder starten (bcpr-ctl start)?" + if [[ "$restart" == "j" ]]; then + + run_sudo "$CTL" -c "$INI" start 2>&1 | log_raw + + log "Hinweis: wenn max25d den Stack besitzt, ggf. max25d neu starten (Operator, nicht dieses Skript)." + fi + pause_enter "Phase 6 fertig — Enter …" + return 0 +} + +phase_double_burst() { + banner "Phase 7 — Double Burst (back-to-back TX)" + log "Zwei Force-TX 3s hintereinander (kurze Pause dazwischen)." + pause_enter "Enter startet Burst A …" + local host_a host_b + smoke_force_tx 3 + host_a="$_LAST_HOST" + sleep 1 + log "Burst B …" + smoke_force_tx 3 + host_b="$_LAST_HOST" + prompt_rf_obs "DoubleBurst-A+B" "${host_a}/${host_b}" + pause_enter "Phase 7 fertig — Enter …" + return 0 +} + +phase_contact_hunt() { + banner "Phase 8 — Contact Hunt (DE-9 + 3.5mm)" + log "OPERATOR-CHECKLISTE:" + log " 1) DE-9 (PC-COM ↔ Host) und 3.5mm (Modem ↔ Radio) fest stecken" + log " 2) Während dem nächsten Key leicht wackeln (Stecker/Kabel)" + log " 3) Wattmeter + Board-LED + Radio-Display beobachten" + log " Ziel: intermittenter Kontakt vs. dauerhaft tot unterscheiden" + pause_enter "Stecker fest? Enter startet 5s force-tx (währenddessen wackeln) …" + + local host + smoke_force_tx 5 + host="$_LAST_HOST" + local obs + ask obs "Beobachtung während Wackeln (z.B. 'kurz 2W' / 'immer 0' / 'LED flackert'):" + prompt_rf_obs "Contact-5s" "$host" + # overwrite last note with wiggle observation if empty note + if [[ -n "$obs" && ${#RES_NOTE[@]} -gt 0 ]]; then + RES_NOTE[$((${#RES_NOTE[@]} - 1))]="$obs | ${_extra:-}" + log "[contact-note] $obs" + fi + pause_enter "Phase 8 fertig — Enter …" + return 0 +} + +phase_usb_note() { + banner "Hinweis U — USB SER12 (kein Produkt-TX)" + log "USB-UART (z.B. /dev/ttyUSB0 / FTDI) ist KEIN unterstützter bcprd SER12-Pfad." + log " bcprd braucht ioperm|/dev/port + echte ISA/LPC iobase — USB hat das nicht." + log " Station: RF nur ttyS0+CB; USB0 = Modem ohne Radio → Wattmeter ungültig." + log " DOSBox Soft-TNC auf USB ≠ bcpr Produkt-TX." + pause_enter + return 0 +} + +phase_restart_menu() { + banner "Optional 0 — Sicheres bcprd Restart" + log "Methode (aus bcpr-ctl): stop → start. Killt NICHT max25d." + log "Wenn max25d den Stack besitzt: nach stop/start ggf. max25d-seitig neu binden." + log "" + log " Aktueller Status:" + + run_sudo "$CTL" -c "$INI" status 2>&1 | log_raw + + local act + ask act "Aktion: [s]top / [t]start / [r]estart / [a]bbruch:" + case "${act,,}" in + s|stop) + run_sudo "$CTL" -c "$INI" stop 2>&1 | log_raw + ;; + t|start) + run_sudo "$CTL" -c "$INI" start 2>&1 | log_raw + ;; + r|restart|re) + run_sudo "$CTL" -c "$INI" stop 2>&1 | log_raw + sleep 1 + run_sudo "$CTL" -c "$INI" start 2>&1 | log_raw + ;; + *) + log "Abbruch — keine Änderung" + ;; + esac + + run_sudo "$CTL" -c "$INI" status 2>&1 | log_raw + + pause_enter + return 0 +} + +phase_summary() { + banner "Phase 9 — Summary + Verdict" + log "Logdatei: $LOG" + log "" + log "┌──────────────────┬──────────┬──────────┬──────────┬──────────┐" + log "│ Phase │ Host │ Watt W │ BoardLED │ RadioTX │" + log "├──────────────────┼──────────┼──────────┼──────────┼──────────┤" + local i n + n=${#RES_PHASE[@]} + if [[ "$n" -eq 0 ]]; then + log "│ (keine TX-Phasen aufgezeichnet) │" + else + for ((i = 0; i < n; i++)); do + printf '│ %-16s │ %-8s │ %-8s │ %-8s │ %-8s │\n' \ + "${RES_PHASE[$i]:0:16}" \ + "${RES_HOST[$i]:0:8}" \ + "${RES_WATT[$i]:0:8}" \ + "${RES_BLED[$i]:0:8}" \ + "${RES_RDISP[$i]:0:8}" | log_raw + if [[ -n "${RES_NOTE[$i]:-}" ]]; then + log "│ note: ${RES_NOTE[$i]}" + fi + done + fi + log "└──────────────────┴──────────┴──────────┴──────────┴──────────┘" + + # Verdict heuristics + local any_host_pass=0 any_host_fail=0 any_rf=0 all_rf_zero=1 + for ((i = 0; i < n; i++)); do + case "${RES_HOST[$i]}" in + *PASS*) any_host_pass=1 ;; + *FAIL*) any_host_fail=1 ;; + esac + case "${RES_WATT[$i]}" in + ''|'0'|'0.0'|'?'|'n'|'nein') ;; + *) + # non-zero / non-empty that looks like power + if [[ "${RES_WATT[$i]}" =~ ^[0-9]*\.?[0-9]+$ ]]; then + if awk -v w="${RES_WATT[$i]}" 'BEGIN{exit !(w>0)}'; then + any_rf=1 + all_rf_zero=0 + fi + else + # free text — if contains number >0 heuristic + if [[ "${RES_WATT[$i]}" =~ [1-9] ]]; then + any_rf=1 + all_rf_zero=0 + fi + fi + ;; + esac + if [[ "${RES_BLED[$i]}" == "j" || "${RES_RDISP[$i]}" == "j" ]]; then + any_rf=1 + all_rf_zero=0 + fi + done + + log "" + log "=== Verdict-Hinweise (Heuristik, kein Automatik-Urteil) ===" + if [[ "$n" -eq 0 ]]; then + log "• Keine Ladder-Daten — nur Preflight/Status gelaufen." + elif [[ "$any_host_pass" -eq 1 && "$all_rf_zero" -eq 1 && "$any_rf" -eq 0 ]]; then + log "• HOST OK / RF tot oder 0 W: Fehlerklasse NACH UART" + log " → DE-9/3.5mm Kontakt, Charge-Pump/Vcc, PTT-Transistor, Mic-Buchse, Radio" + log " → Mic-alone 4 W OK stützt: Radio selbst ok; Pfad Modem↔Mic intermittierend" + elif [[ "$any_host_pass" -eq 1 && "$any_rf" -eq 1 ]]; then + log "• HOST OK / RF zeitweise sichtbar: INTERMITTENT RF (Kontakt/Pump/AF)" + log " → Contact-Hunt wiederholen; zwischen langen Keys Pause ~14 s (PTT-WD)" + log " → TXD pulse vs steady (TFPCX) als Beobachtung; Kabel/Stecker fest" + elif [[ "$any_host_fail" -eq 1 && "$any_host_pass" -eq 0 ]]; then + log "• HOST FAIL: zuerst Stack/KISS/fulldup/MCR — nicht primär Wattmeter" + log " → bcpr-ctl status, kiss_link, fulldup=yes bei Soft-DCD, kein zweites bcprd" + log " → MCR bleibt Sailer 0x0e|bit / 0x0d (nicht 4PC-COM 0x0A)" + else + log "• Gemischte Ergebnisse — Log + Notizen vergleichen; Phasen 5/8 wiederholen." + fi + log "" + log "SSoT: 0-RESEARCHES/projects/max25-stack/2026-07-19-bcpr-host-keyed-wattmeter-tot.md" + log " 2026-07-19-bcpr-mcr-ok-rf-zero-after-4w.md · winning-recipe · RF intermittent" + log " soft-TNC RE: 2026-07-19-baycom-soft-tnc-serial-ptt-re.md (WD · TXD · MCR)" + log "USB: kein Produkt-TX (Phase U)." + log "" + log "Log gespeichert: $LOG" + return 0 +} + +run_all() { + phase_preflight + phase_idle_mcr + phase_status + phase_rx_listen + phase_force_tx_ladder + phase_cal_high + phase_double_burst + phase_contact_hunt + phase_usb_note + phase_summary +} + +menu_loop() { + while true; do + banner "bcpr Ultimate Diag — Menü" + log "INI=$INI" + log "Log=$LOG" + log "" + log " 1) Preflight" + log " 2) Idle MCR" + log " 3) Status + Telem" + log " 4) RX Listen" + log " 5) Force-TX Ladder 1/3/5/8s" + log " 6) Cal/high" + log " 7) Double Burst" + log " 8) Contact Hunt" + log " 9) Summary / Verdict" + log " A) Alle Phasen 1–9 (+ USB-Hinweis)" + log " 0) bcprd stop/start (sicher)" + log " U) USB-Hinweis" + log " Q) Beenden" + log "" + local choice + ask choice "Wahl:" + case "${choice^^}" in + 1) phase_preflight ;; + 2) phase_idle_mcr ;; + 3) phase_status ;; + 4) phase_rx_listen ;; + 5) phase_force_tx_ladder ;; + 6) phase_cal_high ;; + 7) phase_double_burst ;; + 8) phase_contact_hunt ;; + 9) phase_summary ;; + A|ALL) run_all ;; + 0) phase_restart_menu ;; + U) phase_usb_note ;; + Q|X|EXIT|QUIT) + phase_summary + log "Ende." + return 0 + ;; + *) + log "Unbekannte Wahl: $choice" + ;; + esac + done +} + +# ---------- main ---------- + +while [[ $# -gt 0 ]]; do + case "$1" in + -c) + [[ $# -ge 2 ]] || { echo "ERROR: -c needs INI path"; exit 2; } + INI="$2" + shift 2 + ;; + --menu) MODE="menu"; shift ;; + --all) MODE="all"; shift ;; + --phase) + [[ $# -ge 2 ]] || { echo "ERROR: --phase needs N"; exit 2; } + MODE="phase" + PHASE_ONLY="$2" + shift 2 + ;; + -h|--help) usage; exit 0 ;; + *) + echo "ERROR: unknown arg: $1" + usage + exit 2 + ;; + esac +done + +RUN_TS="$(date +%Y%m%d-%H%M%S)" +LOG="/tmp/bcpr-ultimate-diag-${RUN_TS}.log" +: >"$LOG" || { + echo "FEHLER: kann Log nicht schreiben: $LOG" + exit 1 +} + +# Interactive reads use /dev/tty; log()/log_raw append to $LOG (no exec-tee double). +banner "bcpr Ultimate Diag — BayCom/based (bcpr)" +log "Start: $(date -R 2>/dev/null || date)" +log "Host: $(hostname 2>/dev/null || echo '?')" +log "User: $(id -un 2>/dev/null || echo '?') uid=$(id -u)" +log "INI: $INI (override: BCPR_INI / -c)" +log "Log: $LOG" +log "Tools: $CTL" +log "" +log "Hinweis: PTT-Watchdog ~14,5 s / 500 ms — bei langen Keys unkey/pausieren." +log " TXD: bcprd pulst (Sailer); TFPCX oft steady — RF mid-key notieren." +log " MCR: Sailer belassen (kein 4PC-COM 0x0A-Experiment in diesem Skript)." +log "" + +ensure_tools || { + log "Abbruch: Tools fehlen." + exit 1 +} + +# Soft check sudo early (warn only for menu; hard for --all live) +if ! need_sudo; then + log "WARN: ohne sudo -n sind Live/TX/Cal/MCR-Phasen blockiert." + if [[ "$MODE" == "all" ]]; then + log "Abbruch (--all braucht sudo -n)." + exit 1 + fi +fi + +case "$MODE" in + all) + run_all + ;; + phase) + case "$PHASE_ONLY" in + 1) phase_preflight ;; + 2) phase_idle_mcr ;; + 3) phase_status ;; + 4) phase_rx_listen ;; + 5) phase_force_tx_ladder ;; + 6) phase_cal_high ;; + 7) phase_double_burst ;; + 8) phase_contact_hunt ;; + 9) phase_summary ;; + 0) phase_restart_menu ;; + U|u) phase_usb_note ;; + *) + log "FEHLER: unbekannte Phase $PHASE_ONLY" + exit 2 + ;; + esac + ;; + menu|*) + menu_loop + ;; +esac + +log "" +log "Fertig. Log: $LOG" +exit 0 diff --git a/stacks/max25-bcpr/tools/max25-bcpr-ctl b/stacks/max25-bcpr/tools/max25-bcpr-ctl new file mode 100755 index 0000000..39510a7 --- /dev/null +++ b/stacks/max25-bcpr/tools/max25-bcpr-ctl @@ -0,0 +1,379 @@ +#!/usr/bin/env bash +# max25-bcpr-ctl — BayCom/based PC-COM helper (preflight / start / stop). +# Lite policy: plain English operator messages (new Linux user level). +set -euo pipefail + +INI="${MAX25_BCPR_INI:-${BCPR_INI:-/etc/max25/max25-bcpr.ini}}" +BCPRD="${MAX25_BCPRD:-${BCPRD:-max25-bcprd}}" +BCPRD_INIT="${MAX25_BCPRD_INIT:-max25-bcprd-init}" +PIDFILE="${MAX25_BCPR_PIDFILE:-/tmp/max25-bcpr/max25-bcprd.pid}" +STATE_DIR="/tmp/max25-bcpr" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SMOKE_SH="$SCRIPT_DIR/max25-bcpr-rxtx-smoke.sh" + +msg() { echo "max25-bcpr-ctl: $*"; } +ok() { msg "OK — $*"; } +warn(){ msg "WARN — $*"; } +err() { msg "ERROR — $*" >&2; } + +usage() { + cat </dev/null 2>&1; then + command -v "$BCPRD_INIT"; return + fi + local root cand + root="$(cd "$SCRIPT_DIR/../../.." && pwd)" + for cand in \ + ${MAX25_BCPR_BUILD_DIR:-${BCPR_BUILD_DIR:-}}/bin/max25-bcprd-init \ + "$root/build-max25-bcpr/bin/max25-bcprd-init" \ + "$root/build-max25-bcpr-${USER:-user}/bin/max25-bcprd-init" \ + "/tmp/max25-build-max25-bcpr-${USER:-user}/bin/max25-bcprd-init" \ + "$root/build/bin/max25-bcprd-init" \ + /usr/local/bin/max25-bcprd-init /usr/bin/max25-bcprd-init; do + [[ -n "$cand" && -x "$cand" ]] && { echo "$cand"; return; } + done + return 1 +} + +resolve_max25_bcprd() { + if [[ -n "${BCPRD:-}" && "$BCPRD" != "max25-bcprd" && -x "$BCPRD" ]]; then + echo "$BCPRD"; return + fi + if command -v "$BCPRD" >/dev/null 2>&1; then + command -v "$BCPRD"; return + fi + local root cand + root="$(cd "$SCRIPT_DIR/../../.." && pwd)" + for cand in \ + ${MAX25_BCPR_BUILD_DIR:-${BCPR_BUILD_DIR:-}}/bin/max25-bcprd \ + "$root/build-max25-bcpr/bin/max25-bcprd" \ + "$root/build-max25-bcpr-${USER:-user}/bin/max25-bcprd" \ + "/tmp/max25-build-max25-bcpr-${USER:-user}/bin/max25-bcprd" \ + "$root/build/bin/max25-bcprd" \ + /usr/local/bin/max25-bcprd /usr/bin/max25-bcprd; do + [[ -n "$cand" && -x "$cand" ]] && { echo "$cand"; return; } + done + return 1 +} + +ini_get() { + local section="$1" key="$2" + awk -F= -v s="[$section]" -v k="$key" ' + $0 ~ /^\[/ { cur=$0; gsub(/[[:space:]]/,"",cur) } + cur==s && $1 ~ "^[[:space:]]*"k"[[:space:]]*$" { + v=$2; gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); print v; exit + }' "$INI" 2>/dev/null || true +} + +is_truthy() { + case "${1,,}" in yes|true|on|1) return 0 ;; *) return 1 ;; esac +} + +find_setserial() { + local p + for p in /usr/bin/setserial /bin/setserial /sbin/setserial /usr/sbin/setserial; do + [[ -x "$p" ]] && { echo "$p"; return; } + done + command -v setserial 2>/dev/null || true +} + +setserial_port_irq() { + local bin="$1" dev="$2" out port irq + out="$("$bin" -g "$dev" 2>/dev/null)" || return 1 + port="$(printf '%s\n' "$out" | sed -n 's/.*Port:[[:space:]]*\(0x[0-9a-fA-F]\+\).*/\1/p' | head -1)" + irq="$(printf '%s\n' "$out" | sed -n 's/.*IRQ:[[:space:]]*\([0-9]\+\).*/\1/p' | head -1)" + printf '%s %s\n' "${port:-}" "${irq:-}" +} + +norm_hex() { + local v="${1,,}" + if [[ "$v" =~ ^0x ]]; then + printf '0x%x' "$((v))" + else + printf '%s' "$v" + fi +} + +is_freebsd() { + [[ "$(uname -s)" == "FreeBSD" ]] +} + +port_busy() { + local serial="$1" + local out + if command -v lsof >/dev/null 2>&1; then + out="$(lsof "$serial" 2>/dev/null || true)" + [[ -n "$out" ]] && return 0 + return 1 + fi + if command -v fuser >/dev/null 2>&1; then + out="$(fuser "$serial" 2>/dev/null || true)" + [[ -n "$out" ]] + return $? + fi + return 1 +} + +baycom_modules_loaded() { + local m + if ! command -v lsmod >/dev/null 2>&1; then + return 1 + fi + for m in baycom_ser_fdx baycom_ser_hdx baycom_par baycom_epp; do + if lsmod 2>/dev/null | awk '{print $1}' | grep -qx "$m"; then + echo "$m" + return 0 + fi + done + return 1 +} + +cmd_preflight() { + local fail=0 dry serial irq iobase name sysirq got ss ss_port ss_irq pair + local -a seen_irq=() seen_serial=() + if [[ ! -f "$INI" ]]; then + err "config not found ($INI) — run max25-lite-setup or copy stacks/max25-bcpr/share/max25-bcpr.ini.example" + return 1 + fi + dry="$(ini_get max25-bcpr dry_run)"; [[ -z "$dry" ]] && dry="$(ini_get bcpr dry_run)" + sd="$(ini_get max25-bcpr state_dir)"; [[ -z "$sd" ]] && sd="$(ini_get max25-bcpr state_dir)" + [[ -n "$sd" ]] && STATE_DIR="$sd" + msg "checking BayCom/based PC-COM setup (config: $INI)" + + if mod="$(baycom_modules_loaded)"; then + err "Linux BayCom kernel driver is loaded ($mod) — unload it first: sudo modprobe -r $mod" + fail=1 + else + ok "no conflicting Linux BayCom kernel driver" + fi + + ss="$(find_setserial)" + if [[ -z "$ss" ]]; then + warn "setserial not installed — limited port checks (optional: sudo apt install setserial)" + else + ok "setserial found" + fi + + for sec in bc0 bc1; do + serial="$(ini_get "$sec" serial)" + [[ -z "$serial" ]] && continue + enabled="$(ini_get "$sec" enabled)" + if [[ -n "$enabled" ]] && ! is_truthy "$enabled"; then + msg "section [$sec] disabled — skipped" + continue + fi + irq="$(ini_get "$sec" irq)" + iobase="$(ini_get "$sec" iobase)" + msg "port [$sec]: serial=$serial irq=${irq:-?} iobase=${iobase:-?}" + + if is_truthy "$dry"; then + msg "dry-run mode — skipping live port checks" + continue + fi + + if [[ ! -e "$serial" ]]; then + if is_freebsd && [[ -n "$iobase" && "$iobase" != "0" ]]; then + warn "$serial not present — using direct I/O address $iobase (FreeBSD)" + else + err "serial port missing ($serial) — check cable, enable UART in BIOS, or fix max25-bcpr.ini" + fail=1; continue + fi + else + ok "serial port exists ($serial)" + fi + + if [[ -e "$serial" ]]; then + if port_busy "$serial"; then + err "serial port busy ($serial) — close minicom, other TNC tools, or second max25d" + fail=1 + else + ok "serial port free ($serial)" + fi + elif is_freebsd && [[ -n "$iobase" && "$iobase" != "0" ]]; then + ok "no tty node — direct I/O path" + fi + + if [[ -z "$irq" || "$irq" == "0" ]]; then + err "IRQ missing in [$sec] — set irq= in max25-bcpr.ini (often 4 for COM1 / ttyS0)" + fail=1 + fi + + name="${serial##*/}" + sysirq="/sys/class/tty/${name}/irq" + if [[ -r "$sysirq" ]]; then + got="$(tr -d '[:space:]' <"$sysirq")" + if [[ -n "$irq" && "$irq" != "0" && "$got" != "$irq" ]]; then + err "IRQ mismatch — config says $irq but system says $got — fix max25-bcpr.ini" + fail=1 + elif [[ -n "$irq" && "$irq" != "0" ]]; then + ok "IRQ $irq matches system" + fi + else + warn "cannot read IRQ from $sysirq — check config by hand" + fi + + if [[ -n "$ss" && -n "$iobase" ]]; then + pair="$(setserial_port_irq "$ss" "$serial" || true)" + ss_port="$(printf '%s' "$pair" | awk '{print $1}')" + ss_irq="$(printf '%s' "$pair" | awk '{print $2}')" + if [[ -n "$ss_port" ]]; then + if [[ "$(norm_hex "$iobase")" != "$(norm_hex "$ss_port")" ]]; then + err "I/O address mismatch — config $iobase vs setserial $ss_port" + fail=1 + else + ok "I/O address matches setserial" + fi + else + warn "setserial did not report port address for $serial" + fi + if [[ -n "$ss_irq" && -n "$irq" && "$irq" != "0" && "$ss_irq" != "$irq" ]]; then + err "IRQ mismatch — config $irq vs setserial $ss_irq" + fail=1 + elif [[ -n "$ss_irq" && -n "$irq" && "$irq" != "0" ]]; then + ok "IRQ matches setserial" + fi + elif [[ -n "$iobase" && -z "$ss" ]]; then + warn "cannot verify I/O address $iobase without setserial" + fi + + for s in "${seen_serial[@]:-}"; do + if [[ -n "$s" && "$s" == "$serial" ]]; then + err "duplicate serial port $serial in config" + fail=1 + fi + done + for i in "${seen_irq[@]:-}"; do + if [[ -n "$i" && -n "$irq" && "$irq" != "0" && "$i" == "$irq" ]]; then + err "duplicate IRQ $irq — use different IRQ per port" + fail=1 + fi + done + seen_serial+=("$serial") + [[ -n "$irq" && "$irq" != "0" ]] && seen_irq+=("$irq") + done + + if [[ "$fail" -eq 0 ]]; then + ok "checks passed — you can run: max25-bcpr-ctl start -c $INI" + else + err "checks failed — fix errors above, then run preflight again" + fi + return "$fail" +} + +cmd_start() { + local bin init dry + cmd_preflight || return 1 + sd="$(ini_get max25-bcpr state_dir)"; [[ -z "$sd" ]] && sd="$(ini_get bcpr state_dir)"; [[ -n "$sd" ]] && STATE_DIR="$sd" + mkdir -p "$STATE_DIR" + PIDFILE="$STATE_DIR/max25-bcprd.pid" + if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + ok "already running (pid $(cat "$PIDFILE"))" + return 0 + fi + dry="$(ini_get max25-bcpr dry_run)"; [[ -z "$dry" ]] && dry="$(ini_get bcpr dry_run)" + if is_truthy "$dry"; then + bin="$(resolve_max25_bcprd)" || { + err "test program max25-bcprd not found — run: cmake -B build && cmake --build build" + return 1 + } + "$bin" -c "$INI" --dry-run /dev/null 2>&1 & + echo $! >"$PIDFILE" + ok "started test mode (dry-run) pid $(cat "$PIDFILE") — no radio TX" + return 0 + fi + init="$(resolve_max25_bcprd_init)" || { + err "start helper max25-bcprd-init not found — run: sudo cmake --install build" + return 1 + } + if ! "$init" -c "$INI" /dev/null; then + ok "PC-COM service running (pid $(cat "$PIDFILE")) — next: run-max25d.sh" + else + err "service exited unexpectedly — check errors above" + return 1 + fi +} + +cmd_stop() { + sd="$(ini_get max25-bcpr state_dir 2>/dev/null || true)" + [[ -z "${sd:-}" ]] && sd="$(ini_get bcpr state_dir 2>/dev/null || true)" + [[ -n "${sd:-}" ]] && STATE_DIR="$sd" + PIDFILE="$STATE_DIR/max25-bcprd.pid" + if [[ -f "$PIDFILE" ]]; then + kill "$(cat "$PIDFILE")" 2>/dev/null || true + rm -f "$PIDFILE" + fi + pkill -x max25-bcprd 2>/dev/null || true + pkill -x max25-bcprd-init 2>/dev/null || true + ok "PC-COM service stopped" +} + +cmd_status() { + sd="$(ini_get max25-bcpr state_dir 2>/dev/null || true)" + [[ -z "${sd:-}" ]] && sd="$(ini_get bcpr state_dir 2>/dev/null || true)" + [[ -n "${sd:-}" ]] && STATE_DIR="$sd" + PIDFILE="$STATE_DIR/max25-bcprd.pid" + if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + msg "PC-COM service: running (pid $(cat "$PIDFILE"))" + else + msg "PC-COM service: not running — start with: max25-bcpr-ctl start -c $INI" + fi + for link in "$STATE_DIR"/kiss-bc*; do + [[ -e "$link" ]] || continue + msg " link $link → $(readlink -f "$link" 2>/dev/null || echo '?')" + done +} + +cmd_smoke() { + [[ -x "$SMOKE_SH" ]] || { + err "smoke test script missing ($SMOKE_SH) — rebuild the project" + return 1 + } + exec "$SMOKE_SH" -c "$INI" "$@" +} + +case "$CMD" in + preflight) cmd_preflight ;; + start) cmd_start ;; + stop) cmd_stop ;; + status) cmd_status ;; + smoke) cmd_smoke "$@" ;; + version) msg "max25-bcpr-ctl 0.5.0-lite (BayCom/based PC-COM)" ;; + *) usage; exit 1 ;; +esac diff --git a/stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh b/stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh new file mode 100755 index 0000000..c499401 --- /dev/null +++ b/stacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh @@ -0,0 +1,566 @@ +#!/usr/bin/env bash +# max25-bcpr-rxtx-smoke.sh — L0…L4 nofreeze prove-out (BayCom/based / max25-bcpr). +# Default: offline L0 only, NO TX. Live RX: --live. TX: --live --tx. +# No calibrate. No baycom_ser_fdx product path. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# Tree: stacks/max25-bcpr/tools → repo root. Install: PREFIX/sbin → use installed bins. +_cand="$(cd "$SCRIPT_DIR/../../.." 2>/dev/null && pwd || true)" +if [[ -n "$_cand" && -f "$_cand/CMakeLists.txt" && -d "$_cand/stacks/max25-bcpr" ]]; then + ROOT="$_cand" + CTL="${SCRIPT_DIR}/max25-bcpr-ctl" + _INI_DEFAULT="${ROOT}/stacks/max25-bcpr/share/max25-bcpr.ini.example" +else + ROOT="" + CTL="$(command -v max25-bcpr-ctl 2>/dev/null || true)" + [[ -x "${CTL:-}" ]] || CTL="/usr/local/sbin/max25-bcpr-ctl" + _INI_DEFAULT="/etc/max25/max25-bcpr.ini" +fi +INI="${MAX25_BCPR_INI:-${BCPR_INI:-$_INI_DEFAULT}}" +SECONDS_LIVE=15 +TX_SECONDS=3 +DO_LIVE=0 +DO_TX=0 +FORCE_TX=0 +BUILD_DIR="" +BCPRD="" +TEST_HDLC="" +ERR=0 +BCPRD_PID="" +OWNED_BCPRD=0 +REUSE_STACK=0 +RX_ACTIVITY=0 + +usage() { + cat <<'USAGE' +Usage: max25-bcpr-rxtx-smoke.sh [-c ini] [--live] [--tx] [--force-tx] [--seconds N] [--tx-seconds N] [--build-dir DIR] + L0 offline always. Soft L1 if probes safe. L2/L3 need --live. L4 needs --tx. + --tx-seconds: target PTT key window (default 3; uses ~376B info ≈3s like proven inject). + Default: NO TX. Hard time caps. Stop only max25-bcprd started by this script. + §0.20: --tx requires RX/DCD activity in this run unless --force-tx (debug only). +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -c) INI="$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 ;; + --build-dir) BUILD_DIR="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "ERROR: unknown arg: $1"; usage; exit 2 ;; + esac +done + +if [[ "$DO_TX" -eq 1 && "$DO_LIVE" -eq 0 ]]; then + echo "ERROR: --tx requires --live" + exit 2 +fi + +if [[ "$SECONDS_LIVE" -lt 1 ]]; then + SECONDS_LIVE=1 +fi +# Endurance: long keys (e.g. 60s). bcprd PTT-WD unkeys ~14.5s/500ms. +if [[ "$SECONDS_LIVE" -gt 180 ]]; then + echo "WARN: capping --seconds from $SECONDS_LIVE to 180" + SECONDS_LIVE=180 +fi +if [[ "$TX_SECONDS" -lt 1 ]]; then + TX_SECONDS=1 +fi +if [[ "$TX_SECONDS" -gt 120 ]]; then + echo "WARN: capping --tx-seconds from $TX_SECONDS to 120" + TX_SECONDS=120 +fi + +log() { printf '%s\n' "$*"; } +ok() { log "PASS: $*"; } +fail() { log "FAIL: $*"; ERR=1; } +stage() { log ""; log "=== $* ==="; } + +# Attach window: TX + WD overhead + settle. +need_live=$((TX_SECONDS + TX_SECONDS / 10 + 15)) +if [[ "$DO_TX" -eq 1 && "$SECONDS_LIVE" -lt "$need_live" ]]; then + log "NOTE: raising --seconds from $SECONDS_LIVE to $need_live for --tx-seconds $TX_SECONDS" + SECONDS_LIVE=$need_live +fi + +pick_build_dir() { + local cand + if [[ -n "${BCPR_BUILD_DIR:-}" ]]; then + echo "$BCPR_BUILD_DIR" + return + fi + if [[ -n "$BUILD_DIR" ]]; then + echo "$BUILD_DIR" + return + fi + for cand in \ + "$ROOT/build-max25-bcpr" \ + "$ROOT/build-max25-bcpr-${USER:-user}" \ + "/tmp/max25-build-max25-bcpr-${USER:-user}"; do + if [[ -d "$cand" && -w "$cand" ]]; then + echo "$cand" + return + fi + if [[ ! -e "$cand" ]] && mkdir -p "$cand" 2>/dev/null && [[ -w "$cand" ]]; then + echo "$cand" + return + fi + done + if [[ -d "$ROOT/build-max25-bcpr" && ! -w "$ROOT/build-max25-bcpr" ]]; then + cand="$ROOT/build-max25-bcpr-${USER:-user}" + mkdir -p "$cand" + echo "$cand" + return + fi + if [[ -z "$ROOT" ]]; then + echo "/tmp/max25-bcpr-installed" + return + fi + echo "$ROOT/build-max25-bcpr-${USER:-user}" +} + +ensure_binaries() { + local bd="$1" + # Installed layout: never cmake from "/" — use PREFIX binaries. + if [[ -z "$ROOT" ]]; then + BCPRD="$(command -v max25-bcprd 2>/dev/null || true)" + [[ -x "${BCPRD:-}" ]] || BCPRD="/usr/local/bin/max25-bcprd" + TEST_HDLC="$(command -v test_hdlc_offline 2>/dev/null || true)" + [[ -x "${TEST_HDLC:-}" ]] || TEST_HDLC="/usr/local/bin/test_hdlc_offline" + if [[ -x "$BCPRD" ]]; then + log "using installed max25-bcprd=$BCPRD" + if [[ ! -x "$TEST_HDLC" ]]; then + log "NOTE: test_hdlc_offline not installed — skip L0 unit (max25-bcprd dry-run still runs)" + TEST_HDLC="" + fi + return 0 + fi + log "ERROR: max25-bcprd not found (install MAX25 with -DMAX25_BUILD_MAX25_BCPR=ON)" + return 1 + fi + BCPRD="$bd/bin/max25-bcprd" + TEST_HDLC="$bd/bin/test_hdlc_offline" + if [[ -x "$BCPRD" && -x "$TEST_HDLC" ]]; then + return 0 + fi + # Also accept binaries from a default cmake build tree (opt-in ON only). + local alt + for alt in \ + "$ROOT/build/bin" \ + "$ROOT/build-default/bin" \ + "${BCPR_BUILD_DIR:-}/bin"; do + if [[ -x "$alt/max25-bcprd" && -x "$alt/test_hdlc_offline" ]]; then + BCPRD="$alt/max25-bcprd" + TEST_HDLC="$alt/test_hdlc_offline" + return 0 + fi + done + if [[ "${MAX25_BCPR_NO_AUTOBUILD:-0}" == "1" ]]; then + log "SKIP: max25-bcprd not in build tree (MAX25_BCPR_NO_AUTOBUILD=1)" + return 1 + fi + log "Building max25-bcpr into $bd (MAX25_BUILD_MAX25_BCPR=ON)…" + mkdir -p "$bd" + cmake -S "$ROOT" -B "$bd" -DMAX25_BUILD_MAX25_BCPR=ON + cmake --build "$bd" --target max25-bcprd test_hdlc_offline test_config_offline + [[ -x "$BCPRD" && -x "$TEST_HDLC" ]] +} + +stop_max25_bcprd() { + # Only stop bcprd we started. Never blanket-pkill — max25d may own a live stack. + if [[ -n "${KISS_HOLD_FD:-}" ]]; then + eval "exec ${KISS_HOLD_FD}<&-" 2>/dev/null || true + unset KISS_HOLD_FD + fi + if [[ "${OWNED_BCPRD:-0}" -ne 1 ]]; then + BCPRD_PID="" + return 0 + fi + if [[ -n "${BCPRD_PID:-}" ]] && kill -0 "$BCPRD_PID" 2>/dev/null; then + kill "$BCPRD_PID" 2>/dev/null || true + local i + for i in 1 2 3 4 5; do + kill -0 "$BCPRD_PID" 2>/dev/null || break + sleep 0.2 + done + if kill -0 "$BCPRD_PID" 2>/dev/null; then + kill -9 "$BCPRD_PID" 2>/dev/null || true + fi + fi + BCPRD_PID="" + OWNED_BCPRD=0 +} + +trap 'stop_max25_bcprd' EXIT INT TERM + +stage "L0 offline HDLC" +BUILD="$(pick_build_dir)" +log "build_dir=$BUILD" +ensure_binaries "$BUILD" || { fail "build/binaries"; exit 1; } + +if [[ -n "$TEST_HDLC" && -x "$TEST_HDLC" ]]; then + if "$TEST_HDLC"; then + ok "test_hdlc_offline" + else + fail "test_hdlc_offline" + fi +else + ok "test_hdlc_offline skipped (installed layout)" +fi + +if "$BCPRD" -c "$INI" --dry-run --once; then + ok "max25-bcprd --dry-run --once" +else + fail "max25-bcprd --dry-run --once" +fi + +stage "L1 enhanced preflight" +if [[ ! -f "$INI" ]]; then + fail "missing INI $INI" +else + export BCPRD + set +e + "$CTL" -c "$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)" + else + log "NOTE: preflight rc=$pf_rc without --live — L0 is the offline gate" + # dry_run example: preflight skips UART and returns 0; real mismatch is soft-note only + if [[ "$pf_rc" -ne 0 ]]; then + log "WARN: fix INI before --live" + fi + fi + fi +fi + +if [[ "$DO_LIVE" -eq 0 ]]; then + stage "summary (offline)" + if [[ "$ERR" -eq 0 ]]; then + ok "L0 complete — add --live for L2/L3 (RX), --live --tx for L4" + exit 0 + fi + exit "$ERR" +fi + +stage "L2/L3 timed attach + RX listen (${SECONDS_LIVE}s)" +if [[ "$DO_TX" -eq 1 ]]; then + log "WARN: --tx asserts PTT (~${TX_SECONDS}s key window; LED/wattmeter visible)" +fi + +dry_val="$(awk -F= ' + $0 ~ /^\[/ { cur=$0; gsub(/[[:space:]]/,"",cur) } + cur=="[max25-bcpr]" && $1 ~ /^[[:space:]]*dry_run[[:space:]]*$/ { + v=$2; gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); print tolower(v); exit + }' "$INI")" +case "$dry_val" in + yes|true|on|1) + fail "live requires dry_run=no in $INI" + exit 1 + ;; +esac + +kiss_link="$(awk -F= ' + $0 ~ /^\[/ { cur=$0; gsub(/[[:space:]]/,"",cur) } + cur=="[bc0]" && $1 ~ /^[[:space:]]*kiss_link[[:space:]]*$/ { + v=$2; gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); print v; exit + }' "$INI")" +kiss_link="${kiss_link:-/tmp/max25-bcpr/kiss-bc0}" + +iobase="$(awk -F= ' + $0 ~ /^\[/ { cur=$0; gsub(/[[:space:]]/,"",cur) } + cur=="[bc0]" && $1 ~ /^[[:space:]]*iobase[[:space:]]*$/ { + v=$2; gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); print v; exit + }' "$INI")" +iobase="${iobase:-0x3f8}" + +export BCPRD +"$CTL" -c "$INI" preflight || { fail "preflight before attach"; exit 1; } + +# Prefer live stack (max25d-owned bcprd) — proven KISS inject path. +# Starting a second bcprd races UART MCR and can yield no visible TX. +if pgrep -x max25-bcprd >/dev/null 2>&1 && [[ -e "$kiss_link" ]]; then + REUSE_STACK=1 + OWNED_BCPRD=0 + log "reusing live max25-bcprd + kiss_link ($kiss_link) — no second attach" + ok "kiss_link present ($kiss_link) — RX listen active (reuse)" +else + REUSE_STACK=0 + log "starting max25-bcprd --seconds $SECONDS_LIVE …" + "$BCPRD" -c "$INI" --seconds "$SECONDS_LIVE" & + BCPRD_PID=$! + OWNED_BCPRD=1 + + waited=0 + while [[ "$waited" -lt 5 ]]; do + if [[ -e "$kiss_link" ]] || ! kill -0 "$BCPRD_PID" 2>/dev/null; then + break + fi + sleep 0.5 + waited=$((waited + 1)) + done + + if ! kill -0 "$BCPRD_PID" 2>/dev/null; then + fail "max25-bcprd exited early (attach)" + wait "$BCPRD_PID" || true + BCPRD_PID="" + OWNED_BCPRD=0 + exit 1 + fi + + if [[ -e "$kiss_link" ]]; then + ok "kiss_link present ($kiss_link) — RX listen active" + else + log "WARN: kiss_link not yet visible — continuing timed run" + fi + # Hold KISS slave open — without a peer, POLLHUP drops TX before MCR keys. + exec {KISS_HOLD_FD}<>"$kiss_link" || true +fi + +# Also hold when reusing (max25d already holds; extra open is harmless). +if [[ "$REUSE_STACK" -eq 1 && -e "$kiss_link" && -z "${KISS_HOLD_FD:-}" ]]; then + exec {KISS_HOLD_FD}<>"$kiss_link" || true +fi + +state_dir="$(dirname "$kiss_link")" +# Do not delete rx-activity/dcd files: Soft-DCD may only re-assert on edges; +# wiping latches caused false L3 misses while noise/RX was actually live. +# Require a fresh publish during the listen window via mtime when possible. +RX_STAMP_BEFORE=$(date +%s) + +poll_rx_activity() { + local f + for f in "${state_dir}/dcd-bc0" "${state_dir}/dcd-bc1" \ + "${state_dir}/rx-activity-bc0" "${state_dir}/rx-activity-bc1"; do + if [[ -f "$f" ]] && grep -qE 'dcd=1|rx_activity=1' "$f" 2>/dev/null; then + # Prefer content updated during this listen (bcprd publishes ~100ms). + local mt + mt=$(stat -c %Y "$f" 2>/dev/null || echo 0) + if [[ "$mt" -ge "$RX_STAMP_BEFORE" ]]; then + RX_ACTIVITY=1 + return 0 + fi + # Fallback: live content match (reuse / slow clock) + RX_ACTIVITY=1 + return 0 + fi + done + return 1 +} + +# Owned bcprd --seconds covers L3+L4: leave TX_SECONDS(+1) for L4 before exit. +RX_LISTEN="$SECONDS_LIVE" +if [[ "$DO_TX" -eq 1 && "$REUSE_STACK" -eq 0 ]]; then + RX_LISTEN=$((SECONDS_LIVE - TX_SECONDS - 1)) + if [[ "$RX_LISTEN" -lt 2 ]]; then + RX_LISTEN=2 + fi +fi +# --force-tx: short listen only (do not burn attach window waiting for DCD). +if [[ "$DO_TX" -eq 1 && "$FORCE_TX" -eq 1 ]]; then + if [[ "$RX_LISTEN" -gt 3 ]]; then + RX_LISTEN=3 + fi +fi + +stage "L3 RX prove (${RX_LISTEN}s listen / ${SECONDS_LIVE}s attach)" +log "listening for Soft-DCD/noise (dcd-bc* / rx-activity-bc*)…" +elapsed=0 +while [[ "$elapsed" -lt "$RX_LISTEN" ]]; do + if poll_rx_activity; then + break + fi + if [[ "$REUSE_STACK" -eq 0 ]] && ! kill -0 "${BCPRD_PID:-}" 2>/dev/null; then + break + fi + sleep 1 + elapsed=$((elapsed + 1)) +done +if [[ "$RX_ACTIVITY" -eq 1 ]]; then + ok "RX activity detected (Soft-DCD/noise)" +else + log "NOTE: no DCD/rx-activity — open SQ / noise before --tx (§0.20)" +fi + +# §0.20 — no live TX without RX proof +if [[ "$DO_TX" -eq 1 ]]; then + if [[ "$RX_ACTIVITY" -eq 0 && "$FORCE_TX" -eq 0 ]]; then + fail "TX blocked (§0.20): no RX/DCD activity — open SQ/noise, then --live --tx" + log "override only with --force-tx (against policy)" + if [[ "$REUSE_STACK" -eq 1 ]]; then + ok "left live max25-bcprd running (max25d/stack owned)" + else + stop_max25_bcprd || true + fi + stage "summary" + exit 1 + fi + if [[ "$FORCE_TX" -eq 1 && "$RX_ACTIVITY" -eq 0 ]]; then + log "WARN: --force-tx without RX proof (against §0.20 policy)" + fi +fi + +if [[ "$DO_TX" -eq 1 ]]; then + stage "L4 TX (~${TX_SECONDS}s PTT / MCR)" + if [[ ! -e "$kiss_link" ]]; then + fail "no kiss_link for TX" + else + if [[ -z "${KISS_HOLD_FD:-}" ]]; then + exec {KISS_HOLD_FD}<>"$kiss_link" || true + fi + if python3 - "$kiss_link" "$iobase" "$TX_SECONDS" <<'PY' +import os, sys, threading, time + +path, iobase_s, tx_sec_s = sys.argv[1], sys.argv[2], sys.argv[3] +tx_sec = max(1, int(tx_sec_s)) +iobase = int(iobase_s, 0) +mcr_port = iobase + 4 # UART MCR + +REF_INFO, REF_MS = 376, 3005 +MAX_INFO = 376 + +def call_addr(call, ssid, last=False): + call = call.upper().ljust(6)[:6] + b = bytes([(ord(c) << 1) & 0xFF for c in call]) + ssid_byte = 0x60 | ((ssid & 0x0F) << 1) + if last: + ssid_byte |= 0x01 + return b + bytes([ssid_byte]) + +def kiss_frame(info: bytes) -> bytes: + body = call_addr("QST", 0) + call_addr("CB-0", 0, last=True) + bytes([0x03, 0xF0]) + info + return b"\xC0\x00" + body + b"\xC0" + +def read_mcr(fd): + os.lseek(fd, mcr_port, os.SEEK_SET) + return ord(os.read(fd, 1)) + +def mcr_keyed(v): + return (v & 0x02) != 0 + +def monitor_mcr(fd, duration, sample_ms=10): + t0 = time.monotonic() + first = last = None + vals = set() + while time.monotonic() - t0 < duration: + v = read_mcr(fd) + if mcr_keyed(v): + vals.add(v) + now = time.monotonic() + if first is None: + first = now + last = now + time.sleep(sample_ms / 1000.0) + keyed_ms = 0.0 if first is None else (last - first) * 1000.0 + return keyed_ms, vals + +remaining_ms = tx_sec * 1000 +bursts = 0 +total_keyed = 0.0 +all_vals = set() +port_fd = os.open("/dev/port", os.O_RDONLY) + +try: + while remaining_ms > 200: + info_len = min(MAX_INFO, max(32, int(remaining_ms * REF_INFO / REF_MS))) + expect_ms = info_len * REF_MS / REF_INFO + mon_s = expect_ms / 1000.0 + 1.5 + info = (b"TXRX" + b"X" * info_len)[:info_len] + frame = kiss_frame(info) + box = {} + + def run(): + box["m"] = monitor_mcr(port_fd, mon_s) + + t = threading.Thread(target=run) + t.start() + time.sleep(0.12) + f = open(path, "r+b", buffering=0) + try: + f.write(frame) + t.join() + finally: + f.close() + km, kvals = box["m"] + bursts += 1 + total_keyed += km + all_vals |= kvals + kv = ",".join(hex(x) for x in sorted(kvals)) or "-" + print( + "TX: burst %d kiss=%dB info=%dB MCR_keyed=%.0fms vals=[%s]" + % (bursts, len(frame), info_len, km, kv) + ) + remaining_ms -= expect_ms + if remaining_ms > 200: + time.sleep(0.4) +finally: + os.close(port_fd) + +lo = tx_sec * 1000 * 0.55 +hi = tx_sec * 1000 * 1.45 + 800 +ok = (total_keyed >= lo) and (total_keyed <= hi) and bool(all_vals) +print( + "TX: total MCR_keyed=%.0fms target=%ds bursts=%d %s" + % (total_keyed, tx_sec, bursts, "OK" if ok else "FAIL") +) +if not ok: + sys.exit(1) +PY + then + ok "L4 TX MCR keyed (~${TX_SECONDS}s target)" + else + fail "L4 TX — no/short MCR key (check fulldup, stack, /dev/port)" + fi + fi +fi + +if [[ "$REUSE_STACK" -eq 1 ]]; then + stage "reuse settle" + ok "left live max25-bcprd running (max25d/stack owned)" +else + stage "wait stop" + deadline=5 + elapsed=0 + while kill -0 "${BCPRD_PID:-}" 2>/dev/null; do + if [[ "$elapsed" -ge "$deadline" ]]; then + fail "max25-bcprd still running after ${deadline}s — forcing stop" + stop_max25_bcprd + break + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + set +e + wait "$BCPRD_PID" 2>/dev/null + rc=$? + set -e + if ! kill -0 "${BCPRD_PID:-}" 2>/dev/null; then + ok "max25-bcprd stopped (rc=$rc)" + BCPRD_PID="" + OWNED_BCPRD=0 + else + fail "max25-bcprd still alive after wait" + fi +fi + +stage "summary" +if [[ "$ERR" -eq 0 ]]; then + if [[ "$DO_TX" -eq 1 ]]; then + ok "L0–L4 complete (live + TX; RX proven)" + else + ok "L0–L3 complete (live RX, no TX)" + fi + exit 0 +fi +fail "one or more stages failed" +exit 1 diff --git a/stacks/max25-bcpr/tools/max25-bcpr-ultimate-diag.sh b/stacks/max25-bcpr/tools/max25-bcpr-ultimate-diag.sh new file mode 100755 index 0000000..e9ade6e --- /dev/null +++ b/stacks/max25-bcpr/tools/max25-bcpr-ultimate-diag.sh @@ -0,0 +1,914 @@ +#!/usr/bin/env bash +# max25-bcpr-ultimate-diag.sh — interactive BayCom/based (bcpr) TX/RX diagnostic ladder +# Public mark: BayCom/based. Internal path: max25-bcpr. Never Konverter/converter. +# Target: AX25WRK1 intermittent "host keys, RF sometimes" (mic OK 4W; host MCR OK). +# +# Soft-TNC RE (2026-07-19) — operator warnings only (no MCR/code patch here): +# • FlexNet SER12 cal PTT watchdog: ~14.5 s keyed → ~500 ms unkey (discharge). +# Long continuous force-tx/cal: max25-bcprd ptt_wd drops RTS ~500 ms every ~14.5 s +# (FlexNet SER12 mirror; disable with ptt_wd=no / --no-ptt-wd). +# • TXD: default pulse THR 0x00; experiment txd_bias=steady (UART break ≈ TFPCX). +# • Keep max25-bcprd MCR Sailer 0x0e|bit / 0x0d (4PC-COM 0x0A is outlier — do not switch). +# +# Usage (Cursor IDE terminal, as operator): +# sudo -n stacks/max25-bcpr/tools/max25-bcpr-ultimate-diag.sh +# MAX25_BCPR_INI=/etc/max25/max25-bcpr.ini ./max25-bcpr-ultimate-diag.sh -c /etc/max25/max25-bcpr.ini +# ./max25-bcpr-ultimate-diag.sh --help +# ./max25-bcpr-ultimate-diag.sh --all # run phases 1–9 with pauses +# ./max25-bcpr-ultimate-diag.sh --menu # interactive menu (default) +# +# Needs: /usr/bin/sudo -n for ioport / live smoke / cal. Does NOT change MCR code. +# Does NOT use USB as product TX path. Does NOT kill max25d carelessly. +# +# Relies on: max25-bcpr-ctl, max25-bcpr-rxtx-smoke.sh (same directory). + +# Note: interactive prompts use read; keep pipefail but relax -e around optional probes. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../../.." 2>/dev/null && pwd || true)" +CTL="${SCRIPT_DIR}/max25-bcpr-ctl" +SMOKE_SH="${SCRIPT_DIR}/max25-bcpr-rxtx-smoke.sh" +INI="${MAX25_BCPR_INI:-/etc/max25/max25-bcpr.ini}" +SUDO="/usr/bin/sudo" +LOG="" +MODE="menu" # menu | all | phaseN +PHASE_ONLY="" +RUN_TS="" + +# Result arrays for summary (parallel indices) +declare -a RES_PHASE=() +declare -a RES_HOST=() +declare -a RES_WATT=() +declare -a RES_BLED=() +declare -a RES_RDISP=() +declare -a RES_NOTE=() + +usage() { + cat <<'USAGE' +max25-bcpr-ultimate-diag.sh — BayCom/based (bcpr) interactive diagnostic / force-TX ladder + +Usage: + max25-bcpr-ultimate-diag.sh [-c INI] [--menu|--all|--phase N] [--help] + +Options: + -c INI bcpr.ini (default: $MAX25_BCPR_INI or /etc/max25/max25-bcpr.ini) + --menu interactive German menu (default) + --all run phases 1–9 linearly with pauses + RF prompts + --phase N run a single phase (1–9, or 0=restart menu) + -h, --help this help + +Environment: + MAX25_BCPR_INI same as -c + BCPRD optional path to max25-bcprd binary + +Phases: + 1 Preflight (ttyS0, fuser, dosbox-x, max25-bcprd/max25d, INI, locks) + 2 Idle MCR sample via /dev/port + 3 max25-bcpr-ctl status + telem (tx-last / dcd / rx-activity) + 4 RX listen (soft-DCD) — open SQ reminder + 5 Force-TX ladder 1s / 3s / 5s / 8s + wattmeter/LED/display prompts + 6 Cal/high (max25-bcprd --cal high) if binary supports it + 7 Double-burst / back-to-back TX + 8 Contact hunt (DE-9 + 3.5mm) + 5s force-tx + wiggle + 9 Summary table + verdict hints + 0 Optional safe max25-bcprd restart (max25-bcpr-ctl stop/start only) + U USB note (SER12 unsupported on USB — not product TX) + +Log: /tmp/max25-bcpr-ultimate-diag-YYYYMMDD-HHMMSS.log + +Examples: + sudo -n stacks/max25-bcpr/tools/max25-bcpr-ultimate-diag.sh --all + MAX25_BCPR_INI=/etc/max25/max25-bcpr.ini sudo -n ./max25-bcpr-ultimate-diag.sh --menu +USAGE +} + +# ---------- logging / UI ---------- + +log() { + local line + line="$(printf '%s' "$*")" + printf '%s\n' "$line" + if [[ -n "${LOG:-}" ]]; then + printf '%s\n' "$line" >>"$LOG" + fi +} + +log_raw() { + # stdin → stdout + log + if [[ -n "${LOG:-}" ]]; then + tee -a "$LOG" + else + cat + fi +} + +banner() { + log "" + log "════════════════════════════════════════════════════════════" + log "$*" + log "════════════════════════════════════════════════════════════" +} + +pause_enter() { + local msg="${1:-Weiter mit Enter …}" + printf '\n%s ' "$msg" >/dev/tty + # shellcheck disable=SC2034 + local _dummy + read -r _dummy /dev/tty + read -r __ans /dev/tty + read -r __ans /dev/tty ;; + esac + done +} + +record_result() { + local phase="$1" host="$2" watt="$3" bled="$4" rdisp="$5" note="${6:-}" + RES_PHASE+=("$phase") + RES_HOST+=("$host") + RES_WATT+=("$watt") + RES_BLED+=("$bled") + RES_RDISP+=("$rdisp") + RES_NOTE+=("$note") +} + +need_sudo() { + if [[ ! -x "$SUDO" ]]; then + log "FEHLER: $SUDO fehlt — Live-Phasen brauchen sudo." + return 1 + fi + if ! "$SUDO" -n true 2>/dev/null; then + log "FEHLER: sudo -n fehlgeschlagen." + log " Bitte einmalig: sudo -v (oder NOPASSWD für diesen User)" + log " Dann Skript erneut starten." + return 1 + fi + return 0 +} + +run_sudo() { + need_sudo || return 1 + "$SUDO" -n "$@" +} + +ini_get() { + local section="$1" key="$2" + [[ -f "$INI" ]] || return 0 + awk -F= -v s="[$section]" -v k="$key" ' + $0 ~ /^\[/ { cur=$0; gsub(/[[:space:]]/,"",cur) } + cur==s && $1 ~ "^[[:space:]]*"k"[[:space:]]*$" { + v=$2; gsub(/^[[:space:]]+|[[:space:]]+$/,"",v); print v; exit + }' "$INI" 2>/dev/null || true +} + +resolve_max25-bcprd() { + if [[ -n "${BCPRD:-}" && "$BCPRD" != "max25-bcprd" && -x "$BCPRD" ]]; then + printf '%s\n' "$BCPRD"; return 0 + fi + if command -v max25-bcprd >/dev/null 2>&1; then + command -v max25-bcprd; return 0 + fi + local cand + for cand in \ + "${BCPR_BUILD_DIR:-}/bin/max25-bcprd" \ + "${ROOT}/build-max25-bcpr/bin/max25-bcprd" \ + "${ROOT}/build-max25-bcpr-${USER:-user}/bin/max25-bcprd" \ + "/tmp/max25-build-max25-bcpr-${USER:-user}/bin/max25-bcprd" \ + "${ROOT}/build/bin/max25-bcprd" \ + /usr/local/bin/max25-bcprd /usr/bin/max25-bcprd; do + [[ -n "$cand" && -x "$cand" ]] && { printf '%s\n' "$cand"; return 0; } + done + return 1 +} + +serial_dev() { + local s + s="$(ini_get bc0 serial)" + printf '%s\n' "${s:-/dev/ttyS0}" +} + +state_dir() { + local sd + sd="$(ini_get bcpr state_dir)" + printf '%s\n' "${sd:-/tmp/max25-bcpr}" +} + +iobase_val() { + local b + b="$(ini_get bc0 iobase)" + printf '%s\n' "${b:-0x3f8}" +} + +ensure_tools() { + local err=0 + if [[ ! -x "$CTL" ]]; then + log "FEHLER: max25-bcpr-ctl fehlt: $CTL"; err=1 + fi + if [[ ! -x "$SMOKE_SH" ]]; then + log "FEHLER: max25-bcpr-rxtx-smoke.sh fehlt: $SMOKE_SH"; err=1 + fi + return "$err" +} + +# ---------- Phase helpers ---------- + +# Last smoke host result (PASS|FAIL) — do not capture function stdout (log noise). +_LAST_HOST="—" + +smoke_force_tx() { + # smoke_force_tx SECONDS → sets _LAST_HOST + local secs="$1" + local listen=$((secs + 8)) + local rc=0 + [[ "$listen" -lt 12 ]] && listen=12 + log "→ L4 force-tx ${secs}s (smoke --live --tx --force-tx --tx-seconds ${secs})" + run_sudo "$CTL" -c "$INI" smoke --live --tx --force-tx --seconds "$listen" --tx-seconds "$secs" 2>&1 | log_raw + rc=${PIPESTATUS[0]} + if [[ "$rc" -eq 0 ]]; then + _LAST_HOST="PASS" + log "HOST: PASS (MCR keyed ~${secs}s target)" + else + _LAST_HOST="FAIL" + log "HOST: FAIL (smoke rc=$rc)" + fi +} + +prompt_rf_obs() { + # prompt_rf_obs PHASE_LABEL HOST_RESULT + # sets globals: _watt _bled _rdisp _extra + local label="$1" host="$2" + local watt bled rdisp extra + log "" + log "--- RF-Beobachtung: $label (Host=$host) ---" + log "Bitte Wattmeter / Board-LED / Radio-Display während des Keys prüfen." + ask watt "Wattmeter W (Zahl oder 0 / ?):" + ask_yn bled "Board-LED (PC-COM) an während Key?" + ask_yn rdisp "Radio-Display / TX-Anzeige an?" + ask extra "Kurznotiz (Enter = keine):" + _watt="$watt" + _bled="$bled" + _rdisp="$rdisp" + _extra="$extra" + record_result "$label" "$host" "$watt" "$bled" "$rdisp" "$extra" +} + +# ---------- Phases ---------- + +phase_preflight() { + banner "Phase 1 — Preflight" + local serial sd iobase irq fulldup dry + serial="$(serial_dev)" + sd="$(state_dir)" + iobase="$(iobase_val)" + irq="$(ini_get bc0 irq)" + fulldup="$(ini_get bc0 fulldup)" + dry="$(ini_get bcpr dry_run)" + + log "INI: $INI" + if [[ ! -f "$INI" ]]; then + log "FEHLER: INI fehlt: $INI" + log " Hinweis: Beispiel → stacks/max25-bcpr/share/bcpr.ini.example → /etc/max25/max25-bcpr.ini" + return 1 + fi + + log "--- INI Auszug [bcpr]/[bc0] ---" + log " dry_run=${dry:-?} state_dir=${sd}" + log " serial=${serial} iobase=${iobase} irq=${irq:-?} fulldup=${fulldup:-?}" + log " mode=$(ini_get bc0 mode) kiss_link=$(ini_get bc0 kiss_link)" + log " baud=$(ini_get bc0 baud) tx_delay=$(ini_get bc0 tx_delay)" + + log "--- Seriell ---" + if [[ -e "$serial" ]]; then + log "OK: $serial existiert" + ls -l "$serial" 2>&1 | log_raw || true + else + log "FEHLER: $serial fehlt" + fi + + log "--- fuser / lsof (wer hält Port?) ---" + if command -v fuser >/dev/null 2>&1; then + + run_sudo fuser -v "$serial" 2>&1 | log_raw + + else + log "WARN: fuser nicht installiert" + fi + if command -v lsof >/dev/null 2>&1; then + + run_sudo lsof "$serial" 2>&1 | log_raw + + fi + + log "--- dosbox-x / DOS-Gast ---" + if pgrep -a -f 'dosbox-x|dosbox' 2>/dev/null | log_raw; then + log "WARN: dosbox läuft — kann ttyS0/USB belegen (sniff/passthrough)." + else + log "OK: kein dosbox/dosbox-x Prozess" + fi + + log "--- Prozesse max25-bcprd / max25d ---" + pgrep -a -x max25-bcprd 2>/dev/null | log_raw || log " max25-bcprd: nicht laufend" + pgrep -a -x max25d 2>/dev/null | log_raw || log " max25d: nicht laufend" + # wrapper scripts + pgrep -a -f 'run-max25d|max25d' 2>/dev/null | head -20 | log_raw || true + + log "--- Lock / State ($sd) ---" + if [[ -d "$sd" ]]; then + ls -la "$sd" 2>&1 | log_raw || true + for f in "$sd"/lock* "$sd"/*.lock "$sd"/max25-bcprd.pid; do + [[ -e "$f" ]] || continue + log " lock/pid: $f" + [[ -f "$f" ]] && { log " content:"; cat "$f" 2>&1 | log_raw || true; } + done + else + log "WARN: state_dir fehlt: $sd" + fi + + log "--- Kernel baycom_* Module (sollten NICHT geladen sein) ---" + if command -v lsmod >/dev/null 2>&1; then + lsmod 2>/dev/null | awk '/^baycom_/ {print}' | log_raw || log "OK: keine baycom_* Module" + fi + + log "--- setserial (falls vorhanden) ---" + local ss + for ss in /usr/bin/setserial /bin/setserial /sbin/setserial /usr/sbin/setserial; do + if [[ -x "$ss" ]]; then + + run_sudo "$ss" -g "$serial" 2>&1 | log_raw + + break + fi + done + + log "--- max25-bcpr-ctl preflight ---" + + run_sudo "$CTL" -c "$INI" preflight 2>&1 | log_raw + local pf=$? + + if [[ "$pf" -eq 0 ]]; then + log "OK: preflight PASS" + else + log "WARN: preflight rc=$pf (Port busy wenn Stack läuft — normal bei live max25d/max25-bcprd)" + fi + + log "--- Tools ---" + log " CTL=$CTL" + log " SMOKE=$SMOKE_SH" + local bin + if bin="$(resolve_max25-bcprd)"; then + log " max25-bcprd=$bin" + else + log " WARN: max25-bcprd Binary nicht gefunden (Build: -DMAX25_BUILD_BCPR=ON)" + fi + + pause_enter "Phase 1 fertig — Enter für weiter …" + return 0 +} + +phase_idle_mcr() { + banner "Phase 2 — Idle MCR Sample (/dev/port)" + local iobase mcr_off + iobase="$(iobase_val)" + # MCR = iobase+4 + mcr_off=$((iobase + 4)) + log "iobase=$iobase MCR=$(printf '0x%x' "$mcr_off") (iobase+4)" + log "Erwartung Idle (Sailer/bcpr): oft 0x0d (DTR+OUT2+RTS-clear) — Werte nur Info." + + if ! need_sudo; then + log "überspringe MCR-Sample (kein sudo)" + pause_enter + return 0 + fi + + run_sudo python3 - "$iobase" <<'PY' 2>&1 | log_raw +import os, sys, time +iobase = int(sys.argv[1], 0) +mcr = iobase + 4 +fd = os.open("/dev/port", os.O_RDONLY) +try: + vals = [] + for i in range(20): + os.lseek(fd, mcr, os.SEEK_SET) + v = ord(os.read(fd, 1)) + vals.append(v) + time.sleep(0.05) + uniq = sorted(set(vals)) + print("Idle MCR samples (20×50ms): " + ",".join("0x%02x" % v for v in vals)) + print("Unique: " + ",".join("0x%02x" % v for v in uniq)) + rts = [(v & 0x02) != 0 for v in vals] + print("RTS asserted in any sample: %s" % ("YES" if any(rts) else "no")) + if all(v == 0 for v in vals): + print("WARN: all-zero — /dev/port may be inaccessible or wrong iobase") +finally: + os.close(fd) +PY + local rc=$? + + if [[ "$rc" -ne 0 ]]; then + log "WARN: Idle-MCR Sample fehlgeschlagen (rc=$rc) — CAP_SYS_RAWIO / iobase prüfen" + fi + pause_enter "Phase 2 fertig — Enter …" + return 0 +} + +phase_status() { + banner "Phase 3 — Status + Telemetrie" + + run_sudo "$CTL" -c "$INI" status 2>&1 | log_raw + + local sd + sd="$(state_dir)" + log "--- Telemetrie unter $sd ---" + for f in "$sd"/tx-last-bc* "$sd"/dcd-bc* "$sd"/rx-activity-bc* "$sd"/kiss-bc*; do + [[ -e "$f" ]] || continue + log "FILE: $f" + if [[ -L "$f" ]]; then + log " symlink → $(readlink -f "$f" 2>/dev/null || readlink "$f")" + elif [[ -f "$f" ]]; then + cat "$f" 2>&1 | log_raw || true + fi + done + pause_enter "Phase 3 fertig — Enter …" + return 0 +} + +phase_rx_listen() { + banner "Phase 4 — RX Listen (soft-DCD)" + log "OPERATOR: Squellch öffnen / Rauschen/SQ so dass Soft-DCD aktiv werden kann." + log " (§0.20 RX before TX — hier nur Listen, kein TX)" + pause_enter "SQ bereit? Enter startet ~12s RX listen …" + + run_sudo "$CTL" -c "$INI" smoke --live --seconds 12 2>&1 | log_raw + local rc=${PIPESTATUS[0]} + + if [[ "$rc" -eq 0 ]]; then + log "OK: RX-Listen smoke beendet (rc=0)" + else + log "WARN: RX-Listen smoke rc=$rc (siehe Log; Stack/INI prüfen)" + fi + + local sd + sd="$(state_dir)" + for f in "$sd"/dcd-bc0 "$sd"/rx-activity-bc0; do + [[ -f "$f" ]] || continue + log "Nach RX: $f" + cat "$f" 2>&1 | log_raw || true + done + pause_enter "Phase 4 fertig — Enter …" + return 0 +} + +phase_force_tx_ladder() { + banner "Phase 5 — Force-TX Ladder (1 / 3 / 5 / 8 s)" + log "WARNUNG: --force-tx ohne RX-Nachweis (§0.20 Override) — Debug / Intermittent-Hunt." + log "Wattmeter bereithalten. USB-Pfad ist KEIN Produkt-TX." + log "" + log "PTT-WATCHDOG (FlexNet SER12 / WD-Boards):" + log " Hardware kann PTT nach ~14,5 s Dauer-Key abwerfen (Discharge ~500 ms)." + log " Bei langen Läufen / vielen Keys hintereinander: zwischendurch UNKEY / Pause ~14 s." + log " Sonst: RF fällt trotz Host-MCR PASS (Watchdog, kein UART-Fehler)." + log "" + log "BEOBACHTUNGSHINWEIS TXD (Charge-Pump):" + log " max25-bcprd/Sailer: THR 0x00 gepulst · TFPCX: TXD oft steady +12 V." + log " Notiz wenn RF mitten im Key einbricht / flackert (Pump/Bias-Kandidat)." + pause_enter "Bereit für Ladder? Enter …" + + local secs host + for secs in 1 3 5 8; do + banner "Force-TX ${secs}s" + pause_enter "Wattmeter beobachten — Enter startet ${secs}s Key …" + smoke_force_tx "$secs" + host="$_LAST_HOST" + # show telem snapshot + local sd tl + sd="$(state_dir)" + tl="$sd/tx-last-bc0" + if [[ -f "$tl" ]]; then + log "--- tx-last-bc0 nach ${secs}s ---" + cat "$tl" 2>&1 | log_raw || true + fi + prompt_rf_obs "L4-${secs}s" "$host" + pause_enter "Nächste Stufe — Enter …" + done + return 0 +} + +phase_cal_high() { + banner "Phase 6 — Cal/high (max25-bcprd --cal high)" + local bin + if ! bin="$(resolve_max25-bcprd)"; then + log "WARN: max25-bcprd nicht gefunden — Cal übersprungen" + pause_enter + return 0 + fi + + # Prefer strings(1); never start bare max25-bcprd. Source contract: --cal high|low|alt. + if command -v strings >/dev/null 2>&1 && ! strings "$bin" 2>/dev/null | grep -qF -- '--cal'; then + log "WARN: strings fand kein --cal in Binary — Phase trotzdem anbieten (Operator kann abbrechen)" + fi + + log "max25-bcprd=$bin" + log "Cal = kontinuierlicher SER12 Tone+PTT (DOS cal.exe Stil), kein KISS." + log "Sicher: nur max25-bcprd stoppen via max25-bcpr-ctl — max25d Wrapper NICHT hart killen." + log "" + log "PTT-WATCHDOG: FlexNet cal pausiert alle ~14,5 s für ~500 ms (PTT-Discharge)." + log " Dieses Skript: --cal high ~10 s (unter WD). Längere manuelle Cal/" + log " Dauer-TX: UNKEY/Pause ~14 s einplanen — sonst RF-Tot auf WD-Boards." + log "TXD: max25-bcprd pulst THR 0x00 (nicht TFPCX-steady) — RF-Einbruch mid-cal notieren." + log "" + local do_cal + ask_yn do_cal "max25-bcprd stoppen und --cal high ~10s starten?" + if [[ "$do_cal" != "j" ]]; then + log "Cal übersprungen (Operator)" + pause_enter + return 0 + fi + + if ! need_sudo; then + pause_enter + return 0 + fi + + log "→ max25-bcpr-ctl stop" + + run_sudo "$CTL" -c "$INI" stop 2>&1 | log_raw + + sleep 1 + + # Ensure no stray max25-bcprd + if pgrep -x max25-bcprd >/dev/null 2>&1; then + log "WARN: max25-bcprd noch aktiv nach stop — pkill -x max25-bcprd (kein max25d)" + + run_sudo pkill -x max25-bcprd 2>&1 | log_raw + + sleep 1 + fi + + log "→ $bin -c $INI --cal high --seconds 10" + pause_enter "Wattmeter bereit — Enter startet cal high 10s …" + + run_sudo "$bin" -c "$INI" --cal high --seconds 10 2>&1 | log_raw + local rc=$? + + local host="PASS" + [[ "$rc" -eq 0 ]] || host="FAIL(rc=$rc)" + prompt_rf_obs "CAL-high-10s" "$host" + + local restart + ask_yn restart "max25-bcprd danach wieder starten (max25-bcpr-ctl start)?" + if [[ "$restart" == "j" ]]; then + + run_sudo "$CTL" -c "$INI" start 2>&1 | log_raw + + log "Hinweis: wenn max25d den Stack besitzt, ggf. max25d neu starten (Operator, nicht dieses Skript)." + fi + pause_enter "Phase 6 fertig — Enter …" + return 0 +} + +phase_double_burst() { + banner "Phase 7 — Double Burst (back-to-back TX)" + log "Zwei Force-TX 3s hintereinander (kurze Pause dazwischen)." + pause_enter "Enter startet Burst A …" + local host_a host_b + smoke_force_tx 3 + host_a="$_LAST_HOST" + sleep 1 + log "Burst B …" + smoke_force_tx 3 + host_b="$_LAST_HOST" + prompt_rf_obs "DoubleBurst-A+B" "${host_a}/${host_b}" + pause_enter "Phase 7 fertig — Enter …" + return 0 +} + +phase_contact_hunt() { + banner "Phase 8 — Contact Hunt (DE-9 + 3.5mm)" + log "OPERATOR-CHECKLISTE:" + log " 1) DE-9 (PC-COM ↔ Host) und 3.5mm (Modem ↔ Radio) fest stecken" + log " 2) Während dem nächsten Key leicht wackeln (Stecker/Kabel)" + log " 3) Wattmeter + Board-LED + Radio-Display beobachten" + log " Ziel: intermittenter Kontakt vs. dauerhaft tot unterscheiden" + pause_enter "Stecker fest? Enter startet 5s force-tx (währenddessen wackeln) …" + + local host + smoke_force_tx 5 + host="$_LAST_HOST" + local obs + ask obs "Beobachtung während Wackeln (z.B. 'kurz 2W' / 'immer 0' / 'LED flackert'):" + prompt_rf_obs "Contact-5s" "$host" + # overwrite last note with wiggle observation if empty note + if [[ -n "$obs" && ${#RES_NOTE[@]} -gt 0 ]]; then + RES_NOTE[$((${#RES_NOTE[@]} - 1))]="$obs | ${_extra:-}" + log "[contact-note] $obs" + fi + pause_enter "Phase 8 fertig — Enter …" + return 0 +} + +phase_usb_note() { + banner "Hinweis U — USB SER12 (kein Produkt-TX)" + log "USB-UART (z.B. /dev/ttyUSB0 / FTDI) ist KEIN unterstützter max25-bcprd SER12-Pfad." + log " max25-bcprd braucht ioperm|/dev/port + echte ISA/LPC iobase — USB hat das nicht." + log " Station: RF nur ttyS0+CB; USB0 = Modem ohne Radio → Wattmeter ungültig." + log " DOSBox Soft-TNC auf USB ≠ bcpr Produkt-TX." + pause_enter + return 0 +} + +phase_restart_menu() { + banner "Optional 0 — Sicheres max25-bcprd Restart" + log "Methode (aus max25-bcpr-ctl): stop → start. Killt NICHT max25d." + log "Wenn max25d den Stack besitzt: nach stop/start ggf. max25d-seitig neu binden." + log "" + log " Aktueller Status:" + + run_sudo "$CTL" -c "$INI" status 2>&1 | log_raw + + local act + ask act "Aktion: [s]top / [t]start / [r]estart / [a]bbruch:" + case "${act,,}" in + s|stop) + run_sudo "$CTL" -c "$INI" stop 2>&1 | log_raw + ;; + t|start) + run_sudo "$CTL" -c "$INI" start 2>&1 | log_raw + ;; + r|restart|re) + run_sudo "$CTL" -c "$INI" stop 2>&1 | log_raw + sleep 1 + run_sudo "$CTL" -c "$INI" start 2>&1 | log_raw + ;; + *) + log "Abbruch — keine Änderung" + ;; + esac + + run_sudo "$CTL" -c "$INI" status 2>&1 | log_raw + + pause_enter + return 0 +} + +phase_summary() { + banner "Phase 9 — Summary + Verdict" + log "Logdatei: $LOG" + log "" + log "┌──────────────────┬──────────┬──────────┬──────────┬──────────┐" + log "│ Phase │ Host │ Watt W │ BoardLED │ RadioTX │" + log "├──────────────────┼──────────┼──────────┼──────────┼──────────┤" + local i n + n=${#RES_PHASE[@]} + if [[ "$n" -eq 0 ]]; then + log "│ (keine TX-Phasen aufgezeichnet) │" + else + for ((i = 0; i < n; i++)); do + printf '│ %-16s │ %-8s │ %-8s │ %-8s │ %-8s │\n' \ + "${RES_PHASE[$i]:0:16}" \ + "${RES_HOST[$i]:0:8}" \ + "${RES_WATT[$i]:0:8}" \ + "${RES_BLED[$i]:0:8}" \ + "${RES_RDISP[$i]:0:8}" | log_raw + if [[ -n "${RES_NOTE[$i]:-}" ]]; then + log "│ note: ${RES_NOTE[$i]}" + fi + done + fi + log "└──────────────────┴──────────┴──────────┴──────────┴──────────┘" + + # Verdict heuristics + local any_host_pass=0 any_host_fail=0 any_rf=0 all_rf_zero=1 + for ((i = 0; i < n; i++)); do + case "${RES_HOST[$i]}" in + *PASS*) any_host_pass=1 ;; + *FAIL*) any_host_fail=1 ;; + esac + case "${RES_WATT[$i]}" in + ''|'0'|'0.0'|'?'|'n'|'nein') ;; + *) + # non-zero / non-empty that looks like power + if [[ "${RES_WATT[$i]}" =~ ^[0-9]*\.?[0-9]+$ ]]; then + if awk -v w="${RES_WATT[$i]}" 'BEGIN{exit !(w>0)}'; then + any_rf=1 + all_rf_zero=0 + fi + else + # free text — if contains number >0 heuristic + if [[ "${RES_WATT[$i]}" =~ [1-9] ]]; then + any_rf=1 + all_rf_zero=0 + fi + fi + ;; + esac + if [[ "${RES_BLED[$i]}" == "j" || "${RES_RDISP[$i]}" == "j" ]]; then + any_rf=1 + all_rf_zero=0 + fi + done + + log "" + log "=== Verdict-Hinweise (Heuristik, kein Automatik-Urteil) ===" + if [[ "$n" -eq 0 ]]; then + log "• Keine Ladder-Daten — nur Preflight/Status gelaufen." + elif [[ "$any_host_pass" -eq 1 && "$all_rf_zero" -eq 1 && "$any_rf" -eq 0 ]]; then + log "• HOST OK / RF tot oder 0 W: Fehlerklasse NACH UART" + log " → DE-9/3.5mm Kontakt, Charge-Pump/Vcc, PTT-Transistor, Mic-Buchse, Radio" + log " → Mic-alone 4 W OK stützt: Radio selbst ok; Pfad Modem↔Mic intermittierend" + elif [[ "$any_host_pass" -eq 1 && "$any_rf" -eq 1 ]]; then + log "• HOST OK / RF zeitweise sichtbar: INTERMITTENT RF (Kontakt/Pump/AF)" + log " → Contact-Hunt wiederholen; zwischen langen Keys Pause ~14 s (PTT-WD)" + log " → TXD pulse vs steady (TFPCX) als Beobachtung; Kabel/Stecker fest" + elif [[ "$any_host_fail" -eq 1 && "$any_host_pass" -eq 0 ]]; then + log "• HOST FAIL: zuerst Stack/KISS/fulldup/MCR — nicht primär Wattmeter" + log " → max25-bcpr-ctl status, kiss_link, fulldup=yes bei Soft-DCD, kein zweites max25-bcprd" + log " → MCR bleibt Sailer 0x0e|bit / 0x0d (nicht 4PC-COM 0x0A)" + else + log "• Gemischte Ergebnisse — Log + Notizen vergleichen; Phasen 5/8 wiederholen." + fi + log "" + log "SSoT: operator research notes (private)" + log " 2026-07-19-bcpr-mcr-ok-rf-zero-after-4w.md · winning-recipe · RF intermittent" + log " soft-TNC RE: 2026-07-19-baycom-soft-tnc-serial-ptt-re.md (WD · TXD · MCR)" + log "USB: kein Produkt-TX (Phase U)." + log "" + log "Log gespeichert: $LOG" + return 0 +} + +run_all() { + phase_preflight + phase_idle_mcr + phase_status + phase_rx_listen + phase_force_tx_ladder + phase_cal_high + phase_double_burst + phase_contact_hunt + phase_usb_note + phase_summary +} + +menu_loop() { + while true; do + banner "max25-bcpr Ultimate Diag — Menü" + log "INI=$INI" + log "Log=$LOG" + log "" + log " 1) Preflight" + log " 2) Idle MCR" + log " 3) Status + Telem" + log " 4) RX Listen" + log " 5) Force-TX Ladder 1/3/5/8s" + log " 6) Cal/high" + log " 7) Double Burst" + log " 8) Contact Hunt" + log " 9) Summary / Verdict" + log " A) Alle Phasen 1–9 (+ USB-Hinweis)" + log " 0) max25-bcprd stop/start (sicher)" + log " U) USB-Hinweis" + log " Q) Beenden" + log "" + local choice + ask choice "Wahl:" + case "${choice^^}" in + 1) phase_preflight ;; + 2) phase_idle_mcr ;; + 3) phase_status ;; + 4) phase_rx_listen ;; + 5) phase_force_tx_ladder ;; + 6) phase_cal_high ;; + 7) phase_double_burst ;; + 8) phase_contact_hunt ;; + 9) phase_summary ;; + A|ALL) run_all ;; + 0) phase_restart_menu ;; + U) phase_usb_note ;; + Q|X|EXIT|QUIT) + phase_summary + log "Ende." + return 0 + ;; + *) + log "Unbekannte Wahl: $choice" + ;; + esac + done +} + +# ---------- main ---------- + +while [[ $# -gt 0 ]]; do + case "$1" in + -c) + [[ $# -ge 2 ]] || { echo "ERROR: -c needs INI path"; exit 2; } + INI="$2" + shift 2 + ;; + --menu) MODE="menu"; shift ;; + --all) MODE="all"; shift ;; + --phase) + [[ $# -ge 2 ]] || { echo "ERROR: --phase needs N"; exit 2; } + MODE="phase" + PHASE_ONLY="$2" + shift 2 + ;; + -h|--help) usage; exit 0 ;; + *) + echo "ERROR: unknown arg: $1" + usage + exit 2 + ;; + esac +done + +RUN_TS="$(date +%Y%m%d-%H%M%S)" +LOG="/tmp/max25-bcpr-ultimate-diag-${RUN_TS}.log" +: >"$LOG" || { + echo "FEHLER: kann Log nicht schreiben: $LOG" + exit 1 +} + +# Interactive reads use /dev/tty; log()/log_raw append to $LOG (no exec-tee double). +banner "max25-bcpr Ultimate Diag — BayCom/based (bcpr)" +log "Start: $(date -R 2>/dev/null || date)" +log "Host: $(hostname 2>/dev/null || echo '?')" +log "User: $(id -un 2>/dev/null || echo '?') uid=$(id -u)" +log "INI: $INI (override: MAX25_BCPR_INI / -c)" +log "Log: $LOG" +log "Tools: $CTL" +log "" +log "Hinweis: PTT-Watchdog ~14,5 s / 500 ms — bei langen Keys unkey/pausieren." +log " TXD: max25-bcprd pulst (Sailer); TFPCX oft steady — RF mid-key notieren." +log " MCR: Sailer belassen (kein 4PC-COM 0x0A-Experiment in diesem Skript)." +log "" + +ensure_tools || { + log "Abbruch: Tools fehlen." + exit 1 +} + +# Soft check sudo early (warn only for menu; hard for --all live) +if ! need_sudo; then + log "WARN: ohne sudo -n sind Live/TX/Cal/MCR-Phasen blockiert." + if [[ "$MODE" == "all" ]]; then + log "Abbruch (--all braucht sudo -n)." + exit 1 + fi +fi + +case "$MODE" in + all) + run_all + ;; + phase) + case "$PHASE_ONLY" in + 1) phase_preflight ;; + 2) phase_idle_mcr ;; + 3) phase_status ;; + 4) phase_rx_listen ;; + 5) phase_force_tx_ladder ;; + 6) phase_cal_high ;; + 7) phase_double_burst ;; + 8) phase_contact_hunt ;; + 9) phase_summary ;; + 0) phase_restart_menu ;; + U|u) phase_usb_note ;; + *) + log "FEHLER: unbekannte Phase $PHASE_ONLY" + exit 2 + ;; + esac + ;; + menu|*) + menu_loop + ;; +esac + +log "" +log "Fertig. Log: $LOG" +exit 0 -- cgit v1.3.1