summaryrefslogtreecommitdiff
path: root/scripts/max25-lite-setup.sh
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 /scripts/max25-lite-setup.sh
Initial push
Diffstat (limited to 'scripts/max25-lite-setup.sh')
-rwxr-xr-xscripts/max25-lite-setup.sh517
1 files changed, 517 insertions, 0 deletions
diff --git a/scripts/max25-lite-setup.sh b/scripts/max25-lite-setup.sh
new file mode 100755
index 0000000..412d83e
--- /dev/null
+++ b/scripts/max25-lite-setup.sh
@@ -0,0 +1,517 @@
+#!/usr/bin/env bash
+# MAX25-Stack-Lite — semi-automatic setup wizard (dialog / whiptail)
+# Detects serial + audio devices, writes max25d.ini (+ crdop.ini for CRDOP path).
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+UI_TITLE="MAX25-Stack-Lite setup"
+UI_BACKTITLE="MAX25-Stack-Lite | local packet-radio setup"
+UI_WIDTH=72
+UI_HEIGHT=18
+TMP_FILES=()
+
+UI=""
+if command -v whiptail >/dev/null 2>&1; then
+ UI=whiptail
+elif command -v dialog >/dev/null 2>&1; then
+ UI=dialog
+else
+ echo "max25-lite-setup: install whiptail or dialog, then run this command again." >&2
+ exit 1
+fi
+
+cleanup() {
+ local file
+ for file in "${TMP_FILES[@]}"; do
+ rm -f -- "$file"
+ done
+}
+trap cleanup EXIT HUP INT TERM
+
+require_terminal_size() {
+ local columns lines
+ [[ -t 1 && -t 2 ]] || return 0
+ columns="$(tput cols 2>/dev/null || true)"
+ lines="$(tput lines 2>/dev/null || true)"
+ if [[ "$columns" =~ ^[0-9]+$ && "$lines" =~ ^[0-9]+$ ]] \
+ && ((columns < UI_WIDTH || lines < UI_HEIGHT)); then
+ printf '%s\n' \
+ "max25-lite-setup: terminal is ${columns}x${lines}; need at least ${UI_WIDTH}x${UI_HEIGHT}." \
+ "Resize the terminal and run max25-lite-setup again." >&2
+ exit 1
+ fi
+}
+
+show_help() {
+ "$UI" --backtitle "$UI_BACKTITLE" --title "Setup help" --ok-label "Back" \
+ --msgbox "Use Up/Down to move, Space to mark a radio-list choice, and Enter to continue.
+
+Cancel or Esc stops the wizard without writing configuration files.
+
+The wizard writes local-only configuration. It does not start the modem, key a radio, or transmit.
+
+For serial and BayCom/based paths, select the device connected to this computer. For CRDOP, select the local capture/playback device." \
+ "$UI_HEIGHT" "$UI_WIDTH"
+}
+
+msgbox() {
+ "$UI" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" --ok-label "Continue" \
+ --msgbox "$1" 14 "$UI_WIDTH"
+}
+
+yesno() {
+ local prompt="$1" rc
+ while :; do
+ if "$UI" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
+ --yes-label "Yes" --no-label "No" --help-button --help-label "Help" \
+ --yesno "$prompt" 10 "$UI_WIDTH"; then
+ return 0
+ else
+ rc=$?
+ fi
+ if [[ "$rc" -eq 2 ]]; then
+ show_help
+ continue
+ fi
+ return "$rc"
+ done
+}
+
+inputbox() {
+ local prompt="$1" default="${2:-}" value rc
+ while :; do
+ if value=$("$UI" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
+ --ok-label "Continue" --cancel-label "Cancel" --help-button --help-label "Help" \
+ --inputbox "$prompt" 10 "$UI_WIDTH" "$default" 3>&1 1>&2 2>&3); then
+ printf '%s\n' "$value"
+ return 0
+ else
+ rc=$?
+ fi
+ if [[ "$rc" -eq 2 ]]; then
+ show_help
+ continue
+ fi
+ return "$rc"
+ done
+}
+
+menu() {
+ local prompt="$1" value rc
+ shift
+ while :; do
+ if value=$("$UI" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
+ --ok-label "Continue" --cancel-label "Cancel" --help-button --help-label "Help" \
+ --menu "$prompt" "$UI_HEIGHT" "$UI_WIDTH" 10 "$@" 3>&1 1>&2 2>&3); then
+ printf '%s\n' "$value"
+ return 0
+ else
+ rc=$?
+ fi
+ if [[ "$rc" -eq 2 ]]; then
+ show_help
+ continue
+ fi
+ return "$rc"
+ done
+}
+
+radiolist() {
+ local prompt="$1" value rc
+ shift
+ while :; do
+ if value=$("$UI" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
+ --ok-label "Continue" --cancel-label "Cancel" --help-button --help-label "Help" \
+ --radiolist "$prompt" "$UI_HEIGHT" "$UI_WIDTH" 10 "$@" 3>&1 1>&2 2>&3); then
+ printf '%s\n' "$value"
+ return 0
+ else
+ rc=$?
+ fi
+ if [[ "$rc" -eq 2 ]]; then
+ show_help
+ continue
+ fi
+ return "$rc"
+ done
+}
+
+require_terminal_size
+
+list_serial_ports() {
+ local -a ports=()
+ shopt -s nullglob
+ for pat in /dev/ttyUSB* /dev/ttyACM* /dev/ttyS*; do
+ [[ -e "$pat" ]] || continue
+ ports+=("$pat")
+ done
+ shopt -u nullglob
+ if ((${#ports[@]} == 0)); then
+ ports=("/dev/ttyUSB0")
+ fi
+ printf '%s\n' "${ports[@]}"
+}
+
+list_alsa_cards() {
+ if command -v arecord >/dev/null 2>&1; then
+ arecord -l 2>/dev/null | awk '/card [0-9]+:/ {c=$2; gsub(":", "", c); desc=$0; gsub(/^.*card [0-9]+: /, "", desc); gsub(/,.*/, "", desc); printf "hw:%s,0 %s\n", c, desc}' || true
+ fi
+ if command -v aplay >/dev/null 2>&1; then
+ aplay -l 2>/dev/null | awk '/card [0-9]+:/ {c=$2; gsub(":", "", c); desc=$0; gsub(/^.*card [0-9]+: /, "", desc); gsub(/,.*/, "", desc); printf "hw:%s,0 %s (playback)\n", c, desc}' || true
+ fi
+}
+
+msgbox "Welcome to MAX25-Stack-Lite setup.
+
+This wizard detects serial and audio devices on this computer, writes max25d.ini, and shows how to start max25d and max25-terminal locally.
+
+It changes files only after you choose a save location. Use Help for key hints, or Cancel to leave without saving."
+
+MODEM_KIND="$(menu "Packet radio modem path:" \
+ serial "Serial TNC / USB KISS (T-Modem, TNC2)" \
+ baycom "BayCom/based PC-COM (SER12 on COM/ttyS)" \
+ crdop "CRDOP sound-card soft modem (MAX25-SoftModem)" \
+ )" || exit 0
+
+DEVICE_ID="tnc2c"
+HARDWARE="tncs"
+SERIAL_PATH=""
+CRDOP_CAPTURE=""
+CRDOP_PLAYBACK=""
+WRITE_CRDOP_INI=0
+WRITE_BCPR_INI=0
+BCPR_INI_DEST=""
+
+if [[ "$MODEM_KIND" == "baycom" ]]; then
+ DEVICE_ID="max25e0"
+ HARDWARE="tncs"
+ WRITE_BCPR_INI=1
+ mapfile -t PORTS < <(list_serial_ports)
+ menu_args=()
+ idx=0
+ for p in "${PORTS[@]}"; do
+ menu_args+=("$p" "serial port" "$((idx == 0 ? 1 : 0))")
+ idx=$((idx + 1))
+ done
+ SERIAL_PATH="$(radiolist "Select PC-COM serial port (often /dev/ttyS0):" "${menu_args[@]}")" || exit 0
+elif [[ "$MODEM_KIND" == "serial" ]]; then
+ mapfile -t PORTS < <(list_serial_ports)
+ menu_args=()
+ idx=0
+ for p in "${PORTS[@]}"; do
+ menu_args+=("$p" "serial port" "$((idx == 0 ? 1 : 0))")
+ idx=$((idx + 1))
+ done
+ SERIAL_PATH="$(radiolist "Select serial device:" "${menu_args[@]}")" || exit 0
+ if [[ "$SERIAL_PATH" == /dev/ttyACM* ]]; then
+ if yesno "Device looks like USB KISS (T-Modem). Use device id tmodem instead of tnc2c?"; then
+ DEVICE_ID="tmodem"
+ fi
+ fi
+else
+ HARDWARE="soft-modems"
+ DEVICE_ID="soft-crdop"
+ WRITE_CRDOP_INI=1
+ mapfile -t ALSA_LINES < <(list_alsa_cards)
+ if ((${#ALSA_LINES[@]} == 0)); then
+ msgbox "No ALSA cards listed (arecord/aplay). You can edit ~/.config/crdop/crdop.ini later."
+ CRDOP_CAPTURE="hw:0,0"
+ CRDOP_PLAYBACK="hw:0,0"
+ else
+ menu_args=()
+ idx=0
+ for line in "${ALSA_LINES[@]}"; do
+ dev="${line%% *}"
+ label="${line#* }"
+ menu_args+=("$dev" "$label" "$((idx == 0 ? 1 : 0))")
+ idx=$((idx + 1))
+ done
+ sel="$(radiolist "Select capture/playback ALSA device (hw:N,0):" "${menu_args[@]}")" || exit 0
+ CRDOP_CAPTURE="$sel"
+ CRDOP_PLAYBACK="$sel"
+ fi
+fi
+
+CALLERID="$(inputbox "Your AX.25 caller ID (e.g. CB-0):" "CB-0")" || exit 0
+CALLID="$(inputbox "Default destination callsign for CONNECT tests:" "QST")" || exit 0
+
+if yesno "Write system config /etc/max25/max25d.ini? (No = ./local/max25d.ini in repo)"; then
+ INI_DEST="/etc/max25/max25d.ini"
+ CRDOP_INI_DEST="${HOME}/.config/crdop/crdop.ini"
+ BCPR_INI_DEST="/etc/max25/max25-bcpr.ini"
+ NEED_SUDO=1
+else
+ INI_DEST="${ROOT}/local/max25d.ini"
+ CRDOP_INI_DEST="${ROOT}/local/crdop.ini"
+ BCPR_INI_DEST="${ROOT}/local/max25-bcpr.ini"
+ NEED_SUDO=0
+fi
+
+if [[ "$NEED_SUDO" -eq 1 ]]; then
+ if ! command -v sudo >/dev/null 2>&1; then
+ msgbox "System configuration needs sudo, but sudo is not available.
+
+Choose No at the save-location screen to write local configuration in this repository."
+ exit 1
+ fi
+ sudo mkdir -p "$(dirname "$INI_DEST")"
+ mkdir -p "$(dirname "$CRDOP_INI_DEST")"
+else
+ mkdir -p "$(dirname "$INI_DEST")"
+fi
+
+write_max25d_ini() {
+ local dest="$1"
+ if [[ "$MODEM_KIND" == "baycom" ]]; then
+ cat >"$dest" <<EOF
+; MAX25-Stack-Lite — generated by max25-lite-setup.sh (BayCom/based)
+
+[features]
+max25_bcpr = yes
+
+[daemon]
+mode = standalone
+hardware = tncs
+device = max25e0
+
+[network]
+tcp_host = 127.0.0.1
+tcp_port = 7325
+unix_socket = /run/max25/modem.sock
+tcp_password =
+
+[modem]
+callerid = ${CALLERID}
+callid = ${CALLID}
+ax25_ui = yes
+
+[reporting]
+error_transmissions = yes
+voice_transmissions = yes
+data_passes = 3
+data_quality_min = 50
+data_pass_seconds = 20
+
+[stack]
+auto_start = yes
+
+[devices]
+default = max25e0
+max25e0 = max25-bcpr:bc0
+
+[device.max25e0]
+kiss_link = /tmp/max25-bcpr/kiss-bc0
+max25_bcpr_ini = ${BCPR_INI_DEST}
+EOF
+ elif [[ "$MODEM_KIND" == "serial" ]]; then
+ cat >"$dest" <<EOF
+; MAX25-Stack-Lite — generated by max25-lite-setup.sh
+
+[daemon]
+mode = standalone
+hardware = ${HARDWARE}
+device = ${DEVICE_ID}
+
+[network]
+tcp_host = 127.0.0.1
+tcp_port = 7325
+unix_socket = /run/max25/modem.sock
+; Lite: localhost only — terminal on same host as max25d
+tcp_password =
+
+[modem]
+callerid = ${CALLERID}
+callid = ${CALLID}
+ax25_ui = yes
+
+[reporting]
+error_transmissions = yes
+voice_transmissions = yes
+data_passes = 3
+data_quality_min = 50
+data_pass_seconds = 20
+
+[stack]
+auto_start = yes
+serial_watch = yes
+serial_watch_interval = 60
+stack_recover_only = yes
+serial_bootwait_escalate = yes
+serial_bootwait_escalate_after = 3
+
+[devices]
+default = ${DEVICE_ID}
+${DEVICE_ID} = ${SERIAL_PATH}
+
+[serial.${DEVICE_ID}]
+baud = 19200
+line = 8n1
+dtr_rts = yes
+kiss_entry = kiss_on
+EOF
+ else
+ cat >"$dest" <<EOF
+; MAX25-Stack-Lite — generated by max25-lite-setup.sh (CRDOP)
+
+[daemon]
+mode = standalone
+hardware = soft-modems
+device = soft-crdop
+
+[network]
+tcp_host = 127.0.0.1
+tcp_port = 7325
+unix_socket = /run/max25/modem.sock
+; Lite: localhost only — terminal on same host as max25d
+tcp_password =
+
+[modem]
+callerid = ${CALLERID}
+callid = ${CALLID}
+ax25_ui = yes
+
+[reporting]
+error_transmissions = yes
+voice_transmissions = yes
+data_passes = 3
+data_quality_min = 50
+data_pass_seconds = 20
+
+[stack]
+auto_start = yes
+
+[devices]
+default = soft-crdop
+soft-crdop = crdop:default
+
+[device.soft-crdop]
+host = 127.0.0.1
+port = 8515
+listen = yes
+EOF
+ fi
+}
+
+write_crdop_ini() {
+ local dest="$1"
+ mkdir -p "$(dirname "$dest")"
+ cat >"$dest" <<EOF
+; MAX25-SoftModem (CRDOP) — generated by max25-lite-setup.sh
+
+[profile]
+radio_profile = cb
+
+[modem]
+speed_baud = 1200
+duplex = half
+max_speed_baud = 19200
+
+[host]
+port = 8515
+
+[mycall]
+call = ${CALLERID}
+
+[audio]
+backend = alsa-kernel
+no_pulse = yes
+capture = ${CRDOP_CAPTURE}
+playback = ${CRDOP_PLAYBACK}
+
+[coupling]
+mode = line
+EOF
+}
+
+write_bcpr_ini() {
+ local dest="$1"
+ mkdir -p "$(dirname "$dest")"
+ cat >"$dest" <<EOF
+; max25-bcpr — BayCom/based PC-COM (generated by max25-lite-setup.sh)
+
+[max25-bcpr]
+dry_run = no
+state_dir = /tmp/max25-bcpr
+user = max25
+group = max25
+
+[bc0]
+enabled = yes
+serial = ${SERIAL_PATH}
+iobase = 0x3f8
+irq = 4
+mode = ser12*
+kiss_link = /tmp/max25-bcpr/kiss-bc0
+baud = 1200
+tx_delay = 35
+tx_tail = 2
+slottime = 10
+ppersist = 40
+fulldup = no
+EOF
+}
+
+if [[ "$NEED_SUDO" -eq 1 ]]; then
+ tmp="$(mktemp)"
+ TMP_FILES+=("$tmp")
+ write_max25d_ini "$tmp"
+ sudo cp "$tmp" "$INI_DEST"
+ if [[ "$WRITE_CRDOP_INI" -eq 1 ]]; then
+ tmp="$(mktemp)"
+ TMP_FILES+=("$tmp")
+ write_crdop_ini "$tmp"
+ cp "$tmp" "$CRDOP_INI_DEST"
+ fi
+ if [[ "$WRITE_BCPR_INI" -eq 1 ]]; then
+ tmp="$(mktemp)"
+ TMP_FILES+=("$tmp")
+ write_bcpr_ini "$tmp"
+ sudo cp "$tmp" "$BCPR_INI_DEST"
+ fi
+else
+ mkdir -p "${ROOT}/local"
+ write_max25d_ini "$INI_DEST"
+ if [[ "$WRITE_CRDOP_INI" -eq 1 ]]; then
+ write_crdop_ini "$CRDOP_INI_DEST"
+ fi
+ if [[ "$WRITE_BCPR_INI" -eq 1 ]]; then
+ write_bcpr_ini "$BCPR_INI_DEST"
+ fi
+fi
+
+NEXT_STEPS="Configuration written:
+
+ max25d.ini → ${INI_DEST}"
+if [[ "$WRITE_CRDOP_INI" -eq 1 ]]; then
+ NEXT_STEPS="${NEXT_STEPS}
+ crdop.ini → ${CRDOP_INI_DEST}"
+fi
+if [[ "$WRITE_BCPR_INI" -eq 1 ]]; then
+ NEXT_STEPS="${NEXT_STEPS}
+ max25-bcpr.ini → ${BCPR_INI_DEST}"
+fi
+NEXT_STEPS="${NEXT_STEPS}
+
+Build (if not done):
+ cmake -B build && cmake --build build
+ sudo cmake --install build
+
+BayCom/based only — start bcpr first (needs root on Linux):
+ sudo max25-bcpr-ctl start --ini ${BCPR_INI_DEST}
+
+Start daemon:
+ ${ROOT}/scripts/run-max25d.sh ${INI_DEST}
+
+CRDOP only — start soft modem (separate terminal):
+ max25-ctl start --hardware soft-modems --device soft-crdop
+ (or: crdop -c ${CRDOP_INI_DEST})
+
+Terminal + CONNECT:
+ ${ROOT}/scripts/run-max25-terminal.sh -U /run/max25/modem.sock
+ At prompt: CONNECT ${CALLID}
+ Or F10 menu → Connect
+
+RX before TX: confirm receive/noise/SQ before live transmit."
+
+"$UI" --backtitle "$UI_BACKTITLE" --title "Setup complete" --ok-label "Done" \
+ --msgbox "$NEXT_STEPS" "$UI_HEIGHT" "$UI_WIDTH"
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com