Merge branch 'develop' into m17

This commit is contained in:
Jakob Ketterl
2020-12-08 16:57:00 +01:00
29 changed files with 380 additions and 88 deletions

View File

@ -194,10 +194,11 @@ class AudioWriter(object):
try:
rc = decoder.wait(timeout=10)
if rc != 0:
logger.warning("decoder return code: %i", rc)
raise RuntimeError("decoder return code: {0}".format(rc))
except subprocess.TimeoutExpired:
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
decoder.kill()
raise
def start(self):
(self.wavefilename, self.wavefile) = self.getWaveFile()

View File

@ -76,4 +76,4 @@ class Option(CommandMapping):
class Argument(CommandMapping):
def map(self, value):
return value
return str(value)

View File

@ -68,7 +68,9 @@ class FeatureDetector(object):
"red_pitaya": ["soapy_connector", "soapy_red_pitaya"],
"radioberry": ["soapy_connector", "soapy_radioberry"],
"fcdpp": ["soapy_connector", "soapy_fcdpp"],
"sddc": ["sddc_connector"],
"hpsdr": ["hpsdr_connector"],
"eb200": ["eb200_connector"],
# optional features and their requirements
"digital_voice_digiham": ["digiham", "sox"],
"digital_voice_dsd": ["dsd", "sox", "digiham"],
@ -256,7 +258,7 @@ class FeatureDetector(object):
)
def _check_connector(self, command):
required_version = LooseVersion("0.3")
required_version = LooseVersion("0.4")
owrx_connector_version_regex = re.compile("^owrx-connector version (.*)$")
@ -499,9 +501,25 @@ class FeatureDetector(object):
"""
return self.command_is_runnable("dream --help", 0)
def has_sddc_connector(self):
"""
The sddc_connector allows connectivity with SDR devices powered by libsddc, e.g. RX666, RX888, HF103.
You can find more information [here](https://github.com/jketterl/sddc_connector).
"""
return self._check_connector("sddc_connector")
def has_hpsdr_connector(self):
"""
In order to use the HPSDR connector, you will need to install [hpsdrconnector]
(https://github.com/jancona/hpsdrconnector).
"""
return self.command_is_runnable("hpsdrconnector -h")
def has_eb200_connector(self):
"""
To use radios supporting the EB200 radios, you need to install the eb200_connector.
You can find more information [here](https://github.com/jketterl/eb200_connector).
"""
return self._check_connector("eb200_connector")

View File

@ -24,6 +24,12 @@ class Mode(object):
def is_service(self):
return self.service
def get_bandpass(self):
return self.bandpass
def get_modulation(self):
return self.modulation
class AnalogMode(Mode):
pass
@ -36,6 +42,14 @@ class DigitalMode(Mode):
super().__init__(modulation, name, bandpass, requirements, service, squelch)
self.underlying = underlying
def get_bandpass(self):
if self.bandpass is not None:
return self.bandpass
return Modes.findByModulation(self.underlying[0]).get_bandpass()
def get_modulation(self):
return Modes.findByModulation(self.underlying[0]).get_modulation()
class Modes(object):
mappings = [
@ -54,16 +68,24 @@ class Modes(object):
AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False),
DigitalMode("bpsk31", "BPSK31", underlying=["usb"]),
DigitalMode("bpsk63", "BPSK63", underlying=["usb"]),
DigitalMode("ft8", "FT8", underlying=["usb"], requirements=["wsjt-x"], service=True),
DigitalMode("ft4", "FT4", underlying=["usb"], requirements=["wsjt-x"], service=True),
DigitalMode("jt65", "JT65", underlying=["usb"], requirements=["wsjt-x"], service=True),
DigitalMode("jt9", "JT9", underlying=["usb"], requirements=["wsjt-x"], service=True),
DigitalMode("ft8", "FT8", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode("ft4", "FT4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode("jt65", "JT65", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode("jt9", "JT9", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True),
DigitalMode(
"wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True
),
DigitalMode("js8", "JS8Call", underlying=["usb"], requirements=["js8call"], 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", "Packet", underlying=["nfm", "usb", "lsb"], requirements=["packet"], service=True, squelch=False
"packet",
"Packet",
underlying=["nfm", "usb", "lsb"],
bandpass=Bandpass(-6250, 6250),
requirements=["packet"],
service=True,
squelch=False,
),
DigitalMode(
"pocsag",

View File

@ -30,7 +30,7 @@ class PskReporter(object):
sharedInstance = None
creationLock = threading.Lock()
interval = 300
supportedModes = ["FT8", "FT4", "JT9", "JT65", "JS8"]
supportedModes = ["FT8", "FT4", "JT9", "JT65", "FST4", "FST4W", "JS8"]
@staticmethod
def getSharedInstance():

View File

@ -155,18 +155,14 @@ class ServiceHandler(SdrSourceEventClient):
)
else:
for group in groups:
frequencies = sorted([f["frequency"] for f in group])
min = frequencies[0]
max = frequencies[-1]
cf = (min + max) / 2
bw = max - min
cf = self.get_center_frequency(group)
bw = self.get_bandwidth(group)
logger.debug(
"group center frequency: {0}, bandwidth: {1}".format(cf, bw)
)
resampler_props = PropertyLayer()
resampler_props["center_freq"] = cf
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
resampler_props["samp_rate"] = bw + 24000
resampler_props["samp_rate"] = bw
resampler = Resampler(resampler_props, self.source)
resampler.start()
@ -180,6 +176,23 @@ class ServiceHandler(SdrSourceEventClient):
# resampler goes in after the services since it must not be shutdown as long as the services are still running
self.services.append(resampler)
def get_min_max(self, group):
frequencies = sorted(group, key=lambda f: f["frequency"])
lowest = frequencies[0]
min = lowest["frequency"] + Modes.findByModulation(lowest["mode"]).get_bandpass().low_cut
highest = frequencies[-1]
max = highest["frequency"] + Modes.findByModulation(highest["mode"]).get_bandpass().high_cut
return min, max
def get_center_frequency(self, group):
min, max = self.get_min_max(group)
return (min + max) / 2
def get_bandwidth(self, group):
minFreq, maxFreq = self.get_min_max(group)
# minimum bandwidth for a resampler: 25kHz
return max(maxFreq - minFreq, 25000)
def optimizeResampling(self, freqs, bandwidth):
freqs = sorted(freqs, key=lambda f: f["frequency"])
distances = [
@ -203,12 +216,10 @@ class ServiceHandler(SdrSourceEventClient):
previous = split
groups.append([f for f in freqs if previous < f["frequency"]])
def get_bandwitdh(group):
freqs = sorted([f["frequency"] for f in group])
# the group will process the full BW once, plus the reduced BW once for each group member
return bandwidth + len(group) * (freqs[-1] - freqs[0] + 24000)
def get_total_bandwidth(group):
return bandwidth + len(group) * self.get_bandwidth(group)
total_bandwidth = sum([get_bandwitdh(group) for group in groups])
total_bandwidth = sum([get_total_bandwidth(group) for group in groups])
return {
"num_splits": num_splits,
"total_bandwidth": total_bandwidth,
@ -250,16 +261,9 @@ class ServiceHandler(SdrSourceEventClient):
center_freq = source.getProps()["center_freq"]
d.set_offset_freq(frequency - center_freq)
d.set_center_freq(center_freq)
if mode == "packet":
d.set_demodulator("nfm")
d.set_bpf(-4000, 4000)
elif mode == "wspr":
d.set_demodulator("usb")
# WSPR only samples between 1400 and 1600 Hz
d.set_bpf(1350, 1650)
else:
d.set_demodulator("usb")
d.set_bpf(0, 3000)
modeObject = Modes.findByModulation(mode)
d.set_demodulator(modeObject.get_modulation())
d.set_bandpass(modeObject.get_bandpass())
d.set_secondary_demodulator(mode)
d.set_audio_compression("none")
d.set_samp_rate(source.getProps()["samp_rate"])

View File

@ -59,10 +59,7 @@ class SdrSource(ABC):
self.activateProfile()
self.wireEvents()
if "port" in props and props["port"] is not None:
self.port = props["port"]
else:
self.port = getAvailablePort()
self.port = getAvailablePort()
self.monitor = None
self.clients = []
self.spectrumClients = []

View File

@ -23,7 +23,7 @@ class ConnectorSource(SdrSource):
"controlPort": Option("-c"),
"device": Option("-d"),
"iqswap": Flag("-i"),
"rtltcp_compat": Flag("-r"),
"rtltcp_compat": Option("-r"),
"ppm": Option("-P"),
"rf_gain": Option("-g"),
}

15
owrx/source/eb200.py Normal file
View File

@ -0,0 +1,15 @@
from owrx.source.connector import ConnectorSource
from owrx.command import Argument, Flag
class Eb200Source(ConnectorSource):
def getCommandMapper(self):
return (
super()
.getCommandMapper()
.setBase("eb200_connector")
.setMappings({
"long": Flag("-l"),
"remote": Argument(),
})
)

6
owrx/source/sddc.py Normal file
View File

@ -0,0 +1,6 @@
from owrx.source.connector import ConnectorSource
class SddcSource(ConnectorSource):
def getCommandMapper(self):
return super().getCommandMapper().setBase("sddc_connector")

View File

@ -85,8 +85,56 @@ 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):
if self.interval < 60:
return "%y%m%d_%H%M%S"
return "%y%m%d_%H%M"
def decoder_commandline(self, file):
return ["jt9", "--fst4", "-p", str(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):
if self.interval < 60:
return "%y%m%d_%H%M%S"
return "%y%m%d_%H%M"
def decoder_commandline(self, file):
return ["jt9", "--fst4w", "-p", str(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 [Fst4wProfile(i) for i in profiles if i in Fst4wProfile.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 +163,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 +187,8 @@ class WsjtParser(Parser):
class Decoder(ABC):
locator_pattern = re.compile(".*\\s([A-Z0-9]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$")
def parse_timestamp(self, instring, dateformat):
ts = datetime.strptime(instring, dateformat)
return int(
@ -149,23 +199,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(3) == "RR73":
return {"callsign": m.group(1)}
return {"callsign": m.group(1), "locator": m.group(3)}
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 +244,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]+)")