add FST4 and FST4W modes

This commit is contained in:
Jakob Ketterl 2020-12-07 11:56:01 +01:00
parent 71c649b016
commit ac4401175f
3 changed files with 81 additions and 26 deletions

View File

@ -29,7 +29,7 @@ import math
from functools import partial from functools import partial
from owrx.kiss import KissClient, DirewolfConfig 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.js8 import Js8Profiles
from owrx.audio import AudioChopper from owrx.audio import AudioChopper
@ -412,19 +412,23 @@ class dsp(object):
if self.isWsjtMode(): if self.isWsjtMode():
smd = self.get_secondary_demodulator() smd = self.get_secondary_demodulator()
chopper_profile = None chopper_profiles = None
if smd == "ft8": if smd == "ft8":
chopper_profile = Ft8Profile() chopper_profiles = [Ft8Profile()]
elif smd == "wspr": elif smd == "wspr":
chopper_profile = WsprProfile() chopper_profiles = [WsprProfile()]
elif smd == "jt65": elif smd == "jt65":
chopper_profile = Jt65Profile() chopper_profiles = [Jt65Profile()]
elif smd == "jt9": elif smd == "jt9":
chopper_profile = Jt9Profile() chopper_profiles = [Jt9Profile()]
elif smd == "ft4": elif smd == "ft4":
chopper_profile = Ft4Profile() chopper_profiles = [Ft4Profile()]
if chopper_profile is not None: elif smd == "fst4":
chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile) 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() chopper.start()
self.output.send_output("wsjt_demod", chopper.read) self.output.send_output("wsjt_demod", chopper.read)
elif self.isJs8(): elif self.isJs8():
@ -566,7 +570,7 @@ class dsp(object):
def isWsjtMode(self, demodulator=None): def isWsjtMode(self, demodulator=None):
if demodulator is None: if demodulator is None:
demodulator = self.get_secondary_demodulator() 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): def isJs8(self, demodulator = None):
if demodulator is None: if demodulator is None:

View File

@ -74,6 +74,8 @@ class Modes(object):
DigitalMode( DigitalMode(
"wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True "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("js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True),
DigitalMode( DigitalMode(
"packet", "packet",

View File

@ -85,8 +85,52 @@ class Ft4Profile(WsjtProfile):
return ["jt9", "--ft4", "-d", str(self.decoding_depth("ft4")), file] 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): class WsjtParser(Parser):
modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"} modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4", "`": "FST4"}
def parse(self, messages): def parse(self, messages):
for data in messages: for data in messages:
@ -115,7 +159,7 @@ class WsjtParser(Parser):
PskReporter.getSharedInstance().spot(out) PskReporter.getSharedInstance().spot(out)
self.handler.write_wsjt_message(out) self.handler.write_wsjt_message(out)
except ValueError: except (ValueError, IndexError):
logger.exception("error while parsing wsjt message") logger.exception("error while parsing wsjt message")
def pushDecode(self, mode): def pushDecode(self, mode):
@ -139,6 +183,8 @@ class WsjtParser(Parser):
class Decoder(ABC): class Decoder(ABC):
locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$")
def parse_timestamp(self, instring, dateformat): def parse_timestamp(self, instring, dateformat):
ts = datetime.strptime(instring, dateformat) ts = datetime.strptime(instring, dateformat)
return int( return int(
@ -149,23 +195,36 @@ class Decoder(ABC):
def parse(self, msg, dial_freq): def parse(self, msg, dial_freq):
pass 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): class Jt9Decoder(Decoder):
locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$")
def parse(self, msg, dial_freq): def parse(self, msg, dial_freq):
# ft8 sample # ft8 sample
# '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66'
# jt65 sample # jt65 sample
# '2352 -7 0.4 1801 # R0WAS R2ABM KO85' # '2352 -7 0.4 1801 # R0WAS R2ABM KO85'
# '0003 -4 0.4 1762 # CQ R2ABM KO85' # '0003 -4 0.4 1762 # CQ R2ABM KO85'
# fst4 sample
# '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV'
modes = list(WsjtParser.modes.keys()) modes = list(WsjtParser.modes.keys())
if msg[19] in modes: if msg[19] in modes:
dateformat = "%H%M" dateformat = "%H%M"
else: else:
dateformat = "%H%M%S" dateformat = "%H%M%S"
timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat) try:
msg = msg[len(dateformat) + 1 :] timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat)
except ValueError:
timestamp = None
msg = msg[len(dateformat) + 1:]
modeChar = msg[14:15] modeChar = msg[14:15]
mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown" mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown"
wsjt_msg = msg[17:53].strip() wsjt_msg = msg[17:53].strip()
@ -181,16 +240,6 @@ class Jt9Decoder(Decoder):
result.update(self.parseMessage(wsjt_msg)) result.update(self.parseMessage(wsjt_msg))
return result 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): class WsprDecoder(Decoder):
wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")