openwebrx-clone/owrx/wsjt.py

273 lines
8.4 KiB
Python
Raw Normal View History

2020-04-22 21:53:19 +00:00
from datetime import datetime, timezone
2019-07-06 20:21:47 +00:00
from owrx.map import Map, LocatorLocation
import re
2020-04-22 21:53:19 +00:00
from owrx.metrics import Metrics, CounterMetric
from owrx.pskreporter import PskReporter
from owrx.parser import Parser
2020-04-22 21:53:19 +00:00
from owrx.audio import AudioChopperProfile
from abc import ABC, ABCMeta, abstractmethod
from owrx.config import Config
import logging
logger = logging.getLogger(__name__)
class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta):
def decoding_depth(self, mode):
pm = Config.get()
# mode-specific setting?
if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]:
return pm["wsjt_decoding_depths"][mode]
# return global default
if "wsjt_decoding_depth" in pm:
return pm["wsjt_decoding_depth"]
# default when no setting is provided
return 3
class Ft8Profile(WsjtProfile):
2020-04-05 17:08:58 +00:00
def getInterval(self):
return 15
def getFileTimestampFormat(self):
return "%y%m%d_%H%M%S"
2019-07-13 21:16:25 +00:00
def decoder_commandline(self, file):
2019-09-15 14:37:12 +00:00
return ["jt9", "--ft8", "-d", str(self.decoding_depth("ft8")), file]
2019-07-13 21:16:25 +00:00
class WsprProfile(WsjtProfile):
2020-04-05 17:08:58 +00:00
def getInterval(self):
return 120
def getFileTimestampFormat(self):
return "%y%m%d_%H%M"
2019-07-13 21:16:25 +00:00
def decoder_commandline(self, file):
2019-09-15 14:37:12 +00:00
cmd = ["wsprd"]
if self.decoding_depth("wspr") > 1:
cmd += ["-d"]
cmd += [file]
return cmd
2019-07-13 21:16:25 +00:00
class Jt65Profile(WsjtProfile):
2020-04-05 17:08:58 +00:00
def getInterval(self):
return 60
def getFileTimestampFormat(self):
return "%y%m%d_%H%M"
def decoder_commandline(self, file):
2019-09-15 14:37:12 +00:00
return ["jt9", "--jt65", "-d", str(self.decoding_depth("jt65")), file]
class Jt9Profile(WsjtProfile):
2020-04-05 17:08:58 +00:00
def getInterval(self):
return 60
def getFileTimestampFormat(self):
return "%y%m%d_%H%M"
def decoder_commandline(self, file):
2019-09-15 14:37:12 +00:00
return ["jt9", "--jt9", "-d", str(self.decoding_depth("jt9")), file]
class Ft4Profile(WsjtProfile):
2020-04-05 17:08:58 +00:00
def getInterval(self):
return 7.5
def getFileTimestampFormat(self):
return "%y%m%d_%H%M%S"
2019-07-20 11:38:25 +00:00
def decoder_commandline(self, file):
2019-09-15 14:37:12 +00:00
return ["jt9", "--ft4", "-d", str(self.decoding_depth("ft4")), file]
2019-07-20 11:38:25 +00:00
2020-12-07 10:56:01 +00:00
class Fst4Profile(WsjtProfile):
availableIntervals = [15, 30, 60, 120, 300, 900, 1800]
def __init__(self, interval):
self.interval = interval
def getInterval(self):
return self.interval
def getFileTimestampFormat(self):
2020-12-07 23:00:21 +00:00
if self.interval < 60:
return "%y%m%d_%H%M%S"
return "%y%m%d_%H%M"
2020-12-07 10:56:01 +00:00
def decoder_commandline(self, file):
2020-12-07 19:29:22 +00:00
return ["jt9", "--fst4", "-p", str(self.interval), "-d", str(self.decoding_depth("fst4")), file]
2020-12-07 10:56:01 +00:00
@staticmethod
def getEnabledProfiles():
config = Config.get()
profiles = config["fst4_enabled_intervals"] if "fst4_enabled_intervals" in config else []
return [Fst4Profile(i) for i in profiles if i in Fst4Profile.availableIntervals]
class Fst4wProfile(WsjtProfile):
availableIntervals = [120, 300, 900, 1800]
def __init__(self, interval):
self.interval = interval
def getInterval(self):
return self.interval
def getFileTimestampFormat(self):
2020-12-07 23:00:21 +00:00
if self.interval < 60:
return "%y%m%d_%H%M%S"
return "%y%m%d_%H%M"
2020-12-07 10:56:01 +00:00
def decoder_commandline(self, file):
2020-12-07 19:29:22 +00:00
return ["jt9", "--fst4w", "-p", str(self.interval), "-d", str(self.decoding_depth("fst4w")), file]
2020-12-07 10:56:01 +00:00
@staticmethod
def getEnabledProfiles():
config = Config.get()
profiles = config["fst4w_enabled_intervals"] if "fst4w_enabled_intervals" in config else []
return [Fst4Profile(i) for i in profiles if i in Fst4Profile.availableIntervals]
class WsjtParser(Parser):
2020-12-07 10:56:01 +00:00
modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4", "`": "FST4"}
2019-07-11 21:40:09 +00:00
2020-04-22 22:21:59 +00:00
def parse(self, messages):
for data in messages:
try:
freq, raw_msg = data
self.setDialFrequency(freq)
msg = raw_msg.decode().rstrip()
# known debug messages we know to skip
if msg.startswith("<DecodeFinished>"):
return
if msg.startswith(" EOF on input file"):
return
modes = list(WsjtParser.modes.keys())
if msg[21] in modes or msg[19] in modes:
decoder = Jt9Decoder()
else:
decoder = WsprDecoder()
out = decoder.parse(msg, freq)
if "mode" in out:
self.pushDecode(out["mode"])
if "callsign" in out and "locator" in out:
Map.getSharedInstance().updateLocation(
out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band
)
PskReporter.getSharedInstance().spot(out)
self.handler.write_wsjt_message(out)
2020-12-07 10:56:01 +00:00
except (ValueError, IndexError):
2020-04-22 22:21:59 +00:00
logger.exception("error while parsing wsjt message")
2019-07-06 20:21:47 +00:00
def pushDecode(self, mode):
metrics = Metrics.getSharedInstance()
2019-09-12 21:23:50 +00:00
band = "unknown"
if self.band is not None:
band = self.band.getName()
if band is None:
band = "unknown"
if mode is None:
mode = "unknown"
name = "wsjt.decodes.{band}.{mode}".format(band=band, mode=mode)
metric = metrics.getMetric(name)
if metric is None:
metric = CounterMetric()
metrics.addMetric(name, metric)
metric.inc()
2020-04-05 19:48:05 +00:00
class Decoder(ABC):
2020-12-07 23:01:00 +00:00
locator_pattern = re.compile(".*\\s([A-Z0-9]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$")
2020-12-07 10:56:01 +00:00
def parse_timestamp(self, instring, dateformat):
ts = datetime.strptime(instring, dateformat)
2019-09-24 22:47:34 +00:00
return int(
datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000
)
2020-04-05 19:48:05 +00:00
@abstractmethod
def parse(self, msg, dial_freq):
pass
2020-12-07 10:56:01 +00:00
def parseMessage(self, msg):
m = Decoder.locator_pattern.match(msg)
if m is None:
return {}
# this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very
# likely this just means roger roger goodbye.
2020-12-07 23:01:00 +00:00
if m.group(3) == "RR73":
2020-12-07 10:56:01 +00:00
return {"callsign": m.group(1)}
2020-12-07 23:01:00 +00:00
return {"callsign": m.group(1), "locator": m.group(3)}
2020-12-07 10:56:01 +00:00
class Jt9Decoder(Decoder):
2019-09-23 20:45:55 +00:00
def parse(self, msg, dial_freq):
2019-07-13 21:16:25 +00:00
# ft8 sample
# '222100 -15 -0.0 508 ~ CQ EA7MJ IM66'
# jt65 sample
2019-07-20 10:47:10 +00:00
# '2352 -7 0.4 1801 # R0WAS R2ABM KO85'
# '0003 -4 0.4 1762 # CQ R2ABM KO85'
2020-12-07 10:56:01 +00:00
# fst4 sample
# '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV'
2019-07-20 10:47:10 +00:00
modes = list(WsjtParser.modes.keys())
if msg[19] in modes:
dateformat = "%H%M"
else:
2019-07-20 10:47:10 +00:00
dateformat = "%H%M%S"
2020-12-07 10:56:01 +00:00
try:
timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat)
except ValueError:
timestamp = None
msg = msg[len(dateformat) + 1:]
modeChar = msg[14:15]
2019-07-20 10:47:10 +00:00
mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown"
wsjt_msg = msg[17:53].strip()
result = {
2019-07-20 10:47:10 +00:00
"timestamp": timestamp,
"db": float(msg[0:3]),
"dt": float(msg[4:8]),
2019-09-23 20:45:55 +00:00
"freq": dial_freq + int(msg[9:13]),
2019-07-20 10:47:10 +00:00
"mode": mode,
"msg": wsjt_msg,
2019-07-20 10:47:10 +00:00
}
result.update(self.parseMessage(wsjt_msg))
return result
2019-07-13 21:16:25 +00:00
class WsprDecoder(Decoder):
wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")
2019-09-23 20:45:55 +00:00
def parse(self, msg, dial_freq):
2019-07-13 21:16:25 +00:00
# wspr sample
# '2600 -24 0.4 0.001492 -1 G8AXA JO01 33'
2019-07-20 10:47:10 +00:00
# '0052 -29 2.6 0.001486 0 G02CWT IO92 23'
2019-07-14 12:33:30 +00:00
wsjt_msg = msg[29:].strip()
result = {
2019-07-20 10:47:10 +00:00
"timestamp": self.parse_timestamp(msg[0:4], "%H%M"),
"db": float(msg[5:8]),
"dt": float(msg[9:13]),
2019-09-23 20:45:55 +00:00
"freq": dial_freq + int(float(msg[14:24]) * 1e6),
2019-07-20 10:47:10 +00:00
"drift": int(msg[25:28]),
"mode": "WSPR",
"msg": wsjt_msg,
2019-07-20 10:47:10 +00:00
}
result.update(self.parseMessage(wsjt_msg))
return result
2019-07-13 21:16:25 +00:00
def parseMessage(self, msg):
m = WsprDecoder.wspr_splitter_pattern.match(msg)
2019-07-13 21:16:25 +00:00
if m is None:
return {}
return {"callsign": m.group(1), "locator": m.group(2)}