Merge branch 'develop' into pycsdr

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

View File

@ -340,6 +340,27 @@ aprs_igate_beacon = False
# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols)
aprs_symbols_path = "/usr/share/aprs-symbols/png"
# Uncomment the following to customize gateway beacon details reported to the aprs network
# Plese see Dire Wolf's documentation on PBEACON configuration for complete details:
# https://github.com/wb2osz/direwolf/raw/master/doc/User-Guide.pdf
# Symbol in its two-character form as specified by the APRS spec at http://www.aprs.org/symbols/symbols-new.txt
# Default: Receive only IGate (do not send msgs back to RF)
# aprs_igate_symbol = "R&"
# Custom comment about igate
# Default: OpenWebRX APRS gateway
# aprs_igate_comment = "OpenWebRX APRS gateway"
# Antenna Height and Gain details
# Unspecified by default
# Antenna height above average terrain (HAAT) in meters
# aprs_igate_height = "5"
# Antenna gain in dBi
# aprs_igate_gain = "0"
# Antenna direction (N, NE, E, SE, S, SW, W, NW). Omnidirectional by default
# aprs_igate_dir = "NE"
# === PSK Reporter setting ===
# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator

View File

@ -551,13 +551,25 @@ img.openwebrx-mirror-img
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
overflow: hidden;
}
.openwebrx-progressbar-bar
{
.openwebrx-progressbar-bar {
background-color: #00aba6;
border-radius: 5px;
height: 100%;
width: 100%;
position: absolute;
left: -100%;
transition-property: left,background-color;
transition-duration: 1s;
transition-timing-function: ease-in-out;
transform: translateZ(0);
will-change: left;
}
.openwebrx-progressbar--over .openwebrx-progressbar-bar {
background-color: #ff6262;
}
.openwebrx-progressbar-text
@ -566,6 +578,7 @@ img.openwebrx-mirror-img
left:0px;
top:4px;
width: inherit;
z-index: 1;
}
#openwebrx-panel-status

View File

