Make list of providers configurable, and add RBN support.

This commit is contained in:
Ian Renton
2025-09-28 16:46:28 +01:00
parent 92fa0c52cd
commit 61125ca640
4 changed files with 157 additions and 14 deletions

View File

@@ -13,9 +13,9 @@ Currently supports:
* HEMA
* UKBOTA
* Parks n Peaks
* RBN
Future plans:
* RBN
* APRS?
* Packet?

View File

@@ -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

43
main.py
View File

@@ -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

96
providers/rbn.py Normal file
View File

@@ -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"