#!/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" <"$dest" <"$dest" <"$dest" <"$dest" <