Created
January 4, 2026 05:08
-
-
Save justingreerbbi/c6dc7e4a962c1f5d4947d773c2ad45b8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # noxradio-install.sh | |
| # Installs + configures: Direwolf (AIOC auto-detect), AX.25/ROSE modules, KISS TCP, and a minimal web UI to test packets. | |
| # Updated: prompts for callsign during install. | |
| # | |
| # Run: | |
| # chmod +x noxradio-install.sh | |
| # sudo ./noxradio-install.sh | |
| set -euo pipefail | |
| LOG_TAG="[noxradio]" | |
| DW_REPO="https://github.com/wb2osz/direwolf.git" | |
| DW_SRC_DIR="/opt/direwolf-src" | |
| DW_CONF="/etc/direwolf.conf" | |
| DW_USER="direwolf" | |
| WEB_DIR="/opt/noxradio-web" | |
| WEB_PORT="8080" | |
| KISS_PORT="8001" | |
| DEFAULT_MYCALL="N0CALL-10" | |
| # --- helpers --- | |
| log() { echo "${LOG_TAG} $*"; } | |
| warn() { echo "${LOG_TAG} WARNING: $*" >&2; } | |
| die() { echo "${LOG_TAG} ERROR: $*" >&2; exit 1; } | |
| need_cmd() { | |
| command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" | |
| } | |
| is_root() { [[ "${EUID}" -eq 0 ]]; } | |
| # Determine "main" non-root user for web app files/group membership | |
| detect_primary_user() { | |
| if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then | |
| echo "${SUDO_USER}" | |
| else | |
| local u | |
| u="$(ls -1 /home 2>/dev/null | head -n 1 || true)" | |
| [[ -n "${u}" ]] && echo "${u}" || echo "pi" | |
| fi | |
| } | |
| apt_install() { | |
| local pkgs=("$@") | |
| log "Installing packages: ${pkgs[*]}" | |
| DEBIAN_FRONTEND=noninteractive apt-get install -y "${pkgs[@]}" | |
| } | |
| # --- callsign prompt --- | |
| prompt_callsign() { | |
| local existing="" | |
| if [[ -f "${DW_CONF}" ]]; then | |
| existing="$(sed -n 's/^MYCALL[[:space:]]\+\(.*\)$/\1/p' "${DW_CONF}" | head -n 1 || true)" | |
| fi | |
| echo "" | |
| echo "============================================================" | |
| echo " Direwolf Callsign Setup" | |
| echo "============================================================" | |
| if [[ -n "${existing}" ]]; then | |
| echo "Current MYCALL in ${DW_CONF}: ${existing}" | |
| fi | |
| echo "Enter your callsign for Direwolf (any value 4+ characters)" | |
| echo "Example (common APRS style): K1ABC-10" | |
| echo "Press ENTER to keep current value or use default ${DEFAULT_MYCALL}." | |
| echo "" | |
| local input="" | |
| read -r -p "MYCALL [${existing:-${DEFAULT_MYCALL}}]: " input || true | |
| input="${input//[[:space:]]/}" # strip whitespace | |
| if [[ -n "${input}" ]]; then | |
| echo "${input}" | |
| elif [[ -n "${existing}" ]]; then | |
| echo "${existing}" | |
| else | |
| echo "${DEFAULT_MYCALL}" | |
| fi | |
| } | |
| # Basic callsign validation (lenient; allows A-Z 0-9 and optional -SSID) | |
| validate_callsign() { | |
| local cs="$1" | |
| cs="${cs//[[:space:]]/}" | |
| [[ ${#cs} -gt 3 ]] | |
| } | |
| # --- hardware detection (AIOC) --- | |
| detect_audio_capture_card() { | |
| local line card | |
| line="$(arecord -l 2>/dev/null | grep -E 'card [0-9]+' | grep -viE 'bcm2835|vc4hdmi' | head -n 1 || true)" | |
| if [[ -z "${line}" ]]; then | |
| echo "" | |
| return | |
| fi | |
| card="$(echo "${line}" | sed -n 's/.*card \([0-9]\+\).*/\1/p' | head -n 1)" | |
| echo "${card}" | |
| } | |
| detect_ptt_hidraw_device() { | |
| local dev props | |
| for dev in /dev/hidraw*; do | |
| [[ -e "${dev}" ]] || continue | |
| props="$(udevadm info -q property -n "${dev}" 2>/dev/null || true)" | |
| if echo "${props}" | grep -qiE 'CM108|C-Media|Cmedia|USB Audio'; then | |
| echo "${dev}" | |
| return | |
| fi | |
| done | |
| for dev in /dev/hidraw*; do | |
| [[ -e "${dev}" ]] || continue | |
| echo "${dev}" | |
| return | |
| done | |
| echo "" | |
| } | |
| detect_serial_tty_device() { | |
| if [[ -e /dev/ttyACM0 ]]; then | |
| echo "/dev/ttyACM0" | |
| return | |
| fi | |
| if [[ -e /dev/ttyUSB0 ]]; then | |
| echo "/dev/ttyUSB0" | |
| return | |
| fi | |
| echo "" | |
| } | |
| udev_vid_pid_for_node() { | |
| local node="$1" | |
| local props vid pid | |
| props="$(udevadm info -q property -n "${node}" 2>/dev/null || true)" | |
| vid="$(echo "${props}" | sed -n 's/^ID_VENDOR_ID=\(.*\)$/\1/p' | head -n 1)" | |
| pid="$(echo "${props}" | sed -n 's/^ID_MODEL_ID=\(.*\)$/\1/p' | head -n 1)" | |
| if [[ -n "${vid}" && -n "${pid}" ]]; then | |
| echo "${vid} ${pid}" | |
| else | |
| echo "" | |
| fi | |
| } | |
| write_udev_rules() { | |
| local hid_node="$1" | |
| local tty_node="$2" | |
| local rule_file="/etc/udev/rules.d/99-noxradio-aioc.rules" | |
| local hid_vp tty_vp hid_vid hid_pid tty_vid tty_pid | |
| hid_vp="$(udev_vid_pid_for_node "${hid_node}")" | |
| tty_vp="$(udev_vid_pid_for_node "${tty_node}")" | |
| { | |
| echo "# NoxRadio AIOC permissions (generated)" | |
| echo "# Allows non-root Direwolf access to hidraw (CM108-style PTT) and serial (DTR/RTS PTT)" | |
| echo "" | |
| if [[ -n "${hid_vp}" ]]; then | |
| hid_vid="$(echo "${hid_vp}" | awk '{print $1}')" | |
| hid_pid="$(echo "${hid_vp}" | awk '{print $2}')" | |
| echo "SUBSYSTEM==\"hidraw\", ATTRS{idVendor}==\"${hid_vid}\", ATTRS{idProduct}==\"${hid_pid}\", MODE=\"0660\", GROUP=\"input\"" | |
| else | |
| echo "# hidraw VID/PID not detected; falling back to generic (may be too broad):" | |
| echo "SUBSYSTEM==\"hidraw\", MODE=\"0660\", GROUP=\"input\"" | |
| fi | |
| if [[ -n "${tty_vp}" ]]; then | |
| tty_vid="$(echo "${tty_vp}" | awk '{print $1}')" | |
| tty_pid="$(echo "${tty_vp}" | awk '{print $2}')" | |
| echo "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"${tty_vid}\", ATTRS{idProduct}==\"${tty_pid}\", MODE=\"0660\", GROUP=\"dialout\"" | |
| else | |
| echo "# tty VID/PID not detected; generic dialout permissions already handle /dev/ttyACM* and /dev/ttyUSB*" | |
| fi | |
| } > "${rule_file}" | |
| chmod 0644 "${rule_file}" | |
| udevadm control --reload-rules | |
| udevadm trigger | |
| log "Wrote udev rules: ${rule_file}" | |
| } | |
| # --- direwolf install --- | |
| install_direwolf() { | |
| if command -v direwolf >/dev/null 2>&1; then | |
| log "Direwolf already installed at: $(command -v direwolf)" | |
| return | |
| fi | |
| log "Installing Direwolf from source into /usr/local..." | |
| rm -rf "${DW_SRC_DIR}" | |
| git clone "${DW_REPO}" "${DW_SRC_DIR}" | |
| mkdir -p "${DW_SRC_DIR}/build" | |
| pushd "${DW_SRC_DIR}/build" >/dev/null | |
| cmake .. | |
| make -j2 | |
| make install | |
| make install-conf || true | |
| popd >/dev/null | |
| need_cmd direwolf | |
| log "Direwolf installed: $(command -v direwolf)" | |
| } | |
| ensure_direwolf_user() { | |
| if id -u "${DW_USER}" >/dev/null 2>&1; then | |
| log "User ${DW_USER} exists" | |
| else | |
| log "Creating system user: ${DW_USER}" | |
| useradd -r -m -s /usr/sbin/nologin "${DW_USER}" | |
| fi | |
| usermod -aG audio,dialout,input "${DW_USER}" || true | |
| } | |
| write_direwolf_config() { | |
| local audio_card="$1" | |
| local hid_node="$2" | |
| local tty_node="$3" | |
| local mycall="$4" | |
| local adevice_line="" | |
| local ptt_line="" | |
| local ptt_comment="" | |
| if [[ -n "${audio_card}" ]]; then | |
| adevice_line="ADEVICE plughw:${audio_card},0" | |
| else | |
| adevice_line="# ADEVICE plughw:1,0 # TODO: set correct card,device" | |
| fi | |
| if [[ -n "${hid_node}" ]]; then | |
| ptt_line="PTT CM108" | |
| ptt_comment="# Using CM108-style HID PTT (recommended for AIOC)" | |
| elif [[ -n "${tty_node}" ]]; then | |
| ptt_line="PTT ${tty_node} DTR -RTS" | |
| ptt_comment="# Using serial DTR/RTS PTT (fallback)" | |
| else | |
| ptt_line="# PTT CM108 # TODO: enable PTT once AIOC is detected" | |
| ptt_comment="# No hidraw/tty device detected at install time." | |
| fi | |
| cat > "${DW_CONF}" <<EOF | |
| # /etc/direwolf.conf (generated by noxradio-install.sh) | |
| # Edit this file as needed. | |
| ${adevice_line} | |
| ARATE 48000 | |
| # Your callsign | |
| MYCALL ${mycall} | |
| CHANNEL 0 | |
| ${ptt_comment} | |
| ${ptt_line} | |
| # Expose KISS over TCP for your web app / custom software | |
| KISSPORT ${KISS_PORT} | |
| EOF | |
| chmod 0644 "${DW_CONF}" | |
| log "Wrote Direwolf config: ${DW_CONF}" | |
| } | |
| write_direwolf_service() { | |
| local svc="/etc/systemd/system/direwolf.service" | |
| cat > "${svc}" <<EOF | |
| [Unit] | |
| Description=Direwolf Soundcard TNC | |
| After=network.target sound.target | |
| [Service] | |
| Type=simple | |
| User=${DW_USER} | |
| ExecStart=/usr/local/bin/direwolf -c ${DW_CONF} -t 0 | |
| Restart=on-failure | |
| RestartSec=2 | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOF | |
| systemctl daemon-reload | |
| systemctl enable --now direwolf | |
| log "Enabled + started direwolf.service" | |
| } | |
| # --- AX.25 / ROSE setup --- | |
| write_modules_load() { | |
| local f="/etc/modules-load.d/noxradio.conf" | |
| cat > "${f}" <<'EOF' | |
| # NoxRadio packet modules | |
| ax25 | |
| rose | |
| netrom | |
| EOF | |
| chmod 0644 "${f}" | |
| modprobe ax25 2>/dev/null || true | |
| modprobe rose 2>/dev/null || true | |
| modprobe netrom 2>/dev/null || true | |
| log "Configured module autoload: ${f}" | |
| } | |
| write_axports() { | |
| local mycall="$1" | |
| local f="/etc/ax25/axports" | |
| mkdir -p /etc/ax25 | |
| if [[ -f "${f}" ]] && grep -qE '^ax0[[:space:]]' "${f}"; then | |
| # Update ax0 line if exists | |
| sed -i -E "s/^ax0[[:space:]]+.*/ax0\t${mycall}\t9600\t255\t2\tNoxRadio Direwolf KISS bridge/" "${f}" | |
| log "Updated existing ax0 in: ${f}" | |
| return | |
| fi | |
| cat >> "${f}" <<EOF | |
| # NoxRadio AX.25 port (generated) | |
| ax0 ${mycall} 9600 255 2 NoxRadio Direwolf KISS bridge | |
| EOF | |
| log "Updated: ${f}" | |
| } | |
| write_ax25_bridge_script() { | |
| local script="/usr/local/sbin/noxradio-ax25-bridge.sh" | |
| cat > "${script}" <<'EOF' | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| KISS_TCP_HOST="127.0.0.1" | |
| KISS_TCP_PORT="8001" | |
| PTY_LINK="/tmp/dwkiss" | |
| AXPORT="ax0" | |
| INET_ADDR="44.1.1.1" | |
| log() { echo "[ax25-bridge] $*"; } | |
| rm -f "${PTY_LINK}" | |
| log "Starting socat PTY <-> TCP KISS (${KISS_TCP_HOST}:${KISS_TCP_PORT})" | |
| socat -d -d pty,raw,echo=0,link="${PTY_LINK}" "tcp:${KISS_TCP_HOST}:${KISS_TCP_PORT}" & | |
| SOCAT_PID=$! | |
| for i in $(seq 1 30); do | |
| [[ -e "${PTY_LINK}" ]] && break | |
| sleep 0.2 | |
| done | |
| if [[ ! -e "${PTY_LINK}" ]]; then | |
| kill "${SOCAT_PID}" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| log "Attaching KISS: ${PTY_LINK} -> ${AXPORT} (${INET_ADDR})" | |
| kissattach "${PTY_LINK}" "${AXPORT}" "${INET_ADDR}" || true | |
| ip link set "${AXPORT}" up || true | |
| wait "${SOCAT_PID}" | |
| EOF | |
| chmod 0755 "${script}" | |
| log "Wrote: ${script}" | |
| } | |
| write_ax25_bridge_service() { | |
| local svc="/etc/systemd/system/noxradio-ax25-bridge.service" | |
| cat > "${svc}" <<EOF | |
| [Unit] | |
| Description=NoxRadio AX.25 bridge (Direwolf KISS TCP -> ax0) | |
| After=network.target direwolf.service | |
| Wants=direwolf.service | |
| [Service] | |
| Type=simple | |
| User=root | |
| ExecStart=/usr/local/sbin/noxradio-ax25-bridge.sh | |
| Restart=on-failure | |
| RestartSec=2 | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOF | |
| systemctl daemon-reload | |
| systemctl enable --now noxradio-ax25-bridge | |
| log "Enabled + started noxradio-ax25-bridge.service" | |
| } | |
| # --- Web test app --- | |
| write_web_app() { | |
| mkdir -p "${WEB_DIR}" | |
| cat > "${WEB_DIR}/app.py" <<'EOF' | |
| import asyncio | |
| import socket | |
| from typing import Set, Optional, Tuple | |
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect | |
| from fastapi.responses import HTMLResponse | |
| app = FastAPI() | |
| FEND = 0xC0 | |
| FESC = 0xDB | |
| TFEND = 0xDC | |
| TFESC = 0xDD | |
| clients: Set[WebSocket] = set() | |
| _reader_task: Optional[asyncio.Task] = None | |
| def kiss_unescape(data: bytes) -> bytes: | |
| out = bytearray() | |
| i = 0 | |
| while i < len(data): | |
| b = data[i] | |
| if b == FESC and i + 1 < len(data): | |
| nxt = data[i + 1] | |
| if nxt == TFEND: | |
| out.append(FEND) | |
| i += 2 | |
| continue | |
| if nxt == TFESC: | |
| out.append(FESC) | |
| i += 2 | |
| continue | |
| out.append(b) | |
| i += 1 | |
| return bytes(out) | |
| def ax25_addr_decode(addr7: bytes) -> Tuple[str, int, bool]: | |
| call = "" | |
| for c in addr7[0:6]: | |
| ch = (c >> 1) & 0x7F | |
| if ch != 0x20: | |
| call += chr(ch) | |
| ssid = (addr7[6] >> 1) & 0x0F | |
| last = bool(addr7[6] & 0x01) | |
| return call, ssid, last | |
| def parse_ax25_ui(frame: bytes) -> Optional[str]: | |
| if len(frame) < 16: | |
| return None | |
| i = 0 | |
| dest, dest_ssid, _ = ax25_addr_decode(frame[i:i+7]); i += 7 | |
| src, src_ssid, last = ax25_addr_decode(frame[i:i+7]); i += 7 | |
| path = [] | |
| while not last: | |
| if i + 7 > len(frame): | |
| return None | |
| digi, digi_ssid, last = ax25_addr_decode(frame[i:i+7]); i += 7 | |
| path.append(f"{digi}-{digi_ssid}" if digi_ssid else digi) | |
| if i + 2 > len(frame): | |
| return None | |
| control = frame[i]; pid = frame[i+1]; i += 2 | |
| if control != 0x03 or pid != 0xF0: | |
| return None | |
| payload = frame[i:].decode("ascii", errors="replace") | |
| src_str = f"{src}-{src_ssid}" if src_ssid else src | |
| dest_str = f"{dest}-{dest_ssid}" if dest_ssid else dest | |
| if path: | |
| return f"{src_str}>{dest_str},{','.join(path)}:{payload}" | |
| return f"{src_str}>{dest_str}:{payload}" | |
| async def broadcast(msg: str) -> None: | |
| dead = [] | |
| for ws in clients: | |
| try: | |
| await ws.send_text(msg) | |
| except Exception: | |
| dead.append(ws) | |
| for ws in dead: | |
| clients.discard(ws) | |
| async def direwolf_reader(host="127.0.0.1", port=8001) -> None: | |
| while True: | |
| try: | |
| s = socket.create_connection((host, port), timeout=10) | |
| s.settimeout(1.0) | |
| await broadcast("[web] Connected to Direwolf KISS TCP") | |
| buf = bytearray() | |
| while True: | |
| try: | |
| chunk = s.recv(4096) | |
| except socket.timeout: | |
| chunk = b"" | |
| if chunk: | |
| buf.extend(chunk) | |
| while True: | |
| try: | |
| start = buf.index(FEND) | |
| except ValueError: | |
| buf.clear() | |
| break | |
| try: | |
| end = buf.index(FEND, start + 1) | |
| except ValueError: | |
| if start > 0: | |
| del buf[:start] | |
| break | |
| raw = bytes(buf[start + 1:end]) | |
| del buf[:end + 1] | |
| if not raw: | |
| continue | |
| cmd = raw[0] | |
| data = kiss_unescape(raw[1:]) | |
| kiss_cmd = cmd & 0x0F | |
| if kiss_cmd != 0x00: | |
| continue | |
| tnc2 = parse_ax25_ui(data) | |
| if tnc2: | |
| await broadcast(tnc2) | |
| except Exception as e: | |
| await broadcast(f"[web] Direwolf connection error: {e}. Retrying in 2s...") | |
| await asyncio.sleep(2) | |
| finally: | |
| try: | |
| s.close() | |
| except Exception: | |
| pass | |
| @app.get("/") | |
| def index(): | |
| return HTMLResponse( | |
| """ | |
| <!doctype html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"/> | |
| <title>NoxRadio - Direwolf Monitor</title> | |
| <style> | |
| body { font-family: sans-serif; margin: 16px; } | |
| #log { white-space: pre-wrap; border: 1px solid #ccc; padding: 12px; height: 70vh; overflow: auto; } | |
| .row { display: flex; gap: 8px; margin-bottom: 10px; } | |
| button { padding: 8px 10px; } | |
| </style> | |
| </head> | |
| <body> | |
| <h2>NoxRadio - Direwolf Live Monitor</h2> | |
| <div class="row"> | |
| <button onclick="clearLog()">Clear</button> | |
| </div> | |
| <div id="log"></div> | |
| <script> | |
| const log = document.getElementById("log"); | |
| function add(line) { | |
| log.textContent += line + "\\n"; | |
| log.scrollTop = log.scrollHeight; | |
| } | |
| function clearLog() { log.textContent = ""; } | |
| const ws = new WebSocket(`ws://${location.host}/ws`); | |
| ws.onmessage = (ev) => add(ev.data); | |
| ws.onopen = () => add("[web] WebSocket connected"); | |
| ws.onclose = () => add("[web] WebSocket closed"); | |
| ws.onerror = () => add("[web] WebSocket error"); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| ) | |
| @app.websocket("/ws") | |
| async def ws_endpoint(ws: WebSocket): | |
| global _reader_task | |
| await ws.accept() | |
| clients.add(ws) | |
| if _reader_task is None or _reader_task.done(): | |
| _reader_task = asyncio.create_task(direwolf_reader()) | |
| try: | |
| while True: | |
| await ws.receive_text() | |
| except WebSocketDisconnect: | |
| clients.discard(ws) | |
| EOF | |
| log "Wrote web app: ${WEB_DIR}/app.py" | |
| } | |
| setup_web_venv() { | |
| local primary_user="$1" | |
| python3 -m venv "${WEB_DIR}/.venv" | |
| "${WEB_DIR}/.venv/bin/pip" install --upgrade pip | |
| "${WEB_DIR}/.venv/bin/pip" install fastapi "uvicorn[standard]" | |
| chown -R "${primary_user}:${primary_user}" "${WEB_DIR}" | |
| log "Web venv ready: ${WEB_DIR}/.venv" | |
| } | |
| write_web_service() { | |
| local svc="/etc/systemd/system/noxradio-web.service" | |
| local primary_user | |
| primary_user="$(detect_primary_user)" | |
| cat > "${svc}" <<EOF | |
| [Unit] | |
| Description=NoxRadio Web (Direwolf Test UI) | |
| After=network.target direwolf.service | |
| Wants=direwolf.service | |
| [Service] | |
| Type=simple | |
| User=${primary_user} | |
| WorkingDirectory=${WEB_DIR} | |
| Environment=PATH=${WEB_DIR}/.venv/bin:/usr/bin:/bin | |
| ExecStart=${WEB_DIR}/.venv/bin/uvicorn app:app --host 0.0.0.0 --port ${WEB_PORT} | |
| Restart=on-failure | |
| RestartSec=2 | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOF | |
| systemctl daemon-reload | |
| systemctl enable --now noxradio-web | |
| log "Enabled + started noxradio-web.service" | |
| } | |
| # --- verification --- | |
| verify_services() { | |
| log "Service status:" | |
| systemctl --no-pager --full status direwolf || true | |
| systemctl --no-pager --full status noxradio-web || true | |
| systemctl --no-pager --full status noxradio-ax25-bridge || true | |
| log "Listening ports:" | |
| ss -ltnp | grep -E ":(${KISS_PORT}|${WEB_PORT})" || true | |
| log "AX.25 interface (if created):" | |
| ip link show ax0 2>/dev/null || true | |
| } | |
| main() { | |
| [[ "${EUID}" -eq 0 ]] || die "Run as root: sudo $0" | |
| need_cmd apt-get | |
| need_cmd git | |
| need_cmd udevadm | |
| local primary_user | |
| primary_user="$(detect_primary_user)" | |
| log "Primary user: ${primary_user}" | |
| log "Updating OS packages..." | |
| apt-get update -y | |
| apt-get full-upgrade -y | |
| apt_install git build-essential cmake pkg-config \ | |
| libasound2-dev libudev-dev alsa-utils socat \ | |
| python3 python3-venv python3-pip \ | |
| ax25-tools ax25-apps libax25 || true | |
| if ! command -v kissattach >/dev/null 2>&1; then | |
| warn "kissattach not found after install. AX.25 bridge may not work until ax25-tools/ax25-apps packages are available on your OS." | |
| fi | |
| need_cmd arecord | |
| need_cmd aplay | |
| log "Detecting AIOC devices (audio + PTT)..." | |
| local audio_card hid_node tty_node | |
| audio_card="$(detect_audio_capture_card)" | |
| hid_node="$(detect_ptt_hidraw_device)" | |
| tty_node="$(detect_serial_tty_device)" | |
| [[ -n "${audio_card}" ]] && log "Detected USB capture card index: ${audio_card}" || warn "No USB capture device detected yet. Plug in the AIOC and re-run this script." | |
| [[ -n "${hid_node}" ]] && log "Detected hidraw device for PTT: ${hid_node}" || warn "No /dev/hidraw* detected (CM108 PTT). Will try serial PTT if available." | |
| [[ -n "${tty_node}" ]] && log "Detected serial device: ${tty_node}" || warn "No /dev/ttyACM0 or /dev/ttyUSB0 detected." | |
| ensure_direwolf_user | |
| usermod -aG audio,dialout,input "${primary_user}" || true | |
| if [[ -n "${hid_node}" || -n "${tty_node}" ]]; then | |
| write_udev_rules "${hid_node}" "${tty_node}" | |
| else | |
| warn "Skipping udev rules (no hidraw/tty detected)." | |
| fi | |
| install_direwolf | |
| # Prompt user for callsign (validate and re-prompt if bad) | |
| local mycall | |
| while true; do | |
| mycall="$(prompt_callsign)" | |
| if validate_callsign "${mycall}"; then | |
| break | |
| fi | |
| warn "Callsign must be at least 4 characters. Try again." | |
| done | |
| log "Using MYCALL: ${mycall}" | |
| write_direwolf_config "${audio_card}" "${hid_node}" "${tty_node}" "${mycall}" | |
| write_direwolf_service | |
| write_modules_load | |
| write_axports "${mycall}" | |
| if command -v kissattach >/dev/null 2>&1; then | |
| write_ax25_bridge_script | |
| write_ax25_bridge_service | |
| else | |
| warn "Skipping AX.25 bridge service (kissattach missing)." | |
| fi | |
| write_web_app | |
| setup_web_venv "${primary_user}" | |
| write_web_service | |
| local ip | |
| ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)" | |
| echo "" | |
| echo "============================================================" | |
| echo " Install complete" | |
| echo "============================================================" | |
| echo "Direwolf config: ${DW_CONF}" | |
| echo "Web UI: http://${ip:-<pi-ip>}:${WEB_PORT}" | |
| echo "KISS TCP: 127.0.0.1:${KISS_PORT}" | |
| echo "" | |
| echo "Logs:" | |
| echo " sudo journalctl -u direwolf -n 120 --no-pager" | |
| echo " sudo journalctl -u noxradio-web -n 120 --no-pager" | |
| echo " sudo journalctl -u noxradio-ax25-bridge -n 120 --no-pager" | |
| echo "" | |
| verify_services | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment