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) # path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols)
aprs_symbols_path = "/usr/share/aprs-symbols/png" 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 === # === PSK Reporter setting ===
# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info # 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 # 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; -khtml-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
overflow: hidden;
} }
.openwebrx-progressbar-bar .openwebrx-progressbar-bar {
{ background-color: #00aba6;
border-radius: 5px; border-radius: 5px;
height: 100%; height: 100%;
width: 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 .openwebrx-progressbar-text
@ -566,6 +578,7 @@ img.openwebrx-mirror-img
left:0px; left:0px;
top:4px; top:4px;
width: inherit; width: inherit;
z-index: 1;
} }
#openwebrx-panel-status #openwebrx-panel-status

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
from owrx.modes import Modes
import json import json
import logging import logging
@ -12,7 +13,15 @@ class Band(object):
self.upper_bound = dict["upper_bound"] self.upper_bound = dict["upper_bound"]
self.frequencies = [] self.frequencies = []
if "frequencies" in dict: if "frequencies" in dict:
availableModes = [mode.modulation for mode in Modes.getAvailableModes()]
for (mode, freqs) in dict["frequencies"].items(): 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): if not isinstance(freqs, list):
freqs = [freqs] freqs = [freqs]
for f in freqs: for f in freqs:
@ -22,7 +31,7 @@ class Band(object):
mode=mode, frequency=f, band=self.name mode=mode, frequency=f, band=self.name
) )
) )
else: continue
self.frequencies.append({"mode": mode, "frequency": f}) self.frequencies.append({"mode": mode, "frequency": f})
def inBand(self, freq): def inBand(self, freq):

View File

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

View File

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

View File

@ -28,7 +28,9 @@ class DspManager(csdr.output, SdrSourceEventClient):
self.props = PropertyStack() self.props = PropertyStack()
# local demodulator properties not forwarded to the sdr # local demodulator properties not forwarded to the sdr
self.props.addLayer(0, PropertyLayer().filter( self.props.addLayer(
0,
PropertyLayer().filter(
"output_rate", "output_rate",
"hd_output_rate", "hd_output_rate",
"squelch_level", "squelch_level",
@ -39,9 +41,12 @@ class DspManager(csdr.output, SdrSourceEventClient):
"mod", "mod",
"secondary_offset_freq", "secondary_offset_freq",
"dmr_filter", "dmr_filter",
)) ),
)
# properties that we inherit from the sdr # properties that we inherit from the sdr
self.props.addLayer(1, self.sdrSource.getProps().filter( self.props.addLayer(
1,
self.sdrSource.getProps().filter(
"audio_compression", "audio_compression",
"fft_compression", "fft_compression",
"digimodes_fft_size", "digimodes_fft_size",
@ -56,7 +61,8 @@ class DspManager(csdr.output, SdrSourceEventClient):
"start_mod", "start_mod",
"start_freq", "start_freq",
"wfm_deemphasis_tau", "wfm_deemphasis_tau",
)) ),
)
self.dsp = csdr.dsp(self) self.dsp = csdr.dsp(self)
self.dsp.nc_port = self.sdrSource.getPort() self.dsp.nc_port = self.sdrSource.getPort()
@ -71,8 +77,13 @@ class DspManager(csdr.output, SdrSourceEventClient):
bpf[1] = cut bpf[1] = cut
self.dsp.set_bpf(*bpf) self.dsp.set_bpf(*bpf)
def set_dial_freq(key, value): def set_dial_freq(changes):
if self.props["center_freq"] is None or self.props["offset_freq"] is None: 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 return
freq = self.props["center_freq"] + self.props["offset_freq"] freq = self.props["center_freq"] + self.props["offset_freq"]
for parser in self.parsers.values(): for parser in self.parsers.values():

View File

@ -11,6 +11,7 @@ FESC = 0xDB
TFEND = 0xDC TFEND = 0xDC
TFESC = 0xDD TFESC = 0xDD
FEET_PER_METER = 3.28084
class DirewolfConfig(object): class DirewolfConfig(object):
def getConfig(self, port, is_service): def getConfig(self, port, is_service):
@ -39,6 +40,7 @@ IGLOGIN {callsign} {password}
) )
if pm["aprs_igate_beacon"]: if pm["aprs_igate_beacon"]:
#Format beacon lat/lon
lat = pm["receiver_gps"]["lat"] lat = pm["receiver_gps"]["lat"]
lon = pm["receiver_gps"]["lon"] lon = pm["receiver_gps"]["lon"]
direction_ns = "N" if lat > 0 else "S" 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) 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) lon = "{0:03d}^{1:05.2f}{2}".format(int(lon), (lon - int(lon)) * 60, direction_we)
config += """ #Format beacon details
PBEACON sendto=IG delay=0:30 every=60:00 symbol="igate" overlay=R lat={lat} long={lon} comment="OpenWebRX APRS gateway" symbol = str(pm["aprs_igate_symbol"]) if "aprs_igate_symbol" in pm else "R&"
""".format( gain = "GAIN=" + str(pm["aprs_igate_gain"]) if "aprs_igate_gain" in pm else ""
lat=lat, lon=lon 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 return config

View File

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

View File

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

View File

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

View File

@ -29,22 +29,22 @@ class ConnectorSource(SdrSource):
} }
) )
def sendControlMessage(self, prop, value): 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)) 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()) 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: if self.monitor is None:
return return
if ( if (
(prop == "center_freq" or prop == "lfo_offset") ("center_freq" in changes or "lfo_offset" in changes)
and "lfo_offset" in self.sdrProps and "lfo_offset" in self.sdrProps
and self.sdrProps["lfo_offset"] is not None and self.sdrProps["lfo_offset"] is not None
): ):
freq = self.sdrProps["center_freq"] + self.sdrProps["lfo_offset"] changes["center_freq"] = self.sdrProps["center_freq"] + self.sdrProps["lfo_offset"]
self.sendControlMessage("center_freq", freq) changes.pop("lfo_offset", None)
else: self.sendControlMessage(changes)
self.sendControlMessage(prop, value)
def postStart(self): def postStart(self):
logger.debug("opening control socket...") logger.debug("opening control socket...")

View File

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

View File

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

View File

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

View File

@ -80,9 +80,12 @@ class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
values["soapy_settings"] = settings values["soapy_settings"] = settings
return values return values
def onPropertyChange(self, prop, value): def onPropertyChange(self, changes):
mappings = self.getSoapySettingsMappings() mappings = self.getSoapySettingsMappings()
settings = {}
for prop, value in changes.items():
if prop in mappings.keys(): if prop in mappings.keys():
value = "{0}={1}".format(mappings[prop], self.convertSoapySettingsValue(value)) settings[mappings[prop]] = self.convertSoapySettingsValue(value)
prop = "settings" if settings:
super().onPropertyChange(prop, value) 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") pf = PropertyFilter(pm, "testkey")
self.assertEqual(pf["testkey"], "testvalue") self.assertEqual(pf["testkey"], "testvalue")
def testMissesPropert(self): def testMissesProperty(self):
pm = PropertyLayer() pm = PropertyLayer()
pm["testkey"] = "testvalue" pm["testkey"] = "testvalue"
pf = PropertyFilter(pm, "other_key") pf = PropertyFilter(pm, "other_key")
@ -25,7 +25,7 @@ class PropertyFilterTest(TestCase):
mock = Mock() mock = Mock()
pf.wire(mock.method) pf.wire(mock.method)
pm["testkey"] = "testvalue" pm["testkey"] = "testvalue"
mock.method.assert_called_once_with("testkey", "testvalue") mock.method.assert_called_once_with({"testkey": "testvalue"})
def testForwardsPropertyEvent(self): def testForwardsPropertyEvent(self):
pm = PropertyLayer() pm = PropertyLayer()

View File

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

View File

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