Files
spothole/providers/spot/rbn.py
T

130 lines
5.6 KiB
Python

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"