From fd2ffb47a0dea36c0168d7dd64f7c16b63f10981 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Fri, 26 Sep 2025 23:26:39 +0100 Subject: [PATCH] DX Cluster support --- core/constants.py | 1 + data/spot.py | 14 ++++++++- main.py | 9 +++--- providers/dxcluster.py | 65 ++++++++++++++++++++++++++++++++++++++++++ providers/pota.py | 2 +- requirements.txt | 1 + 6 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 providers/dxcluster.py diff --git a/core/constants.py b/core/constants.py index 5e1a850..b3ed0b5 100644 --- a/core/constants.py +++ b/core/constants.py @@ -3,6 +3,7 @@ from data.band import Band # General software SOFTWARE_NAME = "Metaspot by M0TRT" SOFTWARE_VERSION = "0.1" +SERVER_OWNER_CALLSIGN = "M0TRT" # Band definitions BANDS = [ diff --git a/data/spot.py b/data/spot.py index 47e6dec..d7d19af 100644 --- a/data/spot.py +++ b/data/spot.py @@ -1,6 +1,8 @@ from dataclasses import dataclass from datetime import datetime +from pyhamtools.locator import locator_to_latlong, latlong_to_locator + from core.constants import DXCC_FLAGS from core.utils import infer_mode_family_from_mode, infer_band_from_freq, infer_continent_from_callsign, \ infer_country_from_callsign, infer_cq_zone_from_callsign, infer_itu_zone_from_callsign, infer_dxcc_id_from_callsign @@ -97,4 +99,14 @@ class Spot: self.band_contrast_color = band.contrast_color if self.mode and not self.mode_family: - self.mode_family=infer_mode_family_from_mode(self.mode) \ No newline at end of file + self.mode_family=infer_mode_family_from_mode(self.mode) + + if self.grid and not self.latitude: + ll = locator_to_latlong(self.grid) + self.latitude = ll[0] + self.longitude = ll[1] + + if self.latitude and self.longitude and not self.grid: + self.grid = latlong_to_locator(self.latitude, self.longitude, 8) + + # TODO lat/lon from DXCC centre? \ No newline at end of file diff --git a/main.py b/main.py index e959eab..bebcd71 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import signal from time import sleep +from providers.dxcluster import DXCluster from providers.pota import POTA @@ -17,7 +18,10 @@ if __name__ == '__main__': signal.signal(signal.SIGINT, shutdown) # Create providers - providers = [POTA()] # todo all other providers + providers = [ + POTA(), + DXCluster("hrd.wa9pie.net", 8000) + ] # todo all other providers # Set up spot list spot_list = [] # Set up data providers @@ -32,9 +36,6 @@ if __name__ == '__main__': # Todo serve apidocs # Todo serve website - sleep(2) - print(len(spot_list)) - print(spot_list[0]) # NOTES FOR FIELD SPOTTER diff --git a/providers/dxcluster.py b/providers/dxcluster.py new file mode 100644 index 0000000..7741b98 --- /dev/null +++ b/providers/dxcluster.py @@ -0,0 +1,65 @@ +from datetime import datetime, timezone +from threading import Thread + +import pytz + +from core.constants import SERVER_OWNER_CALLSIGN +from data.spot import Spot +from providers.provider import Provider +import telnetlib3 +import re + +callsign_pattern = "([a-z|0-9|/]+)" +frequency_pattern = "([0-9|.]+)" +pattern = re.compile("^DX de "+callsign_pattern+":\\s+"+frequency_pattern+"\\s+"+callsign_pattern+"\\s+(.*)\\s+(\\d{4}Z)", re.IGNORECASE) + +class DXCluster(Provider): + + # Constructor requires hostname and port + def __init__(self, hostname, port): + super().__init__() + self.hostname = hostname + self.port = port + self.telnet = None + self.thread = None + self.run = True + + def name(self): + return "DX Cluster " + self.hostname + " " + str(self.port) + + def start(self): + self.thread = Thread(target=self.handle) + self.thread.start() + + def stop(self): + self.run = False + self.telnet.close() + self.thread.join() + + def handle(self): + self.status = "Connecting" + self.telnet = telnetlib3.Telnet(self.hostname, self.port) + self.telnet.read_until("login: ".encode("ascii")) + self.telnet.write((SERVER_OWNER_CALLSIGN + "\n").encode("ascii")) + self.status = "Waiting for Data" + + while self.run: + # Check new telnet info against regular expression + telnet_output = self.telnet.read_until("\n".encode("ascii")) + match = pattern.match(telnet_output.decode("ascii")) + 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=self.name(), + 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) diff --git a/providers/pota.py b/providers/pota.py index edcb527..7f63999 100644 --- a/providers/pota.py +++ b/providers/pota.py @@ -31,7 +31,7 @@ class POTA(Provider): # Iterate through source data for source_spot in source_data: # Convert to our spot format - spot = Spot(source="POTA", + spot = Spot(source=self.name(), source_id=source_spot["spotId"], dx_call=source_spot["activator"], de_call=source_spot["spotter"], diff --git a/requirements.txt b/requirements.txt index d4ebc34..e6c547d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ +pytz requests~=2.32.5 \ No newline at end of file