Merge branch 'develop' into pycsdr

This commit is contained in:
Jakob Ketterl
2021-01-02 03:11:41 +01:00
22 changed files with 280 additions and 162 deletions

View File

@ -1,3 +1,4 @@
from owrx.modes import Modes
import json
import logging
@ -12,7 +13,15 @@ class Band(object):
self.upper_bound = dict["upper_bound"]
self.frequencies = []
if "frequencies" in dict:
availableModes = [mode.modulation for mode in Modes.getAvailableModes()]
for (mode, freqs) in dict["frequencies"].items():
if mode not in availableModes:
logger.info(
"Modulation \"{mode}\" is not available, bandplan bookmark will not be displayed".format(
mode=mode
)
)
continue
if not isinstance(freqs, list):
freqs = [freqs]
for f in freqs:
@ -22,8 +31,8 @@ class Band(object):
mode=mode, frequency=f, band=self.name
)
)
else:
self.frequencies.append({"mode": mode, "frequency": f})
continue
self.frequencies.append({"mode": mode, "frequency": f})
def inBand(self, freq):
return self.lower_bound <= freq <= self.upper_bound

View File

@ -12,6 +12,7 @@ from owrx.bookmarks import Bookmarks
from owrx.map import Map
from owrx.property import PropertyStack
from owrx.modes import Modes, DigitalMode
from owrx.config import Config
from queue import Queue, Full, Empty
from js8py import Js8Frame
from abc import ABC, ABCMeta, abstractmethod
@ -108,22 +109,26 @@ class OpenWebRxClient(Client, metaclass=ABCMeta):
class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
config_keys = [
"waterfall_colors",
sdr_config_keys = [
"waterfall_min_level",
"waterfall_min_level",
"waterfall_max_level",
"waterfall_auto_level_margin",
"samp_rate",
"fft_size",
"audio_compression",
"fft_compression",
"max_clients",
"start_mod",
"start_freq",
"center_freq",
"initial_squelch_level",
"profile_id",
"squelch_auto_margin",
]
global_config_keys = [
"waterfall_colors",
"waterfall_auto_level_margin",
"fft_size",
"audio_compression",
"fft_compression",
"max_clients",
"frequency_display_precision",
]
@ -132,7 +137,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.dsp = None
self.sdr = None
self.configSub = None
self.sdrConfigSubs = []
self.connectionProperties = {}
try:
@ -142,6 +147,10 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.close()
raise
globalConfig = Config.get().filter(*OpenWebRxReceiverClient.global_config_keys)
self.globalConfigSub = globalConfig.wire(self.write_config)
self.write_config(globalConfig.__dict__())
self.setSdr()
features = FeatureDetector().feature_availability()
@ -154,6 +163,10 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
CpuUsageThread.getSharedInstance().add_client(self)
def __del__(self):
if hasattr(self, "globalConfigSub"):
self.globalConfigSub.cancel()
def onStateChange(self, state):
if state == SdrSource.STATE_RUNNING:
self.handleSdrAvailable()
@ -231,9 +244,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.stopDsp()
if self.configSub is not None:
self.configSub.cancel()
self.configSub = None
while self.sdrConfigSubs:
self.sdrConfigSubs.pop().cancel()
if self.sdr is not None:
self.sdr.removeClient(self)
@ -248,31 +260,38 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.sdr.addClient(self)
def handleSdrAvailable(self):
# send initial config
self.getDsp().setProperties(self.connectionProperties)
stack = PropertyStack()
stack.addLayer(0, self.sdr.getProps())
stack.addLayer(1, Config.get())
configProps = stack.filter(*OpenWebRxReceiverClient.config_keys)
configProps = stack.filter(*OpenWebRxReceiverClient.sdr_config_keys)
def sendConfig(key, value):
config = configProps.__dict__()
# TODO mathematical properties? hmmmm
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
# TODO this is a hack to support multiple sdrs
config["sdr_id"] = self.sdr.getId()
def sendConfig(changes=None):
if changes is None:
config = configProps.__dict__()
else:
config = changes
if changes is None or "start_freq" in changes or "center_freq" in changes:
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
if changes is None or "profile_id" in changes:
config["sdr_id"] = self.sdr.getId()
self.write_config(config)
def sendBookmarks(changes=None):
cf = configProps["center_freq"]
srh = configProps["samp_rate"] / 2
frequencyRange = (cf - srh, cf + srh)
self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange))
self.write_dial_frequencies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange))
bookmarks = [b.__dict__() for b in Bookmarks.getSharedInstance().getBookmarks(frequencyRange)]
self.write_bookmarks(bookmarks)
self.configSub = configProps.wire(sendConfig)
sendConfig(None, None)
self.sdrConfigSubs.append(configProps.wire(sendConfig))
self.sdrConfigSubs.append(stack.filter("center_freq", "samp_rate").wire(sendBookmarks))
# send initial config
sendConfig()
sendBookmarks()
self.__sendProfiles()
self.sdr.addSpectrumClient(self)
@ -289,9 +308,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
self.stopDsp()
CpuUsageThread.getSharedInstance().remove_client(self)
ClientRegistry.getSharedInstance().removeClient(self)
if self.configSub is not None:
self.configSub.cancel()
self.configSub = None
while self.sdrConfigSubs:
self.sdrConfigSubs.pop().cancel()
super().close()
def stopDsp(self):
@ -368,7 +386,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def write_wsjt_message(self, message):
self.send({"type": "wsjt_message", "value": message})
def write_dial_frequendies(self, frequencies):
def write_dial_frequencies(self, frequencies):
self.send({"type": "dial_frequencies", "value": frequencies})
def write_bookmarks(self, bookmarks):

