from __future__ import annotations import logging import re import socket from datetime import datetime from threading import Event, Lock, Thread from typing import Any import pytz import telnetlib3 from core.config import SERVER_OWNER_CALLSIGN from core.utils import decode_telnet_bytes from data.spot import Spot from providers.spot.spot_provider import SpotProvider logger = logging.getLogger(__name__) class RBN(SpotProvider): """Spot 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.""" _LINE_PATTERN = re.compile( r"^DX de ([a-z0-9/]+)-.*:\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)", re.IGNORECASE, ) def __init__(self, provider_config: dict[str, Any]) -> None: """Constructor requires port number.""" name = provider_config.get("name", "RBN") super().__init__(name, provider_config) self._port: int = provider_config["port"] self._telnet: telnetlib3.Telnet | None = None self._telnet_lock: Lock = Lock() self._thread: Thread | None = None self._stop_event: Event = Event() def start(self) -> None: self._thread = Thread(target=self._handle, name=f"RBNSpotProvider-{self.name}", daemon=True) self._thread.start() def stop(self) -> None: self._stop_event.set() with self._telnet_lock: if self._telnet: try: self._telnet.sock.shutdown(socket.SHUT_RDWR) except (AttributeError, OSError): pass self._telnet.close() if self._thread: self._thread.join(timeout=5) if self._thread.is_alive(): logger.warning(f"RBN (port {self._port!s}) worker thread did not exit on time and will be killed.") def _handle(self) -> None: while not self._stop_event.is_set(): connected = False while not connected and not self._stop_event.is_set(): try: self.status = "Connecting" logger.info(f"RBN port {self._port!s} connecting...") new_telnet = telnetlib3.Telnet("telnet.reversebeacon.net", self._port) with self._telnet_lock: self._telnet = new_telnet if self._stop_event.is_set(): # stop() was called while we were connecting, close the connection rather than trying to # read when we know it won't work new_telnet.close() break self._telnet.read_until("Please enter your call: ".encode("latin-1")) self._telnet.write(f"{SERVER_OWNER_CALLSIGN}\n".encode("latin-1")) connected = True logger.info(f"RBN port {self._port!s} connected.") except Exception: self.status = "Error" logger.exception(f"Exception while connecting to RBN (port {self._port!s}).") self._stop_event.wait(timeout=5) self.status = "Waiting for Data" while connected and not self._stop_event.is_set(): try: # Check new telnet info against regular expression telnet_output = self._telnet.read_until("\n".encode("latin-1")) match = self._LINE_PATTERN.match(decode_telnet_bytes(telnet_output)) if match: spot_time = datetime.strptime(match.group(5), "%H%MZ").replace(tzinfo=pytz.UTC) spot_datetime = datetime.combine( datetime.now(pytz.UTC).date(), spot_time.time(), tzinfo=pytz.UTC, ) spot = Spot( source=self.name, dx_call=match.group(3), de_call=match.group(1), freq=float(match.group(2)) * 1000, comment=match.group(4).strip(), time=spot_datetime.timestamp(), ) # Add to our list self._submit(spot) self.status = "OK" self.last_update_time = datetime.now(pytz.UTC) logger.debug(f"Data received from RBN on port {self._port!s}.") except EOFError: connected = False if not self._stop_event.is_set(): self.status = "Restarting" logger.warning(f"Disconnected from RBN provider (port {self._port!s}). Reconnecting...") self._stop_event.wait(timeout=5) else: logger.info(f"RBN provider (port {self._port!s}) shutting down...") self.status = "Shutting down" except Exception: connected = False if not self._stop_event.is_set(): self.status = "Error" logger.exception(f"Exception in RBN provider (port {self._port!s})") self._stop_event.wait(timeout=5) else: logger.info(f"RBN provider (port {self._port!s}) shutting down...") self.status = "Shutting down" self.status = "Disconnected"