diff --git a/README.md b/README.md index 78636dc..2294cad 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ Currently supports: * HEMA * UKBOTA * Parks n Peaks +* RBN Future plans: -* RBN * APRS? * Packet? diff --git a/config-example.yml b/config-example.yml index fb32f24..5489a6f 100644 --- a/config-example.yml +++ b/config-example.yml @@ -2,9 +2,37 @@ # Rename this to "config.yml" before running the software. # Your callsign. Used to log into DX clusters and included in User-Agent when querying HTTP servers. Useful so if your -# server goes crazy and causes other people problems, they know who to contact :) +# server goes crazy and causes other people problems, they know who to contact :) If you don't have one, you can leave +# this as "N0CALL" and it shouldn't do any harm, as we're not sending anything to the various networks, only receiving. server-owner-callsign: "N0CALL" +# Data providers to use. This is an example set, tailor it to your liking by commenting and uncommenting. +# RBN is supported but has such a high data rate, you probably don't want it enabled. +# APRS-IS support is not yet implemented. +# Feel free to write your own provider classes! +providers: +# Some providers don't require any config: + - type: "POTA" + - type: "SOTA" + - type: "WWFF" + - type: "WWBOTA" + - type: "GMA" + - type: "HEMA" + - type: "ParksNPeaks" +# Some, like DX Clusters, require extra config. You can add multiple DX clusters if you want! + - + type: "DXCluster" + host: "hrd.wa9pie.net" + port: 8000 +# RBN uses two ports, 7000 for CW/RTTY and 7001 for FT8, so if you want both, you need to add two entries: +# - +# type: "RBN" +# port: 7000 +# - +# type: "RBN" +# port: 7001 +# - type: "APRS-IS" + # Port to open the local web server on web-server-port: 8080 diff --git a/main.py b/main.py index 72dd078..5e3c7a3 100644 --- a/main.py +++ b/main.py @@ -11,6 +11,7 @@ from providers.gma import GMA from providers.hema import HEMA from providers.parksnpeaks import ParksNPeaks from providers.pota import POTA +from providers.rbn import RBN from providers.sota import SOTA from providers.wwbota import WWBOTA from providers.wwff import WWFF @@ -27,6 +28,31 @@ def shutdown(sig, frame): for p in providers: p.stop() cleanup_timer.stop() +# Utility method to get a data provider based on its config entry. +# TODO we could probably find a way to do this more neatly by iterating through classes in "providers" and getting their +# names, if Python allows that sort of thing +def get_provider_from_config(config_providers_entry): + match config_providers_entry["type"]: + case "POTA": + return POTA() + case "SOTA": + return SOTA() + case "WWFF": + return WWFF() + case "GMA": + return GMA() + case "WWBOTA": + return WWBOTA() + case "HEMA": + return HEMA() + case "ParksNPeaks": + return ParksNPeaks() + case "DXCluster": + return DXCluster(config_providers_entry["host"], config_providers_entry["port"]) + case "RBN": + return RBN(config_providers_entry["port"]) + return None + # Main function if __name__ == '__main__': @@ -43,21 +69,14 @@ if __name__ == '__main__': # Shut down gracefully on SIGINT signal.signal(signal.SIGINT, shutdown) - # Create providers - providers = [ - POTA(), - SOTA(), - WWFF(), - WWBOTA(), - GMA(), - HEMA(), - ParksNPeaks(), - DXCluster("hrd.wa9pie.net", 8000), - # DXCluster("dxc.w3lpl.net", 22) - ] # Set up spot list & status data areas spot_list = [] status_data = {} + + # Create data providers + providers = [] + for entry in config["providers"]: + providers.append(get_provider_from_config(entry)) # Set up data providers for p in providers: p.setup(spot_list=spot_list) # Start data providers diff --git a/providers/rbn.py b/providers/rbn.py new file mode 100644 index 0000000..0e8ab96 --- /dev/null +++ b/providers/rbn.py @@ -0,0 +1,96 @@ +import logging +import re +from datetime import datetime, timezone +from threading import Thread +from time import sleep + +import pytz +import telnetlib3 + +from data.spot import Spot +from core.config import config +from providers.provider import Provider + + +# Provider for the Reverse Beacon Network. Connects to a single port, if you want both CW/RTTY (port 7000) and FT8 +# (port 7001) you need to instantiate two copies of this. The port is provided as an argument to the constructor. +class RBN(Provider): + CALLSIGN_PATTERN = "([a-z|0-9|/]+)" + FREQUENCY_PATTERM = "([0-9|.]+)" + LINE_PATTERN = re.compile( + "^DX de " + CALLSIGN_PATTERN + "-.*:\\s+" + FREQUENCY_PATTERM + "\\s+" + CALLSIGN_PATTERN + "\\s+(.*)\\s+(\\d{4}Z)", + re.IGNORECASE) + + # Constructor requires port number. + def __init__(self, port): + super().__init__() + self.port = port + self.telnet = None + self.thread = Thread(target=self.handle) + self.thread.daemon = True + self.run = True + + def name(self): + return "RBN port " + str(self.port) + + + def start(self): + self.thread.start() + + def stop(self): + self.run = False + self.telnet.close() + self.thread.join() + + def handle(self): + while self.run: + connected = False + while not connected and self.run: + try: + self.status = "Connecting" + logging.info("RBN port " + str(self.port) + " connecting...") + self.telnet = telnetlib3.Telnet("telnet.reversebeacon.net", self.port) + telnet_output = self.telnet.read_until("Please enter your call: ".encode("latin-1")) + self.telnet.write((config["server-owner-callsign"] + "\n").encode("latin-1")) + connected = True + logging.info("RBN port " + str(self.port) + " connected.") + except Exception as e: + self.status = "Error" + logging.exception("Exception while connecting to RBN (port " + str(self.port) + ").") + sleep(5) + + self.status = "Waiting for Data" + while connected and self.run: + try: + # Check new telnet info against regular expression + telnet_output = self.telnet.read_until("\n".encode("latin-1")) + match = self.LINE_PATTERN.match(telnet_output.decode("latin-1")) + if match: + spot_time = datetime.strptime(match.group(5), "%H%MZ") + spot_datetime = datetime.combine(datetime.today(), spot_time.time()).replace(tzinfo=pytz.UTC) + spot = Spot(source="RBN", + dx_call=match.group(3), + de_call=match.group(1), + freq=float(match.group(2)), + comment=match.group(4).strip(), + time=spot_datetime) + # Fill in any blanks + spot.infer_missing() + # Add to our list + self.submit(spot) + + self.status = "OK" + self.last_update_time = datetime.now(timezone.utc) + logging.debug("Data received from RBN on port " + str(self.port) + ".") + + except Exception as e: + connected = False + if self.run: + self.status = "Error" + logging.exception("Exception in RBN provider (port " + str(self.port) + ")") + sleep(5) + else: + logging.info("RBN provider (port " + str(self.port) + ") shutting down...") + self.status = "Shutting down" + + self.status = "Disconnected" \ No newline at end of file