View File

@ -7,11 +7,13 @@ logger = logging.getLogger(__name__)
class CpuUsageThread(threading.Thread):
sharedInstance = None
creationLock = threading.Lock()
@staticmethod
def getSharedInstance():
if CpuUsageThread.sharedInstance is None:
CpuUsageThread.sharedInstance = CpuUsageThread()
with CpuUsageThread.creationLock:
if CpuUsageThread.sharedInstance is None:
CpuUsageThread.sharedInstance = CpuUsageThread()
return CpuUsageThread.sharedInstance
def __init__(self):
@ -23,6 +25,7 @@ class CpuUsageThread(threading.Thread):
super().__init__()
def run(self):
logger.debug("cpu usage thread starting up")
while self.doRun:
try:
cpu_usage = self.get_cpu_usage()

View File

@ -28,35 +28,41 @@ class DspManager(csdr.output, SdrSourceEventClient):
self.props = PropertyStack()
# local demodulator properties not forwarded to the sdr
self.props.addLayer(0, PropertyLayer().filter(
"output_rate",
"hd_output_rate",
"squelch_level",
"secondary_mod",
"low_cut",
"high_cut",
"offset_freq",
"mod",
"secondary_offset_freq",
"dmr_filter",
))
self.props.addLayer(
0,
PropertyLayer().filter(
"output_rate",
"hd_output_rate",
"squelch_level",
"secondary_mod",
"low_cut",
"high_cut",
"offset_freq",
"mod",
"secondary_offset_freq",
"dmr_filter",
),
)
# properties that we inherit from the sdr
self.props.addLayer(1, self.sdrSource.getProps().filter(
"audio_compression",
"fft_compression",
"digimodes_fft_size",
"csdr_dynamic_bufsize",
"csdr_print_bufsizes",
"csdr_through",
"digimodes_enable",
"samp_rate",
"digital_voice_unvoiced_quality",
"temporary_directory",
"center_freq",
"start_mod",
"start_freq",
"wfm_deemphasis_tau",
))
self.props.addLayer(
1,
self.sdrSource.getProps().filter(
"audio_compression",
"fft_compression",
"digimodes_fft_size",
"csdr_dynamic_bufsize",
"csdr_print_bufsizes",
"csdr_through",
"digimodes_enable",
"samp_rate",
"digital_voice_unvoiced_quality",
"temporary_directory",
"center_freq",
"start_mod",
"start_freq",
"wfm_deemphasis_tau",
),
)
self.dsp = csdr.dsp(self)
self.dsp.nc_port = self.sdrSource.getPort()
@ -71,8 +77,13 @@ class DspManager(csdr.output, SdrSourceEventClient):
bpf[1] = cut
self.dsp.set_bpf(*bpf)
def set_dial_freq(key, value):
if self.props["center_freq"] is None or self.props["offset_freq"] is None:
def set_dial_freq(changes):
if (
"center_freq" not in self.props
or self.props["center_freq"] is None
or "offset_freq" not in self.props
or self.props["offset_freq"] is None
):
return
freq = self.props["center_freq"] + self.props["offset_freq"]
for parser in self.parsers.values():

View File

