diff --git a/htdocs/lib/BookmarkBar.js b/htdocs/lib/BookmarkBar.js index e0993a5e..7c40892f 100644 --- a/htdocs/lib/BookmarkBar.js +++ b/htdocs/lib/BookmarkBar.js @@ -11,7 +11,7 @@ function BookmarkBar() { if (!b || !b.frequency || !b.modulation) return; me.getDemodulator().set_offset_frequency(b.frequency - center_freq); if (b.modulation) { - me.getDemodulatorPanel().setMode(b.modulation); + me.getDemodulatorPanel().setMode(b.modulation, b.underlying); } $bookmark.addClass('selected'); }); diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index e828fbea..d235626d 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -18,7 +18,12 @@ function DemodulatorPanel(el) { el.on('click', '.openwebrx-demodulator-button', function() { var modulation = $(this).data('modulation'); if (modulation) { - self.setMode(modulation); + if (self.mode && self.mode.type === 'digimode' && self.mode.underlying.indexOf(modulation) >= 0) { + // keep the mode, just switch underlying modulation + self.setMode(self.mode.modulation, modulation) + } else { + self.setMode(modulation); + } } else { self.disableDigiMode(); } @@ -80,12 +85,13 @@ DemodulatorPanel.prototype.render = function() { this.el.find(".openwebrx-modes").html(html); }; -DemodulatorPanel.prototype.setMode = function(requestedModulation) { +DemodulatorPanel.prototype.setMode = function(requestedModulation, underlyingModulation) { var mode = Modes.findByModulation(requestedModulation); if (!mode) { return; } - if (this.mode === mode) { + + if (this.mode === mode && this.underlyingModulation === underlyingModulation) { return; } if (!mode.isAvailable()) { @@ -93,16 +99,15 @@ DemodulatorPanel.prototype.setMode = function(requestedModulation) { return; } + var modulation; if (mode.type === 'digimode') { - modulation = mode.underlying[0]; - } else { - if (this.mode && this.mode.type === 'digimode' && this.mode.underlying.indexOf(requestedModulation) >= 0) { - // keep the mode, just switch underlying modulation - mode = this.mode; - modulation = requestedModulation; + if (underlyingModulation) { + modulation = underlyingModulation } else { - modulation = mode.modulation; + modulation = mode.underlying[0]; } + } else { + modulation = mode.modulation; } var current = this.collectParams(); @@ -142,6 +147,7 @@ DemodulatorPanel.prototype.setMode = function(requestedModulation) { this.demodulator.start(); this.mode = mode; + this.underlyingModulation = modulation; this.updateButtons(); this.updatePanels(); @@ -149,8 +155,6 @@ DemodulatorPanel.prototype.setMode = function(requestedModulation) { }; DemodulatorPanel.prototype.disableDigiMode = function() { - // just a little trick to get out of the digimode - delete this.mode; this.setMode(this.getDemodulator().get_modulation()); }; @@ -204,7 +208,11 @@ DemodulatorPanel.prototype.stopDemodulator = function() { } DemodulatorPanel.prototype._apply = function(params) { - this.setMode(params.mod); + if (params.secondary_mod) { + this.setMode(params.secondary_mod, params.mod) + } else { + this.setMode(params.mod); + } this.getDemodulator().set_offset_frequency(params.offset_frequency); this.getDemodulator().setSquelch(params.squelch_level); this.updateButtons(); @@ -224,8 +232,9 @@ DemodulatorPanel.prototype.onHashChange = function() { DemodulatorPanel.prototype.transformHashParams = function(params) { var ret = { - mod: params.secondary_mod || params.mod + mod: params.mod }; + if (typeof(params.secondary_mod) !== 'undefined') ret.secondary_mod = params.secondary_mod; if (typeof(params.offset_frequency) !== 'undefined') ret.offset_frequency = params.offset_frequency; if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql); return ret; @@ -330,7 +339,7 @@ DemodulatorPanel.prototype.updateHash = function() { freq: demod.get_offset_frequency() + self.center_freq, mod: demod.get_modulation(), secondary_mod: demod.get_secondary_demod(), - sql: demod.getSquelch(), + sql: demod.getSquelch() }, function(value, key){ if (typeof(value) === 'undefined' || value === false) return undefined; return key + '=' + value; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 8d1ffca8..5dff4374 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1098,7 +1098,8 @@ function on_ws_recv(evt) { return { name: d['mode'].toUpperCase(), modulation: d['mode'], - frequency: d['frequency'] + frequency: d['frequency'], + underlying: d['underlying'] }; }); bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies'); diff --git a/owrx/bands.py b/owrx/bands.py index 1aba72e5..ef4d0e91 100644 --- a/owrx/bands.py +++ b/owrx/bands.py @@ -1,4 +1,4 @@ -from owrx.modes import Modes +from owrx.modes import Modes, DigitalMode from datetime import datetime, timezone import json import os @@ -9,14 +9,14 @@ logger = logging.getLogger(__name__) class Band(object): - def __init__(self, dict): - self.name = dict["name"] - self.lower_bound = dict["lower_bound"] - self.upper_bound = dict["upper_bound"] + def __init__(self, b_dict): + self.name = b_dict["name"] + self.lower_bound = b_dict["lower_bound"] + self.upper_bound = b_dict["upper_bound"] self.frequencies = [] - if "frequencies" in dict: + if "frequencies" in b_dict: availableModes = [mode.modulation for mode in Modes.getAvailableModes()] - for (mode, freqs) in dict["frequencies"].items(): + for (mode, freqs) in b_dict["frequencies"].items(): if mode not in availableModes: logger.info( 'Modulation "{mode}" is not available, bandplan bookmark will not be displayed'.format( @@ -27,14 +27,30 @@ class Band(object): if not isinstance(freqs, list): freqs = [freqs] for f in freqs: - if not self.inBand(f): + f_dict = {"frequency": f} if not isinstance(f, dict) else f + f_dict["mode"] = mode + + if not self.inBand(f_dict["frequency"]): logger.warning( "Frequency for {mode} on {band} is not within band limits: {frequency}".format( - mode=mode, frequency=f, band=self.name + mode=mode, frequency=f_dict["frequency"], band=self.name ) ) continue - self.frequencies.append({"mode": mode, "frequency": f}) + + if "underlying" in f_dict: + m = Modes.findByModulation(mode) + if not isinstance(m, DigitalMode): + logger.warning("%s is not a digital mode, cannot be used with \"underlying\" config", mode) + continue + if f_dict["underlying"] not in m.underlying: + logger.warning( + "%s is not a valid underlying mode for %s; skipping", + f_dict["underlying"], + mode + ) + + self.frequencies.append(f_dict) def inBand(self, freq): return self.lower_bound <= freq <= self.upper_bound diff --git a/owrx/modes.py b/owrx/modes.py index 66812e1f..d606242a 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -55,6 +55,13 @@ class DigitalMode(Mode): def get_modulation(self): return self.get_underlying_mode().get_modulation() + def for_underlying(self, underlying: str): + if underlying not in self.underlying: + raise ValueError("{} is not a valid underlying mode for {}".format(underlying, self.modulation)) + return DigitalMode( + self.modulation, self.name, [underlying], self.bandpass, self.requirements, self.service, self.squelch + ) + class AudioChopperMode(DigitalMode, metaclass=ABCMeta): def __init__(self, modulation, name, bandpass=None, requirements=None): diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py index b271f9a1..6857ae03 100644 --- a/owrx/service/__init__.py +++ b/owrx/service/__init__.py @@ -122,6 +122,13 @@ class ServiceHandler(SdrSourceEventClient): self.startupTimer.start() def updateServices(self): + def addService(dial, source): + try: + service = self.setupService(dial, source) + self.services.append(service) + except Exception: + logger.exception("Error setting up service {mode} on frequency {frequency}".format(**dial)) + with self.lock: logger.debug("re-scheduling services due to sdr changes") self.stopServices() @@ -146,7 +153,7 @@ class ServiceHandler(SdrSourceEventClient): groups = self.optimizeResampling(dials, sr) if groups is None: for dial in dials: - self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source)) + addService(dial, self.source) else: for group in groups: if len(group) > 1: @@ -157,14 +164,14 @@ class ServiceHandler(SdrSourceEventClient): resampler = Resampler(resampler_props, self.source) for dial in group: - self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler)) + addService(dial, resampler) # resampler goes in after the services since it must not be shutdown as long as the services are # still running self.services.append(resampler) else: dial = group[0] - self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source)) + addService(dial, self.source) def get_min_max(self, group): frequencies = sorted(group, key=lambda f: f["frequency"]) @@ -238,23 +245,26 @@ class ServiceHandler(SdrSourceEventClient): return None return best["groups"] - def setupService(self, mode, frequency, source): - logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) + def setupService(self, dial, source): + logger.debug("setting up service {mode} on frequency {frequency}".format(**dial)) - modeObject = Modes.findByModulation(mode) + modeObject = Modes.findByModulation(dial["mode"]) if not isinstance(modeObject, DigitalMode): - logger.warning("mode is not a digimode: %s", mode) + logger.warning("mode is not a digimode: %s", dial["mode"]) return None + if "underlying" in dial: + modeObject = modeObject.for_underlying(dial["underlying"]) + demod = self._getDemodulator(modeObject.get_modulation()) secondaryDemod = self._getSecondaryDemodulator(modeObject.modulation) center_freq = source.getProps()["center_freq"] sampleRate = source.getProps()["samp_rate"] bandpass = modeObject.get_bandpass() if isinstance(secondaryDemod, DialFrequencyReceiver): - secondaryDemod.setDialFrequency(frequency) + secondaryDemod.setDialFrequency(dial["frequency"]) - chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, frequency - center_freq) + chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, dial["frequency"] - center_freq) chain.setBandPass(bandpass.low_cut, bandpass.high_cut) chain.setReader(source.getBuffer().getReader()) @@ -279,7 +289,6 @@ class ServiceHandler(SdrSourceEventClient): def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]: if isinstance(mod, ServiceDemodulatorChain): return mod - # TODO add remaining modes if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: from csdr.chain.digimodes import AudioChopperDemodulator from owrx.wsjt import WsjtParser @@ -297,7 +306,8 @@ class ServiceHandler(SdrSourceEventClient): elif mod == "sstv": from csdr.chain.digimodes import SstvDemodulator return SstvDemodulator(service=True) - return None + + raise ValueError("unsupported service modulation: {}".format(mod)) class Services(object):