mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-23 15:57:43 +00:00
Extract some common code into telnet_spot_provier as a base class for DX cluster and RBN
This commit is contained in:
+29
-103
@@ -1,23 +1,19 @@
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
from datetime import datetime
|
||||
from threading import Event, Lock, Thread
|
||||
|
||||
import pytz
|
||||
import telnetlib3
|
||||
|
||||
from core.config import SERVER_OWNER_CALLSIGN
|
||||
from data.spot import Spot
|
||||
from providers.spot.spot_provider import SpotProvider
|
||||
from core.utils import decode_telnet_bytes
|
||||
from providers.spot.telnet_spot_provider import TelnetSpotProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RBN(SpotProvider):
|
||||
class RBN(TelnetSpotProvider):
|
||||
"""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."""
|
||||
(port 7001) you need to instantiate two copies of this. The port is provided in config."""
|
||||
|
||||
_LINE_PATTERN = re.compile(
|
||||
r"^DX de ([a-z0-9/]+)-.*:\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)",
|
||||
@@ -28,101 +24,31 @@ class RBN(SpotProvider):
|
||||
"""Constructor requires port number."""
|
||||
|
||||
name = provider_config.get("name", "RBN")
|
||||
super().__init__(name, provider_config)
|
||||
self._port = provider_config["port"]
|
||||
self._telnet = None
|
||||
self._telnet_lock = Lock()
|
||||
self._thread = None
|
||||
self._stop_event = Event()
|
||||
super().__init__(
|
||||
name,
|
||||
provider_config,
|
||||
host="telnet.reversebeacon.net",
|
||||
port=provider_config["port"],
|
||||
login_prompt="Please enter your call: ",
|
||||
login_response=SERVER_OWNER_CALLSIGN,
|
||||
)
|
||||
|
||||
def start(self):
|
||||
self._thread = Thread(target=self._handle, name=f"RBNSpotProvider-{self.name}", daemon=True)
|
||||
self._thread.start()
|
||||
def _parse_line(self, line):
|
||||
match = self._LINE_PATTERN.match(line)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
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):
|
||||
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"
|
||||
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,
|
||||
)
|
||||
return 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(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user