Merge branch 'develop' into m17
This commit is contained in:
@ -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()
|
||||
|
@ -76,4 +76,4 @@ class Option(CommandMapping):
|
||||
|
||||
class Argument(CommandMapping):
|
||||
def map(self, value):
|
||||
return value
|
||||
return str(value)
|
||||
|
@ -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")
|
@ -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",
|
||||
|
@ -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():
|
||||
|
@ -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"])
|
||||
|
@ -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 = []
|
||||
|
@ -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
15
owrx/source/eb200.py
Normal 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
6
owrx/source/sddc.py
Normal file
@ -0,0 +1,6 @@
|
||||
from owrx.source.connector import ConnectorSource
|
||||
|
||||
|
||||
class SddcSource(ConnectorSource):
|
||||
def getCommandMapper(self):
|
||||
return super().getCommandMapper().setBase("sddc_connector")
|
85
owrx/wsjt.py
85
owrx/wsjt.py
@ -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]+)")
|
||||
|
Reference in New Issue
Block a user