@ -11,6 +11,7 @@ FESC = 0xDB
TFEND = 0xDC
TFESC = 0xDD
FEET_PER_METER = 3.28084
class DirewolfConfig(object):
def getConfig(self, port, is_service):
@ -39,6 +40,7 @@ IGLOGIN {callsign} {password}
)
if pm["aprs_igate_beacon"]:
#Format beacon lat/lon
lat = pm["receiver_gps"]["lat"]
lon = pm["receiver_gps"]["lon"]
direction_ns = "N" if lat > 0 else "S"
@ -48,11 +50,33 @@ IGLOGIN {callsign} {password}
lat = "{0:02d}^{1:05.2f}{2}".format(int(lat), (lat - int(lat)) * 60, direction_ns)
lon = "{0:03d}^{1:05.2f}{2}".format(int(lon), (lon - int(lon)) * 60, direction_we)
config += """
PBEACON sendto=IG delay=0:30 every=60:00 symbol="igate" overlay=R lat={lat} long={lon} comment="OpenWebRX APRS gateway"
""".format(
lat=lat, lon=lon
)
#Format beacon details
symbol = str(pm["aprs_igate_symbol"]) if "aprs_igate_symbol" in pm else "R&"
gain = "GAIN=" + str(pm["aprs_igate_gain"]) if "aprs_igate_gain" in pm else ""
adir = "DIR=" + str(pm["aprs_igate_dir"]) if "aprs_igate_dir" in pm else ""
comment = str(pm["aprs_igate_comment"]) if "aprs_igate_comment" in pm else "\"OpenWebRX APRS gateway\""
#Convert height from meters to feet if specified
height = ""
if "aprs_igate_height" in pm:
try:
height_m = float(pm["aprs_igate_height"])
height_ft = round(height_m * FEET_PER_METER)
height = "HEIGHT=" + str(height_ft)
except:
logger.error("Cannot parse 'aprs_igate_height', expected float: " + str(pm["aprs_igate_height"]))
if((len(comment) > 0) and ((comment[0] != '"') or (comment[len(comment)-1] != '"'))):
comment = "\"" + comment + "\""
elif(len(comment) == 0):
comment = "\"\""
pbeacon = "PBEACON sendto=IG delay=0:30 every=60:00 symbol={symbol} lat={lat} long={lon} {height} {gain} {adir} comment={comment}".format(
symbol=symbol, lat=lat, lon=lon, height=height, gain=gain, adir=adir, comment=comment )
logger.info("APRS PBEACON String: " + pbeacon)
config += "\n" + pbeacon + "\n"
return config

View File

@ -71,15 +71,22 @@ class PropertyManager(ABC):
pass
return self
def _fireCallbacks(self, name, value):
def _fireCallbacks(self, changes):
if not changes:
return
for c in self.subscribers:
try:
if c.getName() is None:
c.call(name, value)
elif c.getName() == name:
c.call(value)
except Exception as e:
logger.exception(e)
c.call(changes)
except Exception:
logger.exception("exception while firing changes")
for name in changes:
for c in self.subscribers:
try:
if c.getName() == name:
c.call(changes[name])
except Exception:
logger.exception("exception while firing changes")
class PropertyLayer(PropertyManager):
@ -97,7 +104,7 @@ class PropertyLayer(PropertyManager):
if name in self.properties and self.properties[name] == value:
return
self.properties[name] = value
self._fireCallbacks(name, value)
self._fireCallbacks({name: value})
def __dict__(self):
return {k: v for k, v in self.properties.items()}
@ -116,10 +123,9 @@ class PropertyFilter(PropertyManager):
self.props = props
self.pm.wire(self.receiveEvent)
def receiveEvent(self, name, value):
if name not in self.props:
return
self._fireCallbacks(name, value)
def receiveEvent(self, changes):
changesToForward = {name: value for name, value in changes.items() if name in self.props}
self._fireCallbacks(changesToForward)
def __getitem__(self, item):
if item not in self.props:
@ -157,7 +163,7 @@ class PropertyStack(PropertyManager):
"""
highest priority = 0
"""
self._fireChanges(self._addLayer(priority, pm))
self._fireCallbacks(self._addLayer(priority, pm))
def _addLayer(self, priority: int, pm: PropertyManager):
changes = {}
@ -165,8 +171,8 @@ class PropertyStack(PropertyManager):
if key not in self or self[key] != pm[key]:
changes[key] = pm[key]
def eventClosure(name, value):
self.receiveEvent(pm, name, value)
def eventClosure(changes):
self.receiveEvent(pm, changes)
sub = pm.wire(eventClosure)
@ -177,7 +183,7 @@ class PropertyStack(PropertyManager):
def removeLayer(self, pm: PropertyManager):
for layer in self.layers:
if layer["props"] == pm:
self._fireChanges(self._removeLayer(layer))
self._fireCallbacks(self._removeLayer(layer))
def _removeLayer(self, layer):
layer["sub"].cancel()
@ -201,16 +207,11 @@ class PropertyStack(PropertyManager):
changes = {**changes, **self._addLayer(priority, pm)}
changes = {k: v for k, v in changes.items() if k not in originalState or originalState[k] != v}
self._fireChanges(changes)
self._fireCallbacks(changes)
def _fireChanges(self, changes):
for k, v in changes.items():
self._fireCallbacks(k, v)
def receiveEvent(self, layer, name, value):
if layer != self._getTopLayer(name):
return
self._fireCallbacks(name, value)
def receiveEvent(self, layer, changes):
changesToForward = {name: value for name, value in changes.items() if layer == self._getTopLayer(name)}
self._fireCallbacks(changesToForward)
def _getTopLayer(self, item):
layers = [la["props"] for la in sorted(self.layers, key=lambda l: l["priority"])]

