many modifications to digirg and yaesu 991a packet.

This commit is contained in:
Craig McDaniel
2025-12-26 12:41:56 -06:00
parent 51fb12d325
commit bb5b8b95b4
14 changed files with 132 additions and 192 deletions
+26 -36
View File
@@ -34,10 +34,27 @@
- audio
- plugdev
- name: Grant full passwordless sudo access to busnet user
ansible.builtin.copy:
content: "busnet ALL=(ALL) NOPASSWD:ALL"
dest: /etc/sudoers.d/busnet
mode: '0440'
owner: root
group: root
validate: '/usr/sbin/visudo -cf %s'
###################################################################################################
# DIREWOLF CONF
# AX.25 BASH / DIREWOLF
###################################################################################################
- name: Copy digirig-direwolf.sh AX.25 stack script
copy:
src: "templates/digirig-direwolf.sh"
dest: "/opt/busnet/direwolf/digirig-direwolf.sh"
owner: busnet
group: busnet
mode: "0755"
- name: Copy Direwolf config for Digirig
template:
src: "templates/digirig-direwolf.conf.j2"
@@ -50,8 +67,15 @@
# SYSTEMD
###################################################################################################
- name: Copy AX.25 bash script to start direwolf and utils
template:
src: "templates/digirig-direwolf.conf.j2"
dest: "/opt/busnet/direwolf/config/digirig.conf"
owner: busnet
group: busnet
# udev rules start this automatically when the radio USB is connected, and stop when disconnected.
- name: Install systemd unit file for Direwolf and Digirig USB sound card
- name: Install systemd unit file for Direwolf/AX.25 stack
template:
src: "templates/digirig-direwolf.service.j2"
dest: "/etc/systemd/system/digirig-direwolf.service"
@@ -65,40 +89,6 @@
daemon_reload: true
enabled: true
# socat creates a full duplex/bidirectional pipe between a serial port /dev/radio/digirig-tnc 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.
- name: Install systemd unit file for digirig-socat
template:
src: "templates/digirig-socat.service.j2"
dest: "/etc/systemd/system/digirig-socat.service"
owner: root
group: root
mode: "0644"
- name: Enable digirig-socat service
systemd:
name: "digirig-socat"
daemon_reload: true
enabled: true
# This starts after digirig-socat and makes the attachment to the new serial device we just created
# with socat! This effectively connects the Linux AX.25 system to direwolf's KISS TCP port.
- name: Install systemd unit file for digirig-kissattach
template:
src: "templates/digirig-kissattach.service.j2"
dest: "/etc/systemd/system/digirig-kissattach.service"
owner: root
group: root
mode: "0644"
- name: Enable digirig-kissattach service
systemd:
name: "digirig-kissattach"
daemon_reload: true
enabled: true
###################################################################################################
# UDEV RULES
###################################################################################################
@@ -15,13 +15,10 @@
SUBSYSTEM=="tty", ENV{ID_SERIAL_SHORT}=="809986b5195bed119acbed165820eef3", SYMLINK+="radio/digirig-sound", MODE="660", GROUP="plugdev"
# Automatically start direwolf when the radio is connected. Identify the radio by the
# Automatically start direwolf bash script when the radio is connected. Identify the radio by the
# unique setial number.
# - This direwolf instance is only for this radio.
# ---------------------------------------------------------------------------------------------------
ACTION=="add", SUBSYSTEM=="tty", ENV{ID_SERIAL_SHORT}=="809986b5195bed119acbed165820eef3", TAG+="systemd" ENV{SYSTEMD_WANTS}+="digirig-direwolf.service"
# Automatically stop direwolf when the radio is connected.
# ---------------------------------------------------------------------------------------------------
ACTION=="remove", SUBSYSTEM=="tty", ENV{ID_SERIAL_SHORT}=="809986b5195bed119acbed165820eef3",TAG+="systemd" ENV{SYSTEMD_WANTS}-="digirig-direwolf.service"
# Note: We cannot auto-stop direwolf bash script here. We use systemd for that. See the unit file.
@@ -1,6 +1,7 @@
# This file is managed by BusNet Ansible
#
# This is the Direwolf configuration for connecting Direwolf to the Digirig USB sound card.
# This is the Direwolf configuration for connecting Direwolf to the Digirig USB sound card. This assumes
# that we will operate using 1200 baud mode in 2 meters VHF with a dedicated radio for this purpose.
#
# There us a udev rule which creates "/dev/radio/digirig-sound" so that we do not have to worry about
# what USB port or tty number is assigned when this device is connected to the computer.
@@ -1,24 +1,31 @@
# This file is managed by BusNet Ansible.
#
# This service starts Direwolf when the Digirig device is connected to this computer.
# This service starts the direwolf/AX.25 stack bash script when the Digirig device is connected to
# this computer. The bash script then initializes the Linux AX.25 stack.
#
# There is a corresponding udev rule that triggers this service automatically when it detects that
# the radio's USB devices are connected. There is also a udev rule that shuts it down when the USB
# is disconnected.
[Unit]
Description=Direwolf TNC for Digirig
Description=Direwolf TNC and AX.25 Stack
#After=network.target
# Automatically start these other services to complete the Linux AX25 stack.
Requires=digirig-socat.service
Requires=digirig-kissattach.service
# Link the service life cycle to the hardware device /dev/radio/digirig-sound. I don't know why, but
# you can't specify the file location of the device here. You have to do this syntax. Note that the
# udev rule creates this device for the digirig.
BindsTo=dev-radio-digirig\x2dsound.device
After=dev-radio-digirig\x2dsound.device
[Service]
ExecStart=/usr/local/bin/direwolf -q hdx -t 0 -X 1 -c /opt/busnet/direwolf/config/digirig.conf
Type=simple
ExecStart=/bin/bash /opt/busnet/direwolf/digirig-direwolf.sh
Restart=on-failure
RestartSec=5
User=busnet
Group=busnet
# This tells systemd to kill all child processe started by the bash script.
KillMode=control-group
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target
@@ -1,26 +0,0 @@
[Unit]
Description=AX.25 Interface Setup via kissattach for Digirig
# We must start AFTER the socat service has created the virtual TNC link.
#Requires=digirig-socat.service
After=digirig-socat.service
[Service]
Type=Forking # kissattach will fork itself and run as a daemon in the background
User=root
Restart=always
RestartSec=10s
# Wait 6 seconds to give both direwolf and socat time to start.
ExecStartPre=/bin/sleep 6
# This PTY is created by the digirg-socat service, which is a pipe between this device and
# Direwolf's KISS port for the digirig. Kissattach cannot use KISS over TCP directly to Direwolf
# so it needs the extra step of using socat.
ExecStart=/usr/sbin/kissattach /dev/radio/digirig-tnc digirig
# We must do this because it forked itself.
ExecStop=/usr/bin/pkill -f 'kissattach /dev/radio/digirig-tnc digirig'
[Install]
WantedBy=multi-user.target
@@ -1,22 +0,0 @@
[Unit]
Description=SOCAT Virtual Serial Port Bridge to Digirig Direwolf
# This ensures socat only starts AFTER the main Dire Wolf service is active.
#Requires=digirig-direwolf.service
After=digirig-direwolf.service
[Service]
User=root
Restart=always
RestartSec=5s
# Wait 3 seconds to give Direwolf time to start.
ExecStartPre=/bin/sleep 3
# socat creates a full duplex/bidirectional pipe between a serial port /dev/radio/digirig-tnc 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.
ExecStart=/usr/bin/socat PTY,raw,echo=0,link=/dev/radio/digirig-tnc TCP4:127.0.0.1:8001
[Install]
WantedBy=multi-user.target
@@ -22,8 +22,5 @@ SUBSYSTEM=="tty", ENV{ID_SERIAL_SHORT}=="01A9F7D6", SYMLINK+="radio/ft991a-$env{
ACTION=="add", SUBSYSTEM=="tty", ENV{ID_SERIAL_SHORT}=="01A9F7D6", TAG+="systemd" ENV{SYSTEMD_WANTS}+="ft991a-rigctld.service"
ACTION=="add", SUBSYSTEM=="tty", ENV{ID_SERIAL_SHORT}=="01A9F7D6", TAG+="systemd" ENV{SYSTEMD_WANTS}+="ft991a-direwolf.service"
# Automatically stop rigctld and direwolf when the radio is connected.
# ---------------------------------------------------------------------------------------------------
ACTION=="remove", SUBSYSTEM=="tty", ENV{ID_SERIAL_SHORT}=="01A9F7D6",TAG+="systemd" ENV{SYSTEMD_WANTS}-="ft991a-rigctld.service"
ACTION=="remove", SUBSYSTEM=="tty", ENV{ID_SERIAL_SHORT}=="01A9F7D6",TAG+="systemd" ENV{SYSTEMD_WANTS}-="ft991a-direwolf.service"
# Note: We cannot auto-stop rigctld and direwolf here. We use systemd for that. See each service
# systemd unit file.
@@ -24,5 +24,5 @@ MODEM 300 1600:1800 D
# Enable FX.25 with fallback to regular AX.25
FX25TX 1
KISSPORT 8005
AGWPORT 8006
AGWPORT 8005
KISSPORT 8006
@@ -1,7 +1,7 @@
# This file is managed by BusNet Ansible
#
# This is the Direwolf configuration for connecting Direwolf to the Yaesu FT-991a radio when it is
# plugged in via USB and tuned to an HF frequency.
# plugged in via USB and tuned to an UHF frequency.
#
# Note that only one direwolf will ever be running at the same time for this 991a radio, depending
# on what frequency it is tuned to. See /opt/busnet/direwolf/ft991a_monitor.py
@@ -23,5 +23,5 @@ MODEM 9600
# Enable FX.25 with fallback to regular AX.25
FX25TX 1
KISSPORT 8005
AGWPORT 8006
AGWPORT 8005
KISSPORT 8006
@@ -1,7 +1,7 @@
# This file is managed by BusNet Ansible
#
# This is the Direwolf configuration for connecting Direwolf to the Yaesu FT-991a radio when it is
# plugged in via USB and tuned to an HF frequency.
# plugged in via USB and tuned to an VHF frequency.
#
# Note that only one direwolf will ever be running at the same time for this 991a radio, depending
# on what frequency it is tuned to. See /opt/busnet/direwolf/ft991a_monitor.py
@@ -23,5 +23,5 @@ MODEM 1200
# Enable FX.25 with fallback to regular AX.25
FX25TX 1
KISSPORT 8005
AGWPORT 8006
AGWPORT 8005
KISSPORT 8006
@@ -9,6 +9,12 @@
[Unit]
Description=Direwolf TNC for Yaesu FT-991a Auto Frequency Sensing
# Link the service life cycle to the hardware device /dev/radio/ft99a1-00. I don't know why, but
# you can't specify the file location of the device here. You have to do this syntax. Note that
# the udev rule creates this device.
BindsTo=dev-radio-ft991a\x2d01.device
After=dev-radio-ft991a\x2d01.device
[Service]
ExecStartPre=/bin/sleep 5
@@ -11,6 +11,12 @@
[Unit]
Description=rigctld for Yaesu FT-991a
# Link the service life cycle to the hardware device /dev/radio/ft99a1-01. I don't know why, but
# you can't specify the file location of the device here. You have to do this syntax. Note that
# the udev rule creates this device.
BindsTo=dev-radio-ft991a\x2d01.device
After=dev-radio-ft991a\x2d01.device
[Service]
ExecStartPre=/bin/sleep 4
# It uses device symlink /dev/radio/ft991a-00 which is the radio's CAT USB interface
@@ -1,4 +1,25 @@
#!/usr/bin/env python3
# @author Craig McDaniel using Google Gemini LLM
#
# This script polls the Yeasu FT-991a by connecting to a running rigctld daemon and asking that
# daemon what the current radio's frequency is. If the radio is in any HF frequency, it makes
# sure Direwolf is running with a HF AX.25 configuration at 300 baud. If a VHF or UHF frequency
# is detected, it restarts Direwolf with a configuration suitable for that band
#
# * Note that ANY HF frequency will trigger a Direwolf restart with HF config.
#
# * For VHF anf UHF, specific ranges are specified as to only run Direwolf when the radio is
# detected to be in the accepted packet frequencies of each band. The reason is to stop the
# start and restart of Direwolf when the radio is in scan mode, and it constantly scans both
# VHF and UHF frequencies. This will randomly trigger a reload of Direwolf over and over
# when the 5 second poll randomly returns a VHF or UHF frequency. I think scanning data freqs
# are not useful, and a operator would not normally scan those. But they will scan simplex and
# repeater output frequencies, but those aren't for data, so no need to restart Direwolf.
#
# * If this script detects that the radio is on an out of range frequency, it does nothing. If
# Direwolf is already running, it leaves it alone. As soon as it detects that the radio is within
# range of a HF-UHF packet frequency, it makes sure Direwolf is running with the correct config.
#
import socket
import subprocess
@@ -13,9 +34,9 @@ DIREWOLF_PATH = '/usr/local/bin/direwolf'
CONFIG = {
'RIGCTLD_HOST': 'localhost',
'RIGCTLD_PORT': 4000, # Port 4000
'RIGCTLD_TIMEOUT': 5.0, # Timeout
'POLL_INTERVAL': 5.0, # Poll interval
'RIGCTLD_PORT': 4000,
'RIGCTLD_TIMEOUT': 5.0,
'POLL_INTERVAL': 5.0,
# !!! CONFIGURATION FILES !!!
'CONFIG_FILES': {
@@ -24,11 +45,17 @@ CONFIG = {
'UHF': '/opt/busnet/direwolf/config/ft991a-uhf.conf',
},
# Frequency Band Definitions (in Hz)
'HF_MIN_FREQ': 1_800_000,
'HF_MAX_FREQ': 30_000_000,
'VHF_MAX_FREQ': 148_000_000,
'UHF_MAX_FREQ': 450_000_000,
# --- Narrowed Frequency Windows (in Hz) ---
# Edit these to the exact ranges you use for Digital modes.
# Frequencies outside these ranges will cause Dire Wolf to stop.
'HF_MIN_FREQ': 1_800_000,
'HF_MAX_FREQ': 30_000_000,
'VHF_MIN_FREQ': 144_390_000, # This captures APRS at 143390
'VHF_MAX_FREQ': 145_100_000, # and various commonly used packet frequencies between 145000 - 145100
'UHF_MIN_FREQ': 432_000_000,
'UHF_MAX_FREQ': 433_000_000,
'DIREWOLF_ARGS': ['-t', '0', '-X', '1'],
}
@@ -48,41 +75,28 @@ class ModeSwitcherDaemon:
self.current_band: Optional[str] = None
self._shutdown_flag = False
# Ranges
self.hf_min = config['HF_MIN_FREQ']
self.hf_max = config['HF_MAX_FREQ']
self.vhf_min = config['VHF_MIN_FREQ']
self.vhf_max = config['VHF_MAX_FREQ']
self.uhf_min = config['UHF_MIN_FREQ']
self.uhf_max = config['UHF_MAX_FREQ']
def _get_frequency_and_band(self) -> Optional[str]:
"""
Connects, queries, SHUTS DOWN WRITE (EOF), and reads response.
"""
temp_sock: Optional[socket.socket] = None
try:
# 1. Connect
temp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
temp_sock.settimeout(self.rigctld_timeout)
# Disable Nagle's algorithm for speed
temp_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
temp_sock.connect((self.rigctld_host, self.rigctld_port))
# 2. Send Command (Matches your echo "+\get_freq")
# We explicitly send the backslash + newline.
temp_sock.sendall(b'+\\get_freq\n')
temp_sock.shutdown(socket.SHUT_WR) # Send EOF signal
# CRITICAL FIX: Send EOF (End of File) signal.
# This tells rigctld "I am done writing", forcing it to process and reply.
# This mimics the behavior of piping echo into netcat.
temp_sock.shutdown(socket.SHUT_WR)
# 3. Read Response
full_response = temp_sock.recv(4096).decode('utf-8')
# 4. Clean up
temp_sock.close()
# 5. Parse
lines = full_response.strip().split('\n')
frequency_str = None
@@ -92,48 +106,31 @@ class ModeSwitcherDaemon:
break
if frequency_str is None or not frequency_str.isdigit():
if full_response:
print(f"WARN: Invalid response format: {full_response.replace('\n', ' ')}", file=sys.stderr)
return None
frequency = int(frequency_str)
if frequency < self.hf_min:
return 'UNKNOWN'
elif frequency <= self.hf_max:
# Use strict bounded windows to avoid voice scan triggers
if self.hf_min <= frequency <= self.hf_max:
return 'HF'
elif frequency <= self.vhf_max:
elif self.vhf_min <= frequency <= self.vhf_max:
return 'VHF'
elif frequency <= self.uhf_max:
elif self.uhf_min <= frequency <= self.uhf_max:
return 'UHF'
else:
return 'UNKNOWN'
return 'UNKNOWN' # Not in a managed digital window
except socket.timeout:
print(f"ERROR: rigctld response timed out ({self.rigctld_timeout}s).", file=sys.stderr)
return None
except socket.error as e:
print(f"ERROR: Socket error: {e}", file=sys.stderr)
return None
except Exception as e:
print(f"ERROR: Unexpected error: {e}", file=sys.stderr)
print(f"ERROR: rigctld communication error: {e}", file=sys.stderr)
return None
finally:
if temp_sock:
try:
temp_sock.close()
except:
pass
def _kill_direwolf_process(self):
if self.direwolf_process:
print("INFO: Shutting down Dire Wolf child process...")
print(f"INFO: Terminating Dire Wolf on {self.current_band}...")
try:
self.direwolf_process.terminate()
self.direwolf_process.wait(timeout=5)
print("INFO: Dire Wolf terminated successfully.")
except subprocess.TimeoutExpired:
print("WARN: Dire Wolf unresponsive, killing process.")
self.direwolf_process.kill()
self.direwolf_process.wait()
finally:
@@ -141,65 +138,50 @@ class ModeSwitcherDaemon:
self.current_band = None
def _run_direwolf(self, target_band: str):
# Idempotency check
# Idempotency check: Already running on the right band?
if self.direwolf_process and self.direwolf_process.poll() is None and self.current_band == target_band:
return
# Cleanup dead process
if self.direwolf_process and self.direwolf_process.poll() is not None:
print(f"WARN: Dire Wolf process died unexpectedly (Exit Code: {self.direwolf_process.returncode}). Restarting for {target_band}.")
self.direwolf_process = None
# Switch bands
# If running on the wrong band, kill it first
if self.direwolf_process:
print(f"INFO: Switching Dire Wolf from {self.current_band} to {target_band}.")
print(f"INFO: Band changed. Switching from {self.current_band} to {target_band}.")
self._kill_direwolf_process()
try:
config_file = self.config_files[target_band]
print(f"INFO: Launching Dire Wolf with config: {config_file}")
print(f"INFO: Launching Dire Wolf for {target_band}...")
command_args = [self.direwolf_path] + self.direwolf_args + ['-c', config_file]
self.direwolf_process = subprocess.Popen(command_args)
self.current_band = target_band
print(f"INFO: Dire Wolf started (PID: {self.direwolf_process.pid}).")
except KeyError:
print(f"FATAL: Configuration file not defined for band '{target_band}'.", file=sys.stderr)
self._shutdown_flag = True
except Exception as e:
print(f"FATAL: Failed to launch Dire Wolf: {e}", file=sys.stderr)
self._shutdown_flag = True
def handle_signal(self, signum, frame):
print(f"\nINFO: Signal {signum} received. Initiating graceful shutdown...")
self._shutdown_flag = True
def run(self):
signal.signal(signal.SIGINT, self.handle_signal)
signal.signal(signal.SIGTERM, self.handle_signal)
print(f"INFO: Starting Mode Switcher Daemon.")
print("INFO: FT-991A Frequency Monitor Started.")
while not self._shutdown_flag:
target_band = self._get_frequency_and_band()
if target_band is None:
pass
elif target_band == 'UNKNOWN':
print("WARN: Frequency is outside defined ranges. Dire Wolf is not managed.")
else:
# Change: Only act if we find a valid digital band.
# If target_band is 'UNKNOWN' or None (comm error), we do absolutely nothing.
if target_band in ['HF', 'VHF', 'UHF']:
self._run_direwolf(target_band)
# Simple sleep loop to allow signal interrupt
# Poll interval sleep
for _ in range(int(self.poll_interval * 10)):
if self._shutdown_flag: break
time.sleep(0.1)
self._kill_direwolf_process()
print("INFO: Script exited gracefully.")
print("INFO: Monitor exited.")
if __name__ == '__main__':
if not os.path.exists(DIREWOLF_PATH):
@@ -14,7 +14,9 @@
"connect_aliases": {
"telnet": "telnet://{mycall}:CMSTelnet@cms.winlink.org:8772/wl2k"
},
"listen": [],
"listen": [
"ax25"
],
"hamlib_rigs": {
"ft-991a": {"address": "localhost:4000", "network": "tcp"}
},
@@ -22,17 +24,16 @@
"engine": "linux",
"rig": "",
"beacon": {
"every": 0,
"message": "",
"destination": ""
"every": 750,
"message": "This is my Pat Winlink client for P2P Winlink Email. Connect directly and send me a message.",
"destination": "IDENT"
}
},
"ax25_linux": {
"port": "wl2k"
"port": "digirig"
},
"agwpe": {
"name": "hehehe",
"addr": "scanner.busnet:8011",
"addr": "scanner.busnet:8005",
"radio_port": 0
},
"serial-tnc": {
@@ -79,7 +80,8 @@
"enable_http": false,
"allow_forms": false,
"use_server_time": false,
"addr": "localhost:2947"
"update_locator": true,
"addr": "192.168.7.1:2947"
},
"schedule": {},
"version_reporting_disabled": false