diff --git a/htdocs/index.html b/htdocs/index.html index f6d08ac..98916dd 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -27,6 +27,7 @@ + diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js new file mode 100644 index 0000000..66a7196 --- /dev/null +++ b/htdocs/lib/Demodulator.js @@ -0,0 +1,217 @@ +function Filter() { + this.min_passband = 100; +} + +Filter.prototype.getLimits = function() { + var max_bw; + if (secondary_demod === 'pocsag') { + max_bw = 12500; + } else { + max_bw = (audioEngine.getOutputRate() / 2) - 1; + } + return { + high: max_bw, + low: -max_bw + }; +}; + +function Envelope(parent) { + this.parent = parent; + this.dragged_range = Demodulator.draggable_ranges.none; +} + +Envelope.prototype.draw = function(visible_range){ + this.visible_range = visible_range; + this.drag_ranges = demod_envelope_draw( + range, + center_freq + this.parent.offset_frequency + this.parent.low_cut, + center_freq + this.parent.offset_frequency + this.parent.high_cut, + this.color, center_freq + this.parent.offset_frequency + ); +}; + +Envelope.prototype.drag_start = function(x, key_modifiers){ + this.key_modifiers = key_modifiers; + this.dragged_range = demod_envelope_where_clicked(x, this.drag_ranges, key_modifiers); + this.drag_origin = { + x: x, + low_cut: this.parent.low_cut, + high_cut: this.parent.high_cut, + offset_frequency: this.parent.offset_frequency + }; + return this.dragged_range !== Demodulator.draggable_ranges.none; +}; + +Envelope.prototype.drag_move = function(x) { + var dr = Demodulator.draggable_ranges; + var new_value; + if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all + var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x)); + + //dragging the line in the middle of the filter envelope while holding Shift does emulate + //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged + //Filter passband moves in the opposite direction than dragged, hence the minus below. + var minus = (this.dragged_range === dr.bfo) ? -1 : 1; + //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob + //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset + //frequency. + if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { + //we don't let low_cut go beyond its limits + if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.parent.filter.getLimits().low) return true; + //nor the filter passband be too small + if (this.parent.high_cut - new_value < this.parent.filter.min_passband) return true; + //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" + if (new_value >= this.parent.high_cut) return true; + this.parent.low_cut = new_value; + } + if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { + //we don't let high_cut go beyond its limits + if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.parent.filter.getLimits().high) return true; + //nor the filter passband be too small + if (new_value - this.parent.low_cut < this.parent.filter.min_passband) return true; + //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" + if (new_value <= this.parent.low_cut) return true; + this.parent.high_cut = new_value; + } + if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) { + //when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it) + new_value = this.drag_origin.offset_frequency + freq_change; + if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-) + this.parent.offset_frequency = new_value; + } + //now do the actual modifications: + mkenvelopes(this.visible_range); + this.parent.set(); + //will have to change this when changing to multi-demodulator mode: + tunedFrequencyDisplay.setFrequency(center_freq + this.parent.offset_frequency); + return true; +}; + +Envelope.prototype.drag_end = function(){ + demodulator_buttons_update(); + var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset + this.dragged_range = Demodulator.draggable_ranges.none; + return to_return; +}; + + +//******* class Demodulator_default_analog ******* +// This can be used as a base for basic audio demodulators. +// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB + +function Demodulator(offset_frequency, modulation) { + this.offset_frequency = offset_frequency; + this.envelope = new Envelope(this); + this.color = Demodulator.get_next_color(); + this.modulation = modulation; + this.filter = new Filter(); + this.squelch_level = -150; + this.dmr_filter = 3; + this.state = {}; + var mode = Modes.findByModulation(modulation); + if (mode) { + this.low_cut = mode.bandpass.low_cut; + this.high_cut = mode.bandpass.high_cut; + } +} + +//ranges on filter envelope that can be dragged: +Demodulator.draggable_ranges = { + none: 0, + beginning: 1 /*from*/, + ending: 2 /*to*/, + anything_else: 3, + bfo: 4 /*line (while holding shift)*/, + pbs: 5 +}; //to which parameter these correspond in demod_envelope_draw() + +Demodulator.color_index = 0; +Demodulator.colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"]; + +Demodulator.get_next_color = function() { + if (this.color_index >= this.colors.length) this.color_index = 0; + return (this.colors[this.color_index++]); +} + + + +Demodulator.prototype.set_offset_frequency = function(to_what) { + if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return; + this.offset_frequency = Math.round(to_what); + this.set(); + mkenvelopes(get_visible_freq_range()); + tunedFrequencyDisplay.setFrequency(center_freq + to_what); + updateHash(); +}; + +Demodulator.prototype.get_offset_frequency = function() { + return this.offset_frequency; +}; + +Demodulator.prototype.get_modulation = function() { + return this.modulation; +}; + +Demodulator.prototype.start = function() { + this.set(); + ws.send(JSON.stringify({ + "type": "dspcontrol", + "action": "start" + })); +}; + +// TODO check if this is actually used +Demodulator.prototype.stop = function() { +}; + +Demodulator.prototype.send = function(params) { + ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); +} + +Demodulator.prototype.set = function () { //this function sends demodulator parameters to the server + var params = { + "low_cut": this.low_cut, + "high_cut": this.high_cut, + "offset_freq": this.offset_frequency, + "mod": this.modulation, + "dmr_filter": this.dmr_filter, + "squelch_level": this.squelch_level + }; + var to_send = {}; + for (var key in params) { + if (!(key in this.state) || params[key] !== this.state[key]) { + to_send[key] = params[key]; + } + } + if (Object.keys(to_send).length > 0) { + this.send(to_send); + for (var key in to_send) { + this.state[key] = to_send[key]; + } + } + mkenvelopes(get_visible_freq_range()); +}; + +Demodulator.prototype.setSquelch = function(squelch) { + this.squelch_level = squelch; + this.set(); +}; + +Demodulator.prototype.setDmrFilter = function(dmr_filter) { + this.dmr_filter = dmr_filter; + this.set(); +}; + +Demodulator.prototype.setBandpass = function(bandpass) { + this.bandpass = bandpass; + this.low_cut = bandpass.low_cut; + this.high_cut = bandpass.high_cut; + this.set(); +}; + +Demodulator.prototype.getBandpass = function() { + return { + low_cut: this.low_cut, + high_cut: this.high_cut + }; +}; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 15c7f24..1dcdd3f 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -142,7 +142,7 @@ function setSquelchToAuto() { function updateSquelch() { var sliderValue = parseInt($("#openwebrx-panel-squelch").val()); - ws.send(JSON.stringify({"type": "dspcontrol", "params": {"squelch_level": sliderValue}})); + if (demodulators[0]) demodulators[0].setSquelch(sliderValue); } var waterfall_min_level; @@ -238,14 +238,6 @@ function typeInAnimation(element, timeout, what, onFinish) { demodulators = []; -var demodulator_color_index = 0; -var demodulator_colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"]; - -function demodulators_get_next_color() { - if (demodulator_color_index >= demodulator_colors.length) demodulator_color_index = 0; - return (demodulator_colors[demodulator_color_index++]); -} - function demod_envelope_draw(range, from, to, color, line) { // ____ // Draws a standard filter envelope like this: _/ \_ // Parameters are given in offset frequency (Hz). @@ -331,166 +323,6 @@ function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) { // Check return dr.none; //User doesn't drag the envelope for this demodulator } -//******* class Demodulator ******* -// this can be used as a base class for ANY demodulator -Demodulator = function (offset_frequency) { - this.offset_frequency = offset_frequency; - this.envelope = {}; - this.color = demodulators_get_next_color(); - this.stop = function () { - }; -}; -//ranges on filter envelope that can be dragged: -Demodulator.draggable_ranges = { - none: 0, - beginning: 1 /*from*/, - ending: 2 /*to*/, - anything_else: 3, - bfo: 4 /*line (while holding shift)*/, - pbs: 5 -}; //to which parameter these correspond in demod_envelope_draw() - -//******* class Demodulator_default_analog ******* -// This can be used as a base for basic audio demodulators. -// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB - -demodulator_response_time = 50; - -function Demodulator_default_analog(offset_frequency, subtype) { - Demodulator.call(this, offset_frequency); - this.subtype = subtype; - this.filter = { - min_passband: 100, - getLimits: function() { - var max_bw; - if (secondary_demod === 'pocsag') { - max_bw = 12500; - } else { - max_bw = (audioEngine.getOutputRate() / 2) - 1; - } - return { - high: max_bw, - low: -max_bw - }; - } - }; - var mode = Modes.findByModulation(subtype); - if (mode) { - this.low_cut = mode.bandpass.low_cut; - this.high_cut = mode.bandpass.high_cut; - } - - this.wait_for_timer = false; - this.set_after = false; - this.set = function () { //set() is a wrapper to call doset(), but it ensures that doset won't execute more frequently than demodulator_response_time. - if (!this.wait_for_timer) { - this.doset(false); - this.set_after = false; - this.wait_for_timer = true; - var timeout_this = this; //http://stackoverflow.com/a/2130411 - window.setTimeout(function () { - timeout_this.wait_for_timer = false; - if (timeout_this.set_after) timeout_this.set(); - }, demodulator_response_time); - } else { - this.set_after = true; - } - }; - - this.doset = function (first_time) { //this function sends demodulator parameters to the server - var params = { - "low_cut": this.low_cut, - "high_cut": this.high_cut, - "offset_freq": this.offset_frequency - }; - if (first_time) params.mod = this.subtype; - ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); - mkenvelopes(get_visible_freq_range()); - }; - this.doset(true); //we set parameters on object creation - - //******* envelope object ******* - // for drawing the filter envelope above scale - this.envelope.parent = this; - - this.envelope.draw = function (visible_range) { - this.visible_range = visible_range; - this.drag_ranges = demod_envelope_draw(range, - center_freq + this.parent.offset_frequency + this.parent.low_cut, - center_freq + this.parent.offset_frequency + this.parent.high_cut, - this.color, center_freq + this.parent.offset_frequency); - }; - - this.envelope.dragged_range = Demodulator.draggable_ranges.none; - - // event handlers - this.envelope.drag_start = function (x, key_modifiers) { - this.key_modifiers = key_modifiers; - this.dragged_range = demod_envelope_where_clicked(x, this.drag_ranges, key_modifiers); - this.drag_origin = { - x: x, - low_cut: this.parent.low_cut, - high_cut: this.parent.high_cut, - offset_frequency: this.parent.offset_frequency - }; - return this.dragged_range !== Demodulator.draggable_ranges.none; - }; - - this.envelope.drag_move = function (x) { - var dr = Demodulator.draggable_ranges; - var new_value; - if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all - var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x)); - - //dragging the line in the middle of the filter envelope while holding Shift does emulate - //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged - //Filter passband moves in the opposite direction than dragged, hence the minus below. - var minus = (this.dragged_range === dr.bfo) ? -1 : 1; - //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob - //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset - //frequency. - if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { - //we don't let low_cut go beyond its limits - if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.parent.filter.getLimits().low) return true; - //nor the filter passband be too small - if (this.parent.high_cut - new_value < this.parent.filter.min_passband) return true; - //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" - if (new_value >= this.parent.high_cut) return true; - this.parent.low_cut = new_value; - } - if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { - //we don't let high_cut go beyond its limits - if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.parent.filter.getLimits().high) return true; - //nor the filter passband be too small - if (new_value - this.parent.low_cut < this.parent.filter.min_passband) return true; - //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" - if (new_value <= this.parent.low_cut) return true; - this.parent.high_cut = new_value; - } - if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) { - //when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it) - new_value = this.drag_origin.offset_frequency + freq_change; - if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-) - this.parent.offset_frequency = new_value; - } - //now do the actual modifications: - mkenvelopes(this.visible_range); - this.parent.set(); - //will have to change this when changing to multi-demodulator mode: - tunedFrequencyDisplay.setFrequency(center_freq + this.parent.offset_frequency); - return true; - }; - - this.envelope.drag_end = function () { //in this demodulator we've already changed values in the drag_move() function so we shouldn't do too much here. - demodulator_buttons_update(); - var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset - this.dragged_range = Demodulator.draggable_ranges.none; - return to_return; - }; - -} - -Demodulator_default_analog.prototype = new Demodulator(); function mkenvelopes(visible_range) //called from mkscale { @@ -498,7 +330,10 @@ function mkenvelopes(visible_range) //called from mkscale for (var i = 0; i < demodulators.length; i++) { demodulators[i].envelope.draw(visible_range); } - if (demodulators.length) secondary_demod_waterfall_set_zoom(demodulators[0].low_cut, demodulators[0].high_cut); + if (demodulators.length) { + var bandpass = demodulators[0].getBandpass() + secondary_demod_waterfall_set_zoom(bandpass.low_cut, bandpass.high_cut); + } } function demodulator_remove(which) { @@ -519,33 +354,22 @@ function demodulator_analog_replace(subtype, for_digital) { //this function shou secondary_demod_close_window(); secondary_demod_listbox_update(); } - if (!demodulators || !demodulators[0] || demodulators[0].subtype !== subtype) { + if (!demodulators || !demodulators[0] || demodulators[0].get_modulation() !== subtype) { last_analog_demodulator_subtype = subtype; var temp_offset = 0; if (demodulators.length) { - temp_offset = demodulators[0].offset_frequency; + temp_offset = demodulators[0].get_offset_frequency(); demodulator_remove(0); } - demodulator_add(new Demodulator_default_analog(temp_offset, subtype)); + var demod = new Demodulator(temp_offset, subtype); + demod.start(); + demodulator_add(demod); } demodulator_buttons_update(); update_digitalvoice_panels("openwebrx-panel-metadata-" + subtype); updateHash(); } -Demodulator.prototype.set_offset_frequency = function(to_what) { - if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return; - this.offset_frequency = Math.round(to_what); - this.set(); - mkenvelopes(get_visible_freq_range()); - tunedFrequencyDisplay.setFrequency(center_freq + to_what); - updateHash(); -} - -Demodulator.prototype.get_offset_frequency = function() { - return this.offset_frequency; -} - function waterfallWidth() { return $('body').width(); } @@ -988,7 +812,7 @@ function zoom_set(level) { level = parseInt(level); zoom_level = level; //zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+waterfallWidth()/2); //zoom to screen center instead of demod envelope - zoom_center_rel = demodulators[0].offset_frequency; + zoom_center_rel = demodulators[0].get_offset_frequency(); zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack resize_canvases(true); mkscale(); @@ -1468,12 +1292,6 @@ var mute = false; // Optimalise these if audio lags or is choppy: var audio_buffer_maximal_length_sec = 1; //actual number of samples are calculated from sample rate -function webrx_set_param(what, value) { - var params = {}; - params[what] = value; - ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); -} - function parseHash() { if (!window.location.hash) { return {}; @@ -1512,7 +1330,7 @@ function updateHash() { if (!demod) return; window.location.hash = $.map({ freq: demod.get_offset_frequency() + center_freq, - mod: demod.subtype, + mod: demod.get_modulation(), secondary_mod: secondary_demod }, function(value, key){ if (!value) return undefined; @@ -1543,6 +1361,7 @@ function synchronize_demodulator_init(params) { sync_params = $.extend(sync_params, params || {}); if (sync_params.initialParams && sync_params.modes && sync_params.features) { initialize_demodulator(sync_params.initialParams); + delete sync_params.initialParams; } } @@ -1557,10 +1376,7 @@ function initialize_demodulator(initialParams) { if (params.offset_frequency) { demodulators[0].set_offset_frequency(params.offset_frequency); } - ws.send(JSON.stringify({ - "type": "dspcontrol", - "action": "start" - })); + demodulators[0].start() } var reconnect_timeout = false; @@ -1836,7 +1652,7 @@ function update_dmr_timeslot_filtering() { }).toArray().reduce(function (acc, v) { return acc | v; }, 0); - webrx_set_param("dmr_filter", filter); + demodulators[0].setDmrFilter(filter); } function playButtonClick() { @@ -1932,7 +1748,7 @@ function demodulator_buttons_update() { var $buttons = $(".openwebrx-demodulator-button"); $buttons.removeClass("highlighted").removeClass('disabled'); if (!demodulators.length) return; - var mod = demodulators[0].subtype; + var mod = demodulators[0].get_modulation(); $("#openwebrx-button-" + mod).addClass("highlighted"); if (secondary_demod) { $("#openwebrx-button-dig").addClass("highlighted"); @@ -1997,9 +1813,7 @@ function demodulator_digital_replace(subtype) { secondary_demod_start(subtype); demodulator_analog_replace(mode.underlying[0], true); if (mode.bandpass) { - demodulators[0].low_cut = mode.bandpass.low_cut; - demodulators[0].high_cut = mode.bandpass.high_cut; - demodulators[0].set(); + demodulators[0].setBandpass(mode.bandpass); } demodulator_buttons_update(); $('#openwebrx-panel-digimodes').attr('data-mode', subtype); diff --git a/owrx/connection.py b/owrx/connection.py index 8deff4f..07c32ae 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -139,6 +139,7 @@ class OpenWebRxReceiverClient(Client): def handleTextMessage(self, conn, message): try: message = json.loads(message) + logger.debug(message) if "type" in message: if message["type"] == "dspcontrol": if "action" in message and message["action"] == "start": @@ -146,7 +147,7 @@ class OpenWebRxReceiverClient(Client): if "params" in message: params = message["params"] - self.setDspProperties(params) + self.getDsp().setProperties(params) elif message["type"] == "config": if "params" in message: @@ -163,7 +164,7 @@ class OpenWebRxReceiverClient(Client): if "params" in message: self.connectionProperties = message["params"] if self.dsp: - self.setDspProperties(self.connectionProperties) + self.getDsp().setProperties(self.connectionProperties) else: logger.warning("received message without type: {0}".format(message)) @@ -172,39 +173,30 @@ class OpenWebRxReceiverClient(Client): logger.warning("message is not json: {0}".format(message)) def setSdr(self, id=None): - while True: - next = None - if id is not None: - next = SdrService.getSource(id) - if next is None: - next = SdrService.getFirstSource() - if next is None: - # exit condition: no sdrs available - self.handleNoSdrsAvailable() - return + next = None + if id is not None: + next = SdrService.getSource(id) + if next is None: + next = SdrService.getFirstSource() + if next is None: + # exit condition: no sdrs available + self.handleNoSdrsAvailable() + return - # exit condition: no change - if next == self.sdr: - return + # exit condition: no change + if next == self.sdr: + return - self.stopDsp() + self.stopDsp() - if self.configSub is not None: - self.configSub.cancel() - self.configSub = None + if self.configSub is not None: + self.configSub.cancel() + self.configSub = None - self.sdr = next - - self.startDsp() - - # keep trying until we find a suitable SDR - if self.sdr.getState() == SdrSource.STATE_FAILED: - self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName())) - else: - break + self.sdr = next # send initial config - self.setDspProperties(self.connectionProperties) + self.getDsp().setProperties(self.connectionProperties) stack = PropertyStack() stack.addLayer(0, self.sdr.getProps()) @@ -236,9 +228,14 @@ class OpenWebRxReceiverClient(Client): self.write_sdr_error("No SDR Devices available") def startDsp(self): - if self.dsp is None and self.sdr is not None: - self.dsp = DspManager(self, self.sdr) - self.dsp.start() + while True: + logger.debug("starting dsp...") + self.getDsp().start() + if self.sdr.getState() == SdrSource.STATE_FAILED: + self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName())) + self.setSdr() + else: + break def close(self): self.stopDsp() @@ -250,6 +247,7 @@ class OpenWebRxReceiverClient(Client): super().close() def stopDsp(self): + logger.debug("stopDsp") if self.dsp is not None: self.dsp.stop() self.dsp = None @@ -270,9 +268,11 @@ class OpenWebRxReceiverClient(Client): for key, value in params.items(): protected[key] = value - def setDspProperties(self, params): - for key, value in params.items(): - self.dsp.setProperty(key, value) + def getDsp(self): + if self.dsp is None: + logger.debug("new DSP") + self.dsp = DspManager(self, self.sdr) + return self.dsp def write_spectrum_data(self, data): self.mp_send(bytes([0x01]) + data) diff --git a/owrx/dsp.py b/owrx/dsp.py index 1f8850e..979011e 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -135,6 +135,7 @@ class DspManager(csdr.output): super().__init__() def start(self): + logger.debug(self.sdrSource) if self.sdrSource.isAvailable(): self.dsp.start() @@ -160,6 +161,10 @@ class DspManager(csdr.output): sub.cancel() self.subscriptions = [] + def setProperties(self, props): + for k, v in props.items(): + self.setProperty(k, v) + def setProperty(self, prop, value): self.props[prop] = value