View File

@ -111,7 +111,7 @@ class ServiceHandler(SdrSourceEventClient):
for service in services:
service.stop()
def onFrequencyChange(self, key, value):
def onFrequencyChange(self, changes):
self.stopServices()
if not self.source.isAvailable():
return

View File

@ -246,7 +246,7 @@ class ServiceScheduler(SdrSourceEventClient):
if state == SdrSource.BUSYSTATE_IDLE:
self.scheduleSelection()
def onFrequencyChange(self, name, value):
def onFrequencyChange(self, changes):
self.scheduleSelection()
def selectProfile(self):

View File

@ -96,7 +96,7 @@ class SdrSource(ABC):
return self.commandMapper
@abstractmethod
def onPropertyChange(self, name, value):
def onPropertyChange(self, changes):
pass
def wireEvents(self):

View File

@ -29,22 +29,22 @@ class ConnectorSource(SdrSource):
}
)
def sendControlMessage(self, prop, value):
logger.debug("sending property change over control socket: {0} changed to {1}".format(prop, value))
self.controlSocket.sendall("{prop}:{value}\n".format(prop=prop, value=value).encode())
def sendControlMessage(self, changes):
for prop, value in changes.items():
logger.debug("sending property change over control socket: {0} changed to {1}".format(prop, value))
self.controlSocket.sendall("{prop}:{value}\n".format(prop=prop, value=value).encode())
def onPropertyChange(self, prop, value):
def onPropertyChange(self, changes):
if self.monitor is None:
return
if (
(prop == "center_freq" or prop == "lfo_offset")
("center_freq" in changes or "lfo_offset" in changes)
and "lfo_offset" in self.sdrProps
and self.sdrProps["lfo_offset"] is not None
):
freq = self.sdrProps["center_freq"] + self.sdrProps["lfo_offset"]
self.sendControlMessage("center_freq", freq)
else:
self.sendControlMessage(prop, value)
changes["center_freq"] = self.sdrProps["center_freq"] + self.sdrProps["lfo_offset"]
changes.pop("lfo_offset", None)
self.sendControlMessage(changes)
def postStart(self):
logger.debug("opening control socket...")

View File

@ -7,12 +7,8 @@ logger = logging.getLogger(__name__)
class DirectSource(SdrSource, metaclass=ABCMeta):
def onPropertyChange(self, name, value):
logger.debug(
"restarting sdr source due to property change: {0} changed to {1}".format(
name, value
)
)
def onPropertyChange(self, changes):
logger.debug("restarting sdr source due to property changes: {0}".format(changes))
self.stop()
self.sleepOnRestart()
self.start()

View File

@ -30,7 +30,6 @@ class FifiSdrSource(DirectSource):
values = self.getCommandValues()
self.sendRockProgFrequency(values["tuner_freq"])
def onPropertyChange(self, name, value):
if name != "center_freq":
return
self.sendRockProgFrequency(value)
def onPropertyChange(self, changes):
if "center_freq" in changes:
self.sendRockProgFrequency(changes["center_freq"])

View File

@ -6,8 +6,8 @@ logger = logging.getLogger(__name__)
class Resampler(DirectSource):
def onPropertyChange(self, name, value):
logger.warning("Resampler is unable to handle property change ({0} changed to {1})".format(name, value))
def onPropertyChange(self, changes):
logger.warning("Resampler is unable to handle property changes: {0}".format(changes))
def __init__(self, props, sdr):
sdrProps = sdr.getProps()

View File

@ -80,9 +80,12 @@ class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
values["soapy_settings"] = settings
return values
def onPropertyChange(self, prop, value):
def onPropertyChange(self, changes):
mappings = self.getSoapySettingsMappings()
if prop in mappings.keys():
value = "{0}={1}".format(mappings[prop], self.convertSoapySettingsValue(value))
prop = "settings"
super().onPropertyChange(prop, value)
settings = {}
for prop, value in changes.items():
if prop in mappings.keys():
settings[mappings[prop]] = self.convertSoapySettingsValue(value)
if settings:
changes["settings"] = ",".join("{0}={1}".format(k, v) for k, v in settings.items())
super().onPropertyChange(changes)