From 885e361bab496040980d4de2f9329fa286ddfb8a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 15 Jan 2021 16:19:45 +0100 Subject: [PATCH] implement reporting of FST4W spots (in theory) --- htdocs/lib/MessagePanel.js | 4 +-- owrx/wsjt.py | 54 +++++++++++++++++++++++++------------- owrx/wsprnet.py | 15 ++++++++--- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js index a4c8b40..597475b 100644 --- a/htdocs/lib/MessagePanel.js +++ b/htdocs/lib/MessagePanel.js @@ -78,14 +78,14 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) { return $('
').text(input).html() }; - if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'FST4W'].indexOf(msg['mode']) >= 0) { + if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4'].indexOf(msg['mode']) >= 0) { matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); if (matches && matches[2] !== 'RR73') { linkedmsg = html_escape(matches[1]) + '' + matches[2] + ''; } else { linkedmsg = html_escape(linkedmsg); } - } else if (msg['mode'] === 'WSPR') { + } else if (['WSPR', 'FST4W'].indexOf(msg['mode']) >= 0) { matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/); if (matches) { linkedmsg = html_escape(matches[1]) + '' + matches[2] + '' + html_escape(matches[3]); diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 6d2c800..c432831 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -156,12 +156,17 @@ class WsjtParser(Parser): return mode = profile.getMode() - if mode == "WSPR": - decoder = WsprDecoder(profile) + if mode in ["WSPR", "FST4W"]: + messageParser = BeaconMessageParser() else: - decoder = Jt9Decoder(profile) + messageParser = QsoMessageParser() + if mode == "WSPR": + decoder = WsprDecoder(profile, messageParser) + else: + decoder = Jt9Decoder(profile, messageParser) out = decoder.parse(msg, freq) out["mode"] = mode + out["interval"] = profile.getInterval() self.pushDecode(mode) if "callsign" in out and "locator" in out: @@ -195,10 +200,9 @@ class WsjtParser(Parser): class Decoder(ABC): - locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$") - - def __init__(self, profile): + def __init__(self, profile, messageParser): self.profile = profile + self.messageParser = messageParser def parse_timestamp(self, instring): dateformat = self.profile.getTimestampFormat() @@ -215,8 +219,19 @@ class Decoder(ABC): def parse(self, msg, dial_freq): pass - def parseMessage(self, msg): - m = Decoder.locator_pattern.match(msg) + +class MessageParser(ABC): + @abstractmethod + def parse(self, msg): + pass + + +# Used in QSO-style modes (FT8, FT4, FST4) +class QsoMessageParser(MessageParser): + locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$") + + def parse(self, msg): + m = QsoMessageParser.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 @@ -226,6 +241,17 @@ class Decoder(ABC): return {"callsign": m.group(1), "locator": m.group(3)} +# Used in propagation reporting / beacon modes (WSPR / FST4W) +class BeaconMessageParser(MessageParser): + wspr_splitter_pattern = re.compile("([A-Z0-9/]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") + + def parse(self, msg): + m = BeaconMessageParser.wspr_splitter_pattern.match(msg) + if m is None: + return {} + return {"callsign": m.group(1), "locator": m.group(2), "dbm": m.group(3)} + + class Jt9Decoder(Decoder): def parse(self, msg, dial_freq): # ft8 sample @@ -245,13 +271,11 @@ class Jt9Decoder(Decoder): "freq": dial_freq + int(msg[9:13]), "msg": wsjt_msg, } - result.update(self.parseMessage(wsjt_msg)) + result.update(self.messageParser.parse(wsjt_msg)) return result class WsprDecoder(Decoder): - wspr_splitter_pattern = re.compile("([A-Z0-9/]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") - def parse(self, msg, dial_freq): # wspr sample # '2600 -24 0.4 0.001492 -1 G8AXA JO01 33' @@ -266,11 +290,5 @@ class WsprDecoder(Decoder): "drift": int(msg[20:23]), "msg": wsjt_msg, } - result.update(self.parseMessage(wsjt_msg)) + result.update(self.messageParser.parse(wsjt_msg)) return result - - def parseMessage(self, msg): - m = WsprDecoder.wspr_splitter_pattern.match(msg) - if m is None: - return {} - return {"callsign": m.group(1), "locator": m.group(2), "dbm": m.group(3)} diff --git a/owrx/wsprnet.py b/owrx/wsprnet.py index b1b8cd5..35bbe44 100644 --- a/owrx/wsprnet.py +++ b/owrx/wsprnet.py @@ -32,6 +32,13 @@ class Worker(threading.Thread): except Exception: logger.exception("Exception while uploading WSPRNet spot") + def _getMode(self, spot): + interval = round(spot["interval"] / 60) + # FST4W modes are mapped not to conflict with WSPR modes 2 and 15: + if spot["mode"] != "WSPR" and interval in [2, 15]: + return interval + 1 + return interval + def uploadSpot(self, spot): # function=wspr&date=210114&time=1732&sig=-15&dt=0.5&drift=0&tqrg=7.040019&tcall=DF2UU&tgrid=JN48&dbm=37&version=2.3.0-rc3&rcall=DD5JFK&rgrid=JN58SC&rqrg=7.040047&mode=2 # {'timestamp': 1610655960000, 'db': -23.0, 'dt': 0.3, 'freq': 7040048, 'drift': -1, 'msg': 'LA3JJ JO59 37', 'callsign': 'LA3JJ', 'locator': 'JO59', 'mode': 'WSPR'} @@ -42,7 +49,8 @@ class Worker(threading.Thread): "time": date.strftime("%H%M"), "sig": spot["db"], "dt": spot["dt"], - "drift": spot["drift"], + # FST4W does not have drift + "drift": spot["drift"] if "drift" in spot else 0, "tqrg": spot["freq"] / 1E6, "tcall": spot["callsign"], "tgrid": spot["locator"], @@ -51,8 +59,7 @@ class Worker(threading.Thread): "rcall": self.callsign, "rgrid": self.locator, # mode 2 = WSPR 2 minutes - # TODO implement FST4W mode codes - "mode": 2 + "mode": self._getMode(spot) }).encode() request.urlopen("http://wsprnet.org/post/", data) @@ -80,4 +87,4 @@ class WsprnetReporter(Reporter): logger.warning("WSPRNet Queue overflow, one spot lost") def getSupportedModes(self): - return ["WSPR"] + return ["WSPR", "FST4W"]