openwebrx/owrx/aprs/direwolf.py

209 lines
6.7 KiB
Python

from pycsdr.types import Format
from pycsdr.modules import Writer, TcpSource, ExecModule, Buffer
from csdr.module import LogReader
from owrx.config.core import CoreConfig
from owrx.config import Config
from abc import ABC, abstractmethod
import time
import os
import random
import socket
import logging
logger = logging.getLogger(__name__)
FEET_PER_METER = 3.28084
class DirewolfConfigSubscriber(ABC):
@abstractmethod
def onConfigChanged(self):
pass
class DirewolfConfig:
config_keys = [
"aprs_callsign",
"aprs_igate_enabled",
"aprs_igate_server",
"aprs_igate_password",
"receiver_gps",
"aprs_igate_symbol",
"aprs_igate_beacon",
"aprs_igate_gain",
"aprs_igate_dir",
"aprs_igate_comment",
"aprs_igate_height",
]
def __init__(self):
self.subscribers = []
self.configSub = None
self.port = None
def wire(self, subscriber: DirewolfConfigSubscriber):
self.subscribers.append(subscriber)
if self.configSub is None:
pm = Config.get()
self.configSub = pm.filter(*DirewolfConfig.config_keys).wire(self._fireChanged)
def unwire(self, subscriber: DirewolfConfigSubscriber):
self.subscribers.remove(subscriber)
if not self.subscribers and self.configSub is not None:
self.configSub.cancel()
def _fireChanged(self, changes):
for sub in self.subscribers:
try:
sub.onConfigChanged()
except Exception:
logger.exception("Error while notifying Direwolf subscribers")
def getPort(self):
# direwolf has some strange hardcoded port ranges
while self.port is None:
try:
port = random.randrange(1024, 49151)
# test if port is available for use
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("localhost", port))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.close()
self.port = port
except OSError:
pass
return self.port
def getConfig(self, is_service):
pm = Config.get()
config = """
ACHANNELS 1
ADEVICE stdin null
CHANNEL 0
MYCALL {callsign}
MODEM 1200
KISSPORT {port}
AGWPORT off
""".format(
port=self.getPort(), callsign=pm["aprs_callsign"]
)
if is_service and pm["aprs_igate_enabled"]:
pbeacon = ""
if pm["aprs_igate_beacon"]:
# Format beacon lat/lon
lat = pm["receiver_gps"]["lat"]
lon = pm["receiver_gps"]["lon"]
direction_ns = "N" if lat > 0 else "S"
direction_we = "E" if lon > 0 else "W"
lat = abs(lat)
lon = abs(lon)
lat = "{0:02d}^{1:05.2f}{2}".format(int(lat), (lat - int(lat)) * 60, direction_ns)
lon = "{0:03d}^{1:05.2f}{2}".format(int(lon), (lon - int(lon)) * 60, direction_we)
# Convert height from meters to feet if specified
height = ""
if "aprs_igate_height" in pm:
try:
height_m = float(pm["aprs_igate_height"])
height_ft = round(height_m * FEET_PER_METER)
height = "HEIGHT=" + str(height_ft)
except:
logger.error(
"Cannot parse 'aprs_igate_height', expected float: " + str(pm["aprs_igate_height"])
)
pbeacon = 'PBEACON sendto=IG delay=0:30 every=60:00 symbol={symbol} lat={lat} long={lon} {height} {gain} {adir} comment="{comment}"'.format(
symbol=pm["aprs_igate_symbol"],
lat=lat,
lon=lon,
height=height,
gain="GAIN=" + str(pm["aprs_igate_gain"]) if "aprs_igate_gain" in pm else "",
adir="DIR=" + str(pm["aprs_igate_dir"]) if "aprs_igate_dir" in pm else "",
comment=pm["aprs_igate_comment"],
)
logger.info("APRS PBEACON String: " + pbeacon)
config += """
IGSERVER {server}
IGLOGIN {callsign} {password}
{pbeacon}
""".format(
server=pm["aprs_igate_server"],
callsign=pm["aprs_callsign"],
password=pm["aprs_igate_password"],
pbeacon=pbeacon,
)
return config
class DirewolfModule(ExecModule, DirewolfConfigSubscriber):
def __init__(self, service: bool = False):
self.tcpSource = None
self.writer = None
self.service = service
self.direwolfConfigPath = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
tmp_dir=CoreConfig().get_temporary_directory(), myid=id(self)
)
self.direwolfConfig = DirewolfConfig()
self.direwolfConfig.wire(self)
self.__writeConfig()
super().__init__(Format.SHORT, Format.CHAR, ["direwolf", "-c", self.direwolfConfigPath, "-r", "48000", "-t", "0", "-q", "d", "-q", "h"])
# direwolf supplies the data via a socket which we tap into in start()
# the output on its STDOUT is informative, but we still want to log it
buffer = Buffer(Format.CHAR)
self.logReader = LogReader(__name__, buffer)
super().setWriter(buffer)
self.start()
def __writeConfig(self):
file = open(self.direwolfConfigPath, "w")
file.write(self.direwolfConfig.getConfig(self.service))
file.close()
def setWriter(self, writer: Writer) -> None:
self.writer = writer
if self.tcpSource is not None:
self.tcpSource.setWriter(writer)
def start(self):
delay = 0.5
retries = 0
while True:
try:
self.tcpSource = TcpSource(self.direwolfConfig.getPort(), Format.CHAR)
if self.writer:
self.tcpSource.setWriter(self.writer)
break
except ConnectionError:
if retries > 20:
logger.error("maximum number of connection attempts reached. did direwolf start up correctly?")
raise
retries += 1
time.sleep(delay)
def restart(self):
self.__writeConfig()
super().restart()
self.start()
def onConfigChanged(self):
self.restart()
def stop(self) -> None:
super().stop()
self.logReader.stop()
self.logReader = None
os.unlink(self.direwolfConfigPath)
self.direwolfConfig.unwire(self)
self.direwolfConfig = None