From ac4401175f3f6e806c54cb1d13fda1aa4a9c263b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 7 Dec 2020 11:56:01 +0100 Subject: [PATCH] add FST4 and FST4W modes --- csdr/csdr.py | 24 ++++++++------- owrx/modes.py | 2 ++ owrx/wsjt.py | 81 +++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 81 insertions(+), 26 deletions(-) diff --git a/csdr/csdr.py b/csdr/csdr.py index 5ac7925..b6d4453 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -29,7 +29,7 @@ import math from functools import partial from owrx.kiss import KissClient, DirewolfConfig -from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile +from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile, Fst4Profile, Fst4wProfile from owrx.js8 import Js8Profiles from owrx.audio import AudioChopper @@ -412,19 +412,23 @@ class dsp(object): if self.isWsjtMode(): smd = self.get_secondary_demodulator() - chopper_profile = None + chopper_profiles = None if smd == "ft8": - chopper_profile = Ft8Profile() + chopper_profiles = [Ft8Profile()] elif smd == "wspr": - chopper_profile = WsprProfile() + chopper_profiles = [WsprProfile()] elif smd == "jt65": - chopper_profile = Jt65Profile() + chopper_profiles = [Jt65Profile()] elif smd == "jt9": - chopper_profile = Jt9Profile() + chopper_profiles = [Jt9Profile()] elif smd == "ft4": - chopper_profile = Ft4Profile() - if chopper_profile is not None: - chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile) + chopper_profiles = [Ft4Profile()] + elif smd == "fst4": + chopper_profiles = Fst4Profile.getEnabledProfiles() + elif smd == "fst4w": + chopper_profiles = Fst4wProfile.getEnabledProfiles() + if chopper_profiles is not None and len(chopper_profiles): + chopper = AudioChopper(self, self.secondary_process_demod.stdout, *chopper_profiles) chopper.start() self.output.send_output("wsjt_demod", chopper.read) elif self.isJs8(): @@ -566,7 +570,7 @@ class dsp(object): def isWsjtMode(self, demodulator=None): if demodulator is None: demodulator = self.get_secondary_demodulator() - return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] + return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w"] def isJs8(self, demodulator = None): if demodulator is None: diff --git a/owrx/modes.py b/owrx/modes.py index 91fd268..0d1561f 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -74,6 +74,8 @@ class Modes(object): DigitalMode( "wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True ), + DigitalMode("fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), + DigitalMode("fst4w", "FST4W", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True), DigitalMode("js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True), DigitalMode( "packet", diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 0ba8160..60df558 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -85,8 +85,52 @@ class Ft4Profile(WsjtProfile): return ["jt9", "--ft4", "-d", str(self.decoding_depth("ft4")), file] +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): + return "%y%m%d_%H%M%S" + + def decoder_commandline(self, file): + return ["jt9", "--fst4", "-b", "FST4-{0}".format(self.interval), "-d", str(self.decoding_depth("fst4")), file] + + @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): + return "%y%m%d_%H%M%S" + + def decoder_commandline(self, file): + return ["jt9", "--fst4w", "-b", "FST4W-{0}".format(self.interval), "-d", str(self.decoding_depth("fst4w")), file] + + @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): - modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"} + modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4", "`": "FST4"} def parse(self, messages): for data in messages: @@ -115,7 +159,7 @@ class WsjtParser(Parser): PskReporter.getSharedInstance().spot(out) self.handler.write_wsjt_message(out) - except ValueError: + except (ValueError, IndexError): logger.exception("error while parsing wsjt message") def pushDecode(self, mode): @@ -139,6 +183,8 @@ class WsjtParser(Parser): class Decoder(ABC): + locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") + def parse_timestamp(self, instring, dateformat): ts = datetime.strptime(instring, dateformat) return int( @@ -149,23 +195,36 @@ class Decoder(ABC): def parse(self, msg, dial_freq): pass + 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. + if m.group(2) == "RR73": + return {"callsign": m.group(1)} + return {"callsign": m.group(1), "locator": m.group(2)} + class Jt9Decoder(Decoder): - locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$") - def parse(self, msg, dial_freq): # ft8 sample # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' # jt65 sample # '2352 -7 0.4 1801 # R0WAS R2ABM KO85' # '0003 -4 0.4 1762 # CQ R2ABM KO85' + # fst4 sample + # '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV' modes = list(WsjtParser.modes.keys()) if msg[19] in modes: dateformat = "%H%M" else: dateformat = "%H%M%S" - timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat) - msg = msg[len(dateformat) + 1 :] + try: + timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat) + except ValueError: + timestamp = None + msg = msg[len(dateformat) + 1:] modeChar = msg[14:15] mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" wsjt_msg = msg[17:53].strip() @@ -181,16 +240,6 @@ class Jt9Decoder(Decoder): result.update(self.parseMessage(wsjt_msg)) return result - def parseMessage(self, msg): - m = Jt9Decoder.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. - if m.group(2) == "RR73": - return {"callsign": m.group(1)} - return {"callsign": m.group(1), "locator": m.group(2)} - class WsprDecoder(Decoder): wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")