summaryrefslogtreecommitdiff
path: root/stacks/max25-bcpr/tools
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-07 18:23:28 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-07 18:23:28 +0000
commitfa05a5f8238e9e1417235711656e5062ccc843a7 (patch)
tree46fbbd18de54492b59307c16efcc7f3152723238 /stacks/max25-bcpr/tools
Initial push
Diffstat (limited to 'stacks/max25-bcpr/tools')
-rw-r--r--stacks/max25-bcpr/tools/NEBENBEI-MOVED.md5
-rwxr-xr-xstacks/max25-bcpr/tools/bcpr-ultimate-diag.sh916
-rwxr-xr-xstacks/max25-bcpr/tools/max25-bcpr-ctl379
-rwxr-xr-xstacks/max25-bcpr/tools/max25-bcpr-rxtx-smoke.sh566
-rwxr-xr-xstacks/max25-bcpr/tools/max25-bcpr-ultimate-diag.sh914
5 files changed, 2780 insertions, 0 deletions
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 || true
+ log "[pause] $msg"
+}
+
+ask() {
+ # ask VAR "prompt"
+ local __var="$1"
+ local __prompt="$2"
+ local __ans=""
+ printf '%s ' "$__prompt" >/dev/tty
+ read -r __ans </dev/tty || true
+ printf -v "$__var" '%s' "$__ans"
+ log "[antwort] $__prompt → ${__ans}"
+}
+
+ask_yn() {
+ # ask_yn VAR "prompt" → stores j/n/y/n normalized to j|n
+ local __var="$1"
+ local __prompt="$2"
+ local __ans=""
+ while true; do
+ printf '%s [j/n]: ' "$__prompt" >/dev/tty
+ read -r __ans </dev/tty || true
+ case "${__ans,,}" in
+ j|ja|y|yes) printf -v "$__var" 'j'; log "[antwort] $__prompt → j"; return 0 ;;
+ n|nein|no) printf -v "$__var" 'n'; log "[antwort] $__prompt → n"; return 0 ;;
+ *) printf 'Bitte j oder n.\n' >/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 <<USAGE
+max25-bcpr-ctl — BayCom/based PC-COM modem helper
+
+Usage: max25-bcpr-ctl [-c ini] {preflight|start|status|stop|smoke|version}
+
+ preflight check config and serial port before start
+ start start the PC-COM background service
+ status show whether the service is running
+ stop stop the service
+ smoke offline/optional live test (see --help on smoke script)
+
+Config file default: /etc/max25/max25-bcpr.ini
+Create one with: max25-lite-setup (BayCom/based path)
+
+RX before TX — confirm receive before live transmit.
+USAGE
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -c) INI="$2"; shift 2 ;;
+ -h|--help) usage; exit 0 ;;
+ *) break ;;
+ esac
+done
+CMD="${1:-}"
+if [[ $# -gt 0 ]]; then
+ shift
+fi
+
+resolve_max25_bcprd_init() {
+ if [[ -n "${BCPRD_INIT:-}" && "$BCPRD_INIT" != "max25-bcprd-init" && -x "$BCPRD_INIT" ]]; then
+ echo "$BCPRD_INIT"; return
+ fi
+ if command -v "$BCPRD_INIT" >/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 >/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
+ err "PC-COM service failed to start — run preflight again; use sudo if asked"
+ return 1
+ fi
+ if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/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 || true
+ log "[pause] $msg"
+}
+
+ask() {
+ # ask VAR "prompt"
+ local __var="$1"
+ local __prompt="$2"
+ local __ans=""
+ printf '%s ' "$__prompt" >/dev/tty
+ read -r __ans </dev/tty || true
+ printf -v "$__var" '%s' "$__ans"
+ log "[antwort] $__prompt → ${__ans}"
+}
+
+ask_yn() {
+ # ask_yn VAR "prompt" → stores j/n/y/n normalized to j|n
+ local __var="$1"
+ local __prompt="$2"
+ local __ans=""
+ while true; do
+ printf '%s [j/n]: ' "$__prompt" >/dev/tty
+ read -r __ans </dev/tty || true
+ case "${__ans,,}" in
+ j|ja|y|yes) printf -v "$__var" 'j'; log "[antwort] $__prompt → j"; return 0 ;;
+ n|nein|no) printf -v "$__var" 'n'; log "[antwort] $__prompt → n"; return 0 ;;
+ *) printf 'Bitte j oder n.\n' >/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
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com