@ -4,6 +4,7 @@ function DemodulatorPanel(el) {
self.demodulator = null;
self.mode = null;
self.squelchMargin = 10;
self.initialParams = {};
var displayEl = el.find('.webrx-actual-freq')
this.tuneableFrequencyDisplay = displayEl.tuneableFrequencyDisplay();
@ -180,7 +181,7 @@ DemodulatorPanel.prototype.collectParams = function() {
squelch_level: -150,
mod: 'nfm'
}
return $.extend(new Object(), defaults, this.initialParams || {}, this.transformHashParams(this.parseHash()));
return $.extend(new Object(), defaults, this.initialParams, this.transformHashParams(this.parseHash()));
};
DemodulatorPanel.prototype.startDemodulator = function() {
@ -206,7 +207,7 @@ DemodulatorPanel.prototype._apply = function(params) {
};
DemodulatorPanel.prototype.setInitialParams = function(params) {
this.initialParams = params;
$.extend(this.initialParams, params);
};
DemodulatorPanel.prototype.onHashChange = function() {

View File

@ -3,7 +3,6 @@ ProgressBar = function(el) {
this.$innerText = $('<span class="openwebrx-progressbar-text">' + this.getDefaultText() + '</span>');
this.$innerBar = $('<div class="openwebrx-progressbar-bar"></div>');
this.$el.empty().append(this.$innerText, this.$innerBar);
this.$innerBar.css('width', '0%');
};
ProgressBar.prototype.getDefaultText = function() {
@ -19,7 +18,7 @@ ProgressBar.prototype.set = function(val, text, over) {
ProgressBar.prototype.setValue = function(val) {
if (val < 0) val = 0;
if (val > 1) val = 1;
this.$innerBar.stop().animate({width: val * 100 + '%'}, 700);
this.$innerBar.css({left: (val - 1) * 100 + '%'});
};
ProgressBar.prototype.setText = function(text) {
@ -27,7 +26,7 @@ ProgressBar.prototype.setText = function(text) {
};
ProgressBar.prototype.setOver = function(over) {
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
this.$el[over ? 'addClass' : 'removeClass']('openwebrx-progressbar--over');
};
AudioBufferProgressBar = function(el) {

View File

@ -314,14 +314,16 @@ function scale_px_from_freq(f, range) {
}
function get_visible_freq_range() {
var out = {};
if (!bandwidth) return false;
var fcalc = function (x) {
var canvasWidth = waterfallWidth() * zoom_levels[zoom_level];
return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2);
};
out.start = fcalc(0);
out.center = fcalc(waterfallWidth() / 2);
out.end = fcalc(waterfallWidth());
var out = {
start: fcalc(0),
center: fcalc(waterfallWidth() / 2),
end: fcalc(waterfallWidth()),
}
out.bw = out.end - out.start;
out.hps = out.bw / waterfallWidth();
return out;
@ -426,6 +428,7 @@ var range;
function mkscale() {
//clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes):
range = get_visible_freq_range();
if (!range) return;
mkenvelopes(range); //when scale changes we will always have to redraw filter envelopes, too
scale_ctx.clearRect(0, 22, scale_ctx.canvas.width, scale_ctx.canvas.height - 22);
scale_ctx.strokeStyle = "#fff";
@ -442,9 +445,7 @@ function mkscale() {
};
var last_large;
var x;
for (; ;) {
x = scale_px_from_freq(marker_hz, range);
if (x > window.innerWidth) break;
while ((x = scale_px_from_freq(marker_hz, range)) <= window.innerWidth) {
scale_ctx.beginPath();
scale_ctx.moveTo(x, 22);
if (marker_hz % spacing.params.large_marker_per_hz === 0) { //large marker
@ -700,41 +701,60 @@ function on_ws_recv(evt) {
switch (json.type) {
case "config":
var config = json['value'];
waterfall_colors = buildWaterfallColors(config['waterfall_colors']);
waterfall_min_level_default = config['waterfall_min_level'];
waterfall_max_level_default = config['waterfall_max_level'];
waterfall_auto_level_margin = config['waterfall_auto_level_margin'];
if ('waterfall_colors' in config)
waterfall_colors = buildWaterfallColors(config['waterfall_colors']);
if ('waterfall_min_level' in config)
waterfall_min_level_default = config['waterfall_min_level'];
if ('waterfall_max_level' in config)
waterfall_max_level_default = config['waterfall_max_level'];
if ('waterfall_auto_level_margin' in config)
waterfall_auto_level_margin = config['waterfall_auto_level_margin'];
waterfallColorsDefault();
var initial_demodulator_params = {
mod: config['start_mod'],
offset_frequency: config['start_offset_freq'],
squelch_level: Number.isInteger(config['initial_squelch_level']) ? config['initial_squelch_level'] : -150
};
var initial_demodulator_params = {};
if ('start_mod' in config)
initial_demodulator_params['mod'] = config['start_mod'];
if ('start_offset_freq' in config)
initial_demodulator_params['offset_frequency'] = config['start_offset_freq'];
if ('initial_squelch_level' in config)
initial_demodulator_params['squelch_level'] = Number.isInteger(config['initial_squelch_level']) ? config['initial_squelch_level'] : -150;
bandwidth = config['samp_rate'];
center_freq = config['center_freq'];
fft_size = config['fft_size'];
var audio_compression = config['audio_compression'];
audioEngine.setCompression(audio_compression);
divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
fft_compression = config['fft_compression'];
divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
$('#openwebrx-bar-clients').progressbar().setMaxClients(config['max_clients']);
if ('samp_rate' in config)
bandwidth = config['samp_rate'];
if ('center_freq' in config)
center_freq = config['center_freq'];
if ('fft_size' in config)
fft_size = config['fft_size'];
if ('audio_compression' in config) {
var audio_compression = config['audio_compression'];
audioEngine.setCompression(audio_compression);
divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
}
if ('fft_compression' in config) {
fft_compression = config['fft_compression'];
divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + ".");
}
if ('max_clients' in config)
$('#openwebrx-bar-clients').progressbar().setMaxClients(config['max_clients']);
waterfall_init();
var demodulatorPanel = $('#openwebrx-panel-receiver').demodulatorPanel();
demodulatorPanel.setCenterFrequency(center_freq);
demodulatorPanel.setInitialParams(initial_demodulator_params);
demodulatorPanel.setSquelchMargin(config['squelch_auto_margin']);
if ('squelch_auto_margin' in config)
demodulatorPanel.setSquelchMargin(config['squelch_auto_margin']);
bookmarks.loadLocalBookmarks();
if ('sdr_id' in config && 'profile_id' in config) {
currentprofile = config['sdr_id'] + '|' + config['profile_id'];
$('#openwebrx-sdr-profiles-listbox').val(currentprofile);
}
waterfall_clear();
currentprofile = config['sdr_id'] + '|' + config['profile_id'];
$('#openwebrx-sdr-profiles-listbox').val(currentprofile);
$('#openwebrx-panel-receiver').demodulatorPanel().setFrequencyPrecision(config['frequency_display_precision']);
if ('frequency_display_precision' in config)
$('#openwebrx-panel-receiver').demodulatorPanel().setFrequencyPrecision(config['frequency_display_precision']);
break;
case "secondary_config":

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)

View File

@ -11,7 +11,7 @@ class PropertyFilterTest(TestCase):
pf = PropertyFilter(pm, "testkey")
self.assertEqual(pf["testkey"], "testvalue")
def testMissesPropert(self):
def testMissesProperty(self):
pm = PropertyLayer()
pm["testkey"] = "testvalue"
pf = PropertyFilter(pm, "other_key")
@ -25,7 +25,7 @@ class PropertyFilterTest(TestCase):
mock = Mock()
pf.wire(mock.method)
pm["testkey"] = "testvalue"
mock.method.assert_called_once_with("testkey", "testvalue")
mock.method.assert_called_once_with({"testkey": "testvalue"})
def testForwardsPropertyEvent(self):
pm = PropertyLayer()

View File

@ -19,7 +19,7 @@ class PropertyLayerTest(TestCase):
mock = Mock()
pm.wire(mock.method)
pm["testkey"] = "after"
mock.method.assert_called_once_with("testkey", "after")
mock.method.assert_called_once_with({"testkey": "after"})
def testUnsubscribe(self):
pm = PropertyLayer()
@ -27,7 +27,7 @@ class PropertyLayerTest(TestCase):
mock = Mock()
sub = pm.wire(mock.method)
pm["testkey"] = "between"
mock.method.assert_called_once_with("testkey", "between")
mock.method.assert_called_once_with({"testkey": "between"})
mock.reset_mock()
pm.unwire(sub)

View File

@ -49,7 +49,7 @@ class PropertyStackTest(TestCase):
mock = Mock()
stack.wire(mock.method)
layer["testkey"] = "testvalue"
mock.method.assert_called_once_with("testkey", "testvalue")
mock.method.assert_called_once_with({"testkey": "testvalue"})
def testPropertyChangeEventPriority(self):
low_layer = PropertyLayer()
@ -64,7 +64,7 @@ class PropertyStackTest(TestCase):
low_layer["testkey"] = "modified low value"
mock.method.assert_not_called()
high_layer["testkey"] = "modified high value"
mock.method.assert_called_once_with("testkey", "modified high value")
mock.method.assert_called_once_with({"testkey": "modified high value"})
def testPropertyEventOnLayerAdd(self):
low_layer = PropertyLayer()
@ -162,7 +162,7 @@ class PropertyStackTest(TestCase):
mock = Mock()
stack.wire(mock.method)
stack.removeLayer(layer)
mock.method.assert_called_once_with("testkey", None)
mock.method.assert_called_once_with({"testkey": None})
mock.reset_mock()
layer["testkey"] = "after"