Added auto frequency sensing script for 991a to run direwolf with HF, VHF or UHF config files automatically.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# There us a udev rule which creates "/dev/radio/ft99a1-00" and "/dev/radio/ft991a-01" so that we
|
||||
# do not have to worry about what USB port or tty number is assigned when this radio is connected
|
||||
# to the computer.
|
||||
|
||||
# We can reference this audio device by name already.
|
||||
ADEVICE plughw:CODEC,0
|
||||
|
||||
# The custom udev rule makes this device available by name.
|
||||
PTT /dev/radio/ft991a-01 RTS
|
||||
|
||||
MYCALL K0BIT
|
||||
|
||||
MODEM 300 1600:1800 D
|
||||
#MODEM 300 1600:1800
|
||||
|
||||
# Enable FX.25 with fallback to regular AX.25
|
||||
FX25TX 1
|
||||
|
||||
KISSPORT 8005
|
||||
AGWPORT 8006
|
||||
@@ -0,0 +1,27 @@
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# There us a udev rule which creates "/dev/radio/ft99a1-00" and "/dev/radio/ft991a-01" so that we
|
||||
# do not have to worry about what USB port or tty number is assigned when this radio is connected
|
||||
# to the computer.
|
||||
|
||||
# We can reference this audio device by name already.
|
||||
ADEVICE plughw:CODEC,0
|
||||
|
||||
# The custom udev rule makes this device available by name.
|
||||
PTT /dev/radio/ft991a-01 RTS
|
||||
|
||||
MYCALL K0BIT
|
||||
|
||||
MODEM 9600
|
||||
|
||||
# Enable FX.25 with fallback to regular AX.25
|
||||
FX25TX 1
|
||||
|
||||
KISSPORT 8005
|
||||
AGWPORT 8006
|
||||
210
ansible/roles/scanner_ft991a/templates/ft991a_monitor.py
Normal file
210
ansible/roles/scanner_ft991a/templates/ft991a_monitor.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
from typing import Optional, Dict
|
||||
|
||||
# --- Configuration (Centralized) ---
|
||||
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
|
||||
|
||||
# !!! CONFIGURATION FILES !!!
|
||||
'CONFIG_FILES': {
|
||||
'HF': '/opt/busnet/direwolf/config/ft991a-hf.conf',
|
||||
'VHF': '/opt/busnet/direwolf/config/ft991a-vhf.conf',
|
||||
'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,
|
||||
|
||||
'DIREWOLF_ARGS': ['-t', '0', '-X', '1'],
|
||||
}
|
||||
|
||||
|
||||
class ModeSwitcherDaemon:
|
||||
def __init__(self, config: Dict):
|
||||
self.rigctld_host = config['RIGCTLD_HOST']
|
||||
self.rigctld_port = config['RIGCTLD_PORT']
|
||||
self.rigctld_timeout = config['RIGCTLD_TIMEOUT']
|
||||
self.poll_interval = config['POLL_INTERVAL']
|
||||
self.config_files = config['CONFIG_FILES']
|
||||
self.direwolf_path = DIREWOLF_PATH
|
||||
self.direwolf_args = config['DIREWOLF_ARGS']
|
||||
|
||||
self.direwolf_process: Optional[subprocess.Popen] = None
|
||||
self.current_band: Optional[str] = None
|
||||
self._shutdown_flag = False
|
||||
|
||||
self.hf_min = config['HF_MIN_FREQ']
|
||||
self.hf_max = config['HF_MAX_FREQ']
|
||||
self.vhf_max = config['VHF_MAX_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')
|
||||
|
||||
# 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
|
||||
|
||||
for line in lines:
|
||||
if line.startswith('Frequency:'):
|
||||
frequency_str = line.split(':')[1].strip()
|
||||
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:
|
||||
return 'HF'
|
||||
elif frequency <= self.vhf_max:
|
||||
return 'VHF'
|
||||
elif frequency <= self.uhf_max:
|
||||
return 'UHF'
|
||||
else:
|
||||
return 'UNKNOWN'
|
||||
|
||||
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)
|
||||
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...")
|
||||
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:
|
||||
self.direwolf_process = None
|
||||
self.current_band = None
|
||||
|
||||
def _run_direwolf(self, target_band: str):
|
||||
# Idempotency check
|
||||
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 self.direwolf_process:
|
||||
print(f"INFO: Switching Dire Wolf 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}")
|
||||
|
||||
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.")
|
||||
|
||||
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:
|
||||
self._run_direwolf(target_band)
|
||||
|
||||
# Simple sleep loop to allow signal interrupt
|
||||
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.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if not os.path.exists(DIREWOLF_PATH):
|
||||
print(f"FATAL: Dire Wolf executable not found at {DIREWOLF_PATH}.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
daemon = ModeSwitcherDaemon(CONFIG)
|
||||
daemon.run()
|
||||
Reference in New Issue
Block a user