diff --git a/ansible/roles/argus_airspy_aprs/tasks/main.yml b/ansible/roles/argus_airspy_aprs/tasks/main.yml index fa4db94..680be3d 100644 --- a/ansible/roles/argus_airspy_aprs/tasks/main.yml +++ b/ansible/roles/argus_airspy_aprs/tasks/main.yml @@ -1,10 +1,9 @@ -# Role: scanner_airspy_aprs # Configure AirSpy SDR for APRS reception on VHF 144.390 MHz # # This role sets up: # - airspy-fmradion for FM demodulation # - Direwolf for APRS packet decoding -# - Full AX.25 stack integration (receive-only) +# - Linux kernel AX.25 stack for receive only demodulation of AX.25 packets # # NOTE: This role depends on scanner_digirig having been run first to set up: # - AX.25 packages and kernel modules @@ -12,61 +11,52 @@ # - Base directory structure # -- name: Create direwolf directories +- name: Create directories file: path: "{{item}}" state: directory - owner: busnet - group: busnet + owner: argus + group: argus mode: u=rwx,g=rwx,o=rx with_items: - - /opt/busnet/direwolf - - /opt/busnet/direwolf/config - - /opt/busnet/direwolf/logs - -################################################################################################### -# Package Dependencies -################################################################################################### - -- name: Install socat for PTY bridging - ansible.builtin.apt: - name: - - socat - state: present + - /opt/argus/scripts + - /opt/argus/direwolf + - /opt/argus/direwolf/config + - /opt/argus/direwolf/logs ################################################################################################### # AirSpy bash script and Direwolf config ################################################################################################### -- name: Copy airspy_aprs.sh AX.25 stack script +- name: Copy aprs_airspy.sh AX.25 stack script copy: - src: "templates/airspy_aprs.sh" - dest: "/opt/busnet/direwolf/airspy_aprs.sh" - owner: busnet - group: busnet + src: "templates/aprs_airspy.sh" + dest: "/opt/argus/scripts/aprs_airspy.sh" + owner: argus + group: argus mode: "0755" - name: Copy Direwolf config for AirSpy APRS template: src: "templates/direwolf_airspy_aprs.conf" - dest: "/opt/busnet/direwolf/config/airspy-aprs.conf" - owner: busnet - group: busnet + dest: "/opt/argus/direwolf/config/airspy-aprs.conf" + owner: argus + group: argus ################################################################################################### # SYSTEMD ################################################################################################### -- name: Install systemd unit file for AirSpy APRS +- name: Install systemd unit file for argus-aprs-airspy.service.j2 template: - src: "templates/airspy-aprs.service.j2" - dest: "/etc/systemd/system/airspy-aprs.service" + src: "templates/argus-aprs-airspy.service.j2" + dest: "/etc/systemd/system/argus-aprs-airspy.service" owner: root group: root mode: "0644" -- name: Enable airspy-aprs service +- name: Enable argus-aprs-airspy.service systemd: - name: "airspy-aprs" + name: "argus-aprs-airspy.service" daemon_reload: true enabled: true diff --git a/ansible/roles/argus_airspy_aprs/templates/airspy-aprs.service.j2 b/ansible/roles/argus_airspy_aprs/templates/airspy-aprs.service.j2 deleted file mode 100644 index ded3e56..0000000 --- a/ansible/roles/argus_airspy_aprs/templates/airspy-aprs.service.j2 +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=AirSpy APRS Receiver with Direwolf and AX.25 Stack -Documentation=https://github.com/wb2osz/direwolf -After=network.target - -[Service] -Type=simple -ExecStart=/opt/busnet/direwolf/airspy_aprs.sh -Restart=on-failure -RestartSec=10 -User=busnet -Group=busnet - -# Give it time to start up properly -TimeoutStartSec=30 - -[Install] -WantedBy=multi-user.target diff --git a/ansible/roles/argus_airspy_aprs/templates/airspy_aprs.sh b/ansible/roles/argus_airspy_aprs/templates/airspy_aprs.sh deleted file mode 100644 index 21ebced..0000000 --- a/ansible/roles/argus_airspy_aprs/templates/airspy_aprs.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash -# @author Craig McDaniel -# -# This script runs airspy-fmradion to demodulate FM signals from the AirSpy SDR, pipes the audio -# to Direwolf for APRS packet decoding, then sets up the AX.25 stack for kernel integration. -# -# This is receive-only APRS monitoring on VHF 144.390 MHz. -# - -# Configuration -FMRADION_CMD="airspy-fmradion -m nbfm -t airspy -q -M -c freq=144390000,srate=2500000,lagc,magc,lgain=14,mgain=15,vgain=15" -DIREWOLF_CMD="direwolf -q d -c /opt/busnet/direwolf/config/airspy-aprs.conf -n 1 -r 48000 -b 16 -" -SOCAT_CMD="/usr/bin/socat PTY,raw,echo=0,link=/dev/radio/airspy-tnc TCP4:127.0.0.1:8003" -KISSATTACH_CMD="/usr/sbin/kissattach /dev/radio/airspy-tnc airspy" -KISSPARAMS_CMD="kissparms -c 5 -p airspy" - -cleanup() { - echo - echo "Exit signal detected. Cleaning up..." - trap - SIGTERM # prevent recursion - kill 0 - exit -} - -# Trap exit signals -trap cleanup SIGINT SIGTERM - -echo -echo "======================================================================" -echo "Starting AirSpy FM Demodulator → Direwolf pipeline..." -echo "======================================================================" -echo -# Start the pipeline: airspy-fmradion pipes audio to direwolf -$FMRADION_CMD | $DIREWOLF_CMD & -PIPELINE_PID=$! - -sleep 1 -if ! kill -0 $PIPELINE_PID 2>/dev/null; then - echo "Pipeline failed to start." - exit 1 -fi - -# Wait 3 seconds for Direwolf to initialize and open KISS TCP port 8003 -sleep 3 - -echo -echo "======================================================================" -echo "Starting socat PTY bridge..." -echo "======================================================================" -echo -# Start this in the background -sudo $SOCAT_CMD & -SOCAT_PID=$! -if ! kill -0 $SOCAT_PID 2>/dev/null; then - echo "Socat failed to start." - kill $PIPELINE_PID - exit 1 -fi - -# Wait for PTY device to be created -sleep 1 - -echo -echo "======================================================================" -echo "Starting kissattach and setting kissparams..." -echo "======================================================================" -echo -# Kissattach will automatically fork itself in background. -sudo $KISSATTACH_CMD -if [ $? -ne 0 ]; then - echo "kissattach failed to start." - cleanup -fi - -# Configure AX.25 parameters for receive only. This is probably not necessary? -# Maybe somebody will do a bunch of tests and see if it actually matters one day. -sudo $KISSPARAMS_CMD - -echo -echo "AirSpy APRS stack online! Monitoring processes..." - -# 'wait -n' waits for ANY background process to exit. -# It returns as soon as the pipeline or socat dies. -wait -n - -# If we get here, one of the processes died. -cleanup diff --git a/ansible/roles/argus_airspy_aprs/templates/aprs_airspy.sh b/ansible/roles/argus_airspy_aprs/templates/aprs_airspy.sh new file mode 100644 index 0000000..ed38ee5 --- /dev/null +++ b/ansible/roles/argus_airspy_aprs/templates/aprs_airspy.sh @@ -0,0 +1,193 @@ +#!/bin/bash +# Start the APRS Receive-Only APRS AX.25 Stack +# @author Craig McDaniel +# +# This script runs airspy-fmradion to demodulate FM signals from the AirSpy SDR, pipes the audio +# to Direwolf for APRS packet decoding, then sets up the AX.25 stack for kernel integration. +# +# This is receive-only APRS monitoring on VHF 144.390 MHz. +# +# +# Here's a diagram of the stack this creates: +# ------------------------------------------------------------------------------------------------- +# +# RF signals! _ RF signals! +# \ _/ +# \ / +# +--------+ +# | AirSpy | ----- RF signals! +# +--------+ +# | +# | USB cable +# | +# +----|----------------------------------------------------------------------------+ +# | | This is your computer. The things in here are virtual components. | +# | | | +# | +-----------------+ | +# | | airspy-fmradion | <--- software radio FM demodulator | +# | +-----------------+ | +# | | | +# | | <-- UNIX pipe! | +# | | | +# | +-----------+ +-------+ +------------+ Linux kernel | +# | | Direwolf | <---> | Socat | <----> | Kissattach | <------> ax1 interface | +# | +-----------+ +-------+ +------------+ / | +# | | /--------+ | +# | AGWPE port / | +# | available for +---------------+ | +# | receive only use. | mheard daemon | | +# | +---------------+ | +# +---------------------------------------------------------------------------------+ + +# This software demodulates the FM signal in software from the AirSpy receiver. +FMRADION_CMD="airspy-fmradion -m nbfm -t airspy -q -M -c freq=144390000,srate=2500000,lagc,magc,lgain=14,mgain=15,vgain=15" + +# Command to execute direwolf including any options and location of config file. +DIREWOLF_CMD="direwolf -q d -c /opt/argus/direwolf/config/airspy-aprs.conf -n 1 -r 48000 -b 16 -" + +# The AX.25 port for the radio defined in /etc/ax25/axports. Note that this is not the same thing as +# the kernel AX.25 interfaces, which are named things like ax0, ax1, etc... +AX25_PORT_NAME="airspy" + +# The PTY/TTY (whatever it's called) that socat will create for kissattach +TNC_DEV="/dev/radio/airspy-tnc" + +# This is the socat command to that connects one end of a virtual pipe to Direwolf's KISS TCP port, +# and the other end to the PTY/TTY. The IP and port specified here must match the Direwolf config +# file. +SOCAT_CMD="/usr/bin/socat PTY,raw,echo=0,link=${TNC_DEV} TCP4:127.0.0.1:8003" + +# These are the commands to create the kernel AX.25 interface using the kissattach utility. +KISSATTACH_CMD="/usr/sbin/kissattach ${TNC_DEV} ${AX25_PORT_NAME}" +KISSPARAMS_CMD="kissparms -c 5 -p ${AX25_PORT_NAME}" + + +# Cleanup the AX.25 stack +cleanup_stack() +{ + # Kill kissattach explicitly (it forks, so we can't track its PID) + sudo pkill -f "kissattach ${TNC_DEV} ${AX25_PORT_NAME}" + sleep 1 + + # Kill socat by PID + if [ -n "$SOCAT_PID" ]; then + sudo kill $SOCAT_PID 2>/dev/null + sleep 1 + fi + + # Kill demodulator/direwolf by PID and wait for it to finish + if [ -n "$PIPELINE_PID" ] && kill -0 $PIPELINE_PID 2>/dev/null; then + kill $PIPELINE_PID 2>/dev/null + wait $PIPELINE_PID 2>/dev/null + fi +} + + +# Start the AX.25 stack for APRS receive only. +start_stack() +{ + # We start the software that demodulates the FM signal then pipes its output stream directly + # to Direwolf. Direwolf config says to read from stdin. + $FMRADION_CMD | $DIREWOLF_CMD & + PIPELINE_PID=$! + # NOTE: If Direwolf encounters an error, it will just WAIT something like 15 seconds before + # exiting. It does not fail fast. That means that the following check will pass, but then + # we go to move on to create socat which will fail. I don't have a good solution for this + # case just yet, but I'm recording this note here. + if ! kill -0 $PIPELINE_PID; then + echo "Direwolf failed to start." + cleanup_stack + exit 1 + fi + echo "Fm Demodulator --> Direwolf started (PID: $PIPELINE_PID)" + + # Start socat PTY bridge. + # socat creates a full duplex/bidirectional pipe between a serial port ${TNC_DEV} and Direwolf's + # KISS TCP interface, which is running on localhost:8001. The kissattach program needs a serial + # port to work, and this is how we accomplish connecting kissattach to direwolf. + sleep 3 + sudo $SOCAT_CMD & + SOCAT_PID=$! + if ! kill -0 $SOCAT_PID; then + echo "Socat failed to start." + cleanup_stack + exit 1 + fi + echo "socat started (PID: $SOCAT_PID)" + + # Run kissatach to create an AX.25 network interface on the system. + # Kissattach annoyingly forks itself in the background, so we can't directly monitor the process. + # Kissattach cannot use KISS over TCP directly to Direwolf. It expects a serial terminal to exist, + # which is why we use socat to create the pipe between Direwolf's KISS TCP port and this serial + # device. + sleep 1 + sudo $KISSATTACH_CMD + if [ $? -ne 0 ]; then + echo "kissattach failed to start." + cleanup_stack + fi + + # Configure AX.25 parameters for receive only. This is probably not necessary? + # Maybe somebody will do a bunch of tests and see if it actually matters one day. + sudo $KISSPARAMS_CMD + + # Restart mheardd daemon. If mheardd was started before ax1, ax2 or other AX.25 kernel interface + # was created, and now we receive AX.25 packets on this new interface we just created, mheardd + # just throws errors about a new, unknown interface. Ugh. + if sudo systemctl is-enabled --quiet mheardd.service; then + echo "Restarting mheardd..." + sudo systemctl restart mheardd.service + fi +} + + +# This is our POSIX signal handler so we cleanly shut down before exiting. +handle_signal() +{ + echo "We received a SIGINT or SIGTERM signal. Shutting down APRS Receive-only AX.25 stack." + cleanup_stack + exit 0 +} + + +################################################################################################### +# +# MAIN ENTRY POINT +# +################################################################################################### + + +echo "" +echo "======================================================================" +echo "APRS Receive-only AX.25 Stack Started" +echo "======================================================================" +echo "" + +# This is the bash way of intercepting these signals. We execute the cleanup function when we receive +# either one. +trap handle_signal SIGINT SIGTERM + +# Load required kernel modules +echo "Loading kernel modules..." +sudo modprobe ax25 +if [ $? -ne 0 ]; then + echo "Failed to load ax25 kernel module" + exit 1 +fi + +# Clean up any orphaned kissattach processes from previous runs. We must do this because kissattach +# forks itself in the background, and we therefore cannot keep track of its PID. We use pkill to +# make sure no previous kissattach is running. +sudo pkill -f "kissattach ${TNC_DEV} ${AX25_PORT_NAME}" +sleep 1 + +# Start the AX.25 stack +start_stack +echo "All processes started. Monitoring via wait..." + +# 'wait -n' waits for ANY background process to exit. +# It returns as soon as fmradion, direwolf, or socat exits. Kissattach forks itself so we can't wait on it. +wait -n + +# If we get here, one of the processes died. Cleanup the stack and exit. +cleanup_stack diff --git a/ansible/roles/argus_airspy_aprs/templates/argus-aprs-airspy.service.j2 b/ansible/roles/argus_airspy_aprs/templates/argus-aprs-airspy.service.j2 new file mode 100644 index 0000000..925da61 --- /dev/null +++ b/ansible/roles/argus_airspy_aprs/templates/argus-aprs-airspy.service.j2 @@ -0,0 +1,17 @@ +[Unit] +Description=APRS AirSpy Receive-Only AX.25 Stack +After=network.target + +[Service] +Type=simple +ExecStart=/opt/argus/scripts/aprs_airspy.sh +Restart=on-failure +RestartSec=10 +User=argus +Group=argus + +# Give it time to start up properly +TimeoutStartSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/argus_airspy_aprs/templates/direwolf_airspy_aprs.conf b/ansible/roles/argus_airspy_aprs/templates/direwolf_airspy_aprs.conf index 2913167..ad27b55 100644 --- a/ansible/roles/argus_airspy_aprs/templates/direwolf_airspy_aprs.conf +++ b/ansible/roles/argus_airspy_aprs/templates/direwolf_airspy_aprs.conf @@ -8,7 +8,7 @@ ACHANNELS 1 ADEVICE stdin null ARATE 48000 -MYCALL K0BIT-2 +MYCALL N0CAL MODEM 1200 AGWPORT 8002