-
-
FM
-
AM
-
LSB
-
USB
-
CW
-
-
-
DMR
-
DStar
-
NXDN
-
YSF
-
-
-
DIG
-
-
+
@@ -250,17 +214,7 @@
-
+
Cancel
diff --git a/htdocs/lib/BookmarkBar.js b/htdocs/lib/BookmarkBar.js
index d039494..ecae43b 100644
--- a/htdocs/lib/BookmarkBar.js
+++ b/htdocs/lib/BookmarkBar.js
@@ -8,12 +8,10 @@ function BookmarkBar() {
var $bookmark = $(e.target).closest('.bookmark');
me.$container.find('.bookmark').removeClass('selected');
var b = $bookmark.data();
- if (!b || !b.frequency || (!b.modulation && !b.digital_modulation)) return;
- demodulators[0].set_offset_frequency(b.frequency - center_freq);
+ if (!b || !b.frequency || !b.modulation) return;
+ me.getDemodulator().set_offset_frequency(b.frequency - center_freq);
if (b.modulation) {
- demodulator_analog_replace(b.modulation);
- } else if (b.digital_modulation) {
- demodulator_digital_replace(b.digital_modulation);
+ me.getDemodulatorPanel().setMode(b.modulation);
}
$bookmark.addClass('selected');
});
@@ -104,40 +102,26 @@ BookmarkBar.prototype.render = function(){
};
BookmarkBar.prototype.showEditDialog = function(bookmark) {
- var $form = this.$dialog.find("form");
if (!bookmark) {
bookmark = {
name: "",
- frequency: center_freq + demodulators[0].offset_frequency,
- modulation: demodulators[0].subtype
+ frequency: center_freq + this.getDemodulator().get_offset_frequency(),
+ modulation: this.getDemodulator().get_secondary_demod() || this.getDemodulator().get_modulation()
}
}
- ['name', 'frequency', 'modulation'].forEach(function(key){
- $form.find('#' + key).val(bookmark[key]);
- });
- this.$dialog.data('id', bookmark.id);
+ this.$dialog.bookmarkDialog().setValues(bookmark);
this.$dialog.show();
this.$dialog.find('#name').focus();
};
BookmarkBar.prototype.storeBookmark = function() {
var me = this;
- var bookmark = {};
- var valid = true;
- ['name', 'frequency', 'modulation'].forEach(function(key){
- var $input = me.$dialog.find('#' + key);
- valid = valid && $input[0].checkValidity();
- bookmark[key] = $input.val();
- });
- if (!valid) {
- me.$dialog.find("form :submit").click();
- return;
- }
+ var bookmark = this.$dialog.bookmarkDialog().getValues();
+ if (!bookmark) return;
bookmark.frequency = Number(bookmark.frequency);
var bookmarks = me.localBookmarks.getBookmarks();
- bookmark.id = me.$dialog.data('id');
if (!bookmark.id) {
if (bookmarks.length) {
bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; }));
@@ -154,6 +138,14 @@ BookmarkBar.prototype.storeBookmark = function() {
me.$dialog.hide();
};
+BookmarkBar.prototype.getDemodulatorPanel = function() {
+ return $('#openwebrx-panel-receiver').demodulatorPanel();
+};
+
+BookmarkBar.prototype.getDemodulator = function() {
+ return this.getDemodulatorPanel().getDemodulator();
+};
+
BookmarkLocalStorage = function(){
};
@@ -171,7 +163,3 @@ BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
this.setBookmarks(bookmarks);
};
-
-
-
-
diff --git a/htdocs/lib/BookmarkDialog.js b/htdocs/lib/BookmarkDialog.js
new file mode 100644
index 0000000..4a0a184
--- /dev/null
+++ b/htdocs/lib/BookmarkDialog.js
@@ -0,0 +1,36 @@
+$.fn.bookmarkDialog = function() {
+ var $el = this;
+ return {
+ setModes: function(modes) {
+ $el.find('#modulation').html(modes.filter(function(m){
+ return m.isAvailable();
+ }).map(function(m) {
+ return '
';
+ }).join(''));
+ return this;
+ },
+ setValues: function(bookmark) {
+ var $form = $el.find('form');
+ ['name', 'frequency', 'modulation'].forEach(function(key){
+ $form.find('#' + key).val(bookmark[key]);
+ });
+ $el.data('id', bookmark.id || false);
+ return this;
+ },
+ getValues: function() {
+ var bookmark = {};
+ var valid = true;
+ ['name', 'frequency', 'modulation'].forEach(function(key){
+ var $input = $el.find('#' + key);
+ valid = valid && $input[0].checkValidity();
+ bookmark[key] = $input.val();
+ });
+ if (!valid) {
+ $el.find("form :submit").click();
+ return;
+ }
+ bookmark.id = $el.data('id');
+ return bookmark;
+ }
+ }
+}
\ No newline at end of file
diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js
new file mode 100644
index 0000000..1c47560
--- /dev/null
+++ b/htdocs/lib/Demodulator.js
@@ -0,0 +1,356 @@
+function Filter(demodulator) {
+ this.demodulator = demodulator;
+ this.min_passband = 100;
+}
+
+Filter.prototype.getLimits = function() {
+ var max_bw;
+ if (this.demodulator.get_secondary_demod() === 'pocsag') {
+ max_bw = 12500;
+ } else {
+ max_bw = (audioEngine.getOutputRate() / 2) - 1;
+ }
+ return {
+ high: max_bw,
+ low: -max_bw
+ };
+};
+
+function Envelope(demodulator) {
+ this.demodulator = demodulator;
+ this.dragged_range = Demodulator.draggable_ranges.none;
+}
+
+Envelope.prototype.draw = function(visible_range){
+ this.visible_range = visible_range;
+ var line = center_freq + this.demodulator.offset_frequency;
+
+ // ____
+ // Draws a standard filter envelope like this: _/ \_
+ // Parameters are given in offset frequency (Hz).
+ // Envelope is drawn on the scale canvas.
+ // A "drag range" object is returned, containing information about the draggable areas of the envelope
+ // (beginning, ending and the line showing the offset frequency).
+ var env_bounding_line_w = 5; //
+ var env_att_w = 5; // _______ ___env_h2 in px ___|_____
+ var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_
+ var env_h2 = 5; // |||env_att_line_w |_env_lineplus
+ var env_lineplus = 1; // ||env_bounding_line_w
+ var env_line_click_area = 6;
+ //range=get_visible_freq_range();
+ var from = center_freq + this.demodulator.offset_frequency + this.demodulator.low_cut;
+ var from_px = scale_px_from_freq(from, range);
+ var to = center_freq + this.demodulator.offset_frequency + this.demodulator.high_cut;
+ var to_px = scale_px_from_freq(to, range);
+ if (to_px < from_px) /* swap'em */ {
+ var temp_px = to_px;
+ to_px = from_px;
+ from_px = temp_px;
+ }
+
+ from_px -= (env_att_w + env_bounding_line_w);
+ to_px += (env_att_w + env_bounding_line_w);
+ // do drawing:
+ scale_ctx.lineWidth = 3;
+ var color = this.color || '#ffff00'; // yellow
+ scale_ctx.strokeStyle = color;
+ scale_ctx.fillStyle = color;
+ var drag_ranges = {envelope_on_screen: false, line_on_screen: false};
+ if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen?
+ {
+ drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w};
+ drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px};
+ drag_ranges.whole_envelope = {x1: from_px, x2: to_px};
+ drag_ranges.envelope_on_screen = true;
+ scale_ctx.beginPath();
+ scale_ctx.moveTo(from_px, env_h1);
+ scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1);
+ scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2);
+ scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2);
+ scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1);
+ scale_ctx.lineTo(to_px, env_h1);
+ scale_ctx.globalAlpha = 0.3;
+ scale_ctx.fill();
+ scale_ctx.globalAlpha = 1;
+ scale_ctx.stroke();
+ }
+ if (typeof line !== "undefined") // out of screen?
+ {
+ var line_px = scale_px_from_freq(line, range);
+ if (!(line_px < 0 || line_px > window.innerWidth)) {
+ drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2};
+ drag_ranges.line_on_screen = true;
+ scale_ctx.moveTo(line_px, env_h1 + env_lineplus);
+ scale_ctx.lineTo(line_px, env_h2 - env_lineplus);
+ scale_ctx.stroke();
+ }
+ }
+ this.drag_ranges = drag_ranges;
+};
+
+Envelope.prototype.drag_start = function(x, key_modifiers){
+ this.key_modifiers = key_modifiers;
+ this.dragged_range = this.where_clicked(x, this.drag_ranges, key_modifiers);
+ this.drag_origin = {
+ x: x,
+ low_cut: this.demodulator.low_cut,
+ high_cut: this.demodulator.high_cut,
+ offset_frequency: this.demodulator.offset_frequency
+ };
+ return this.dragged_range !== Demodulator.draggable_ranges.none;
+};
+
+Envelope.prototype.where_clicked = function(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by envelope_draw().
+ var in_range = function (x, range) {
+ return range.x1 <= x && range.x2 >= x;
+ };
+ var dr = Demodulator.draggable_ranges;
+
+ if (key_modifiers.shiftKey) {
+ //Check first: shift + center drag emulates BFO knob
+ if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo;
+ //Check second: shift + envelope drag emulates PBF knob
+ if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs;
+ }
+ if (drag_ranges.envelope_on_screen) {
+ // For low and high cut:
+ if (in_range(x, drag_ranges.beginning)) return dr.beginning;
+ if (in_range(x, drag_ranges.ending)) return dr.ending;
+ // Last priority: having clicked anything else on the envelope, without holding the shift key
+ if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else;
+ }
+ return dr.none; //User doesn't drag the envelope for this demodulator
+};
+
+
+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.demodulator.filter.getLimits().low) return true;
+ //nor the filter passband be too small
+ if (this.demodulator.high_cut - new_value < this.demodulator.filter.min_passband) return true;
+ //sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
+ if (new_value >= this.demodulator.high_cut) return true;
+ this.demodulator.setLowCut(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.demodulator.filter.getLimits().high) return true;
+ //nor the filter passband be too small
+ if (new_value - this.demodulator.low_cut < this.demodulator.filter.min_passband) return true;
+ //sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
+ if (new_value <= this.demodulator.low_cut) return true;
+ this.demodulator.setHighCut(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.demodulator.set_offset_frequency(new_value);
+ }
+ //now do the actual modifications:
+ //mkenvelopes(this.visible_range);
+ //this.demodulator.set();
+ return true;
+};
+
+Envelope.prototype.drag_end = function(){
+ 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);
+ this.squelch_level = -150;
+ this.dmr_filter = 3;
+ this.started = false;
+ this.state = {};
+ this.secondary_demod = false;
+ var mode = Modes.findByModulation(modulation);
+ if (mode) {
+ this.low_cut = mode.bandpass.low_cut;
+ this.high_cut = mode.bandpass.high_cut;
+ }
+ this.listeners = {
+ "frequencychange": [],
+ "squelchchange": []
+ };
+}
+
+//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 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.on = function(event, handler) {
+ this.listeners[event].push(handler);
+};
+
+Demodulator.prototype.emit = function(event, params) {
+ this.listeners[event].forEach(function(fn) {
+ fn(params);
+ });
+};
+
+Demodulator.prototype.set_offset_frequency = function(to_what) {
+ if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
+ to_what = Math.round(to_what);
+ if (this.offset_frequency === to_what) {
+ return;
+ }
+ this.offset_frequency = to_what;
+ this.set();
+ this.emit("frequencychange", to_what);
+ mkenvelopes(get_visible_freq_range());
+};
+
+Demodulator.prototype.get_offset_frequency = function() {
+ return this.offset_frequency;
+};
+
+Demodulator.prototype.get_modulation = function() {
+ return this.modulation;
+};
+
+Demodulator.prototype.start = function() {
+ this.started = true;
+ 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
+ if (!this.started) return;
+ 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,
+ "secondary_mod": this.secondary_demod,
+ "secondary_offset_freq": this.secondary_offset_freq
+ };
+ 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) {
+ if (this.squelch_level == squelch) {
+ return;
+ }
+ this.squelch_level = squelch;
+ this.set();
+ this.emit("squelchchange", squelch);
+};
+
+Demodulator.prototype.getSquelch = function() {
+ return this.squelch_level;
+};
+
+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.setLowCut = function(low_cut) {
+ this.low_cut = low_cut;
+ this.set();
+};
+
+Demodulator.prototype.setHighCut = function(high_cut) {
+ this.high_cut = high_cut;
+ this.set();
+};
+
+Demodulator.prototype.getBandpass = function() {
+ return {
+ low_cut: this.low_cut,
+ high_cut: this.high_cut
+ };
+};
+
+Demodulator.prototype.set_secondary_demod = function(secondary_demod) {
+ if (this.secondary_demod === secondary_demod) {
+ return;
+ }
+ this.secondary_demod = secondary_demod;
+ this.set();
+};
+
+Demodulator.prototype.get_secondary_demod = function() {
+ return this.secondary_demod;
+};
+
+Demodulator.prototype.set_secondary_offset_freq = function(secondary_offset) {
+ if (this.secondary_offset_freq === secondary_offset) {
+ return;
+ }
+ this.secondary_offset_freq = secondary_offset;
+ this.set();
+};
diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js
new file mode 100644
index 0000000..439a8e5
--- /dev/null
+++ b/htdocs/lib/DemodulatorPanel.js
@@ -0,0 +1,333 @@
+function DemodulatorPanel(el) {
+ var self = this;
+ self.el = el;
+ self.demodulator = null;
+ self.mode = null;
+
+ var displayEl = el.find('.webrx-actual-freq')
+ this.tuneableFrequencyDisplay = displayEl.tuneableFrequencyDisplay();
+ displayEl.on('frequencychange', function(event, freq) {
+ self.getDemodulator().set_offset_frequency(freq - self.center_freq);
+ });
+
+ Modes.registerModePanel(this);
+ el.on('click', '.openwebrx-demodulator-button', function() {
+ var modulation = $(this).data('modulation');
+ if (modulation) {
+ self.setMode(modulation);
+ } else {
+ self.disableDigiMode();
+ }
+ });
+ el.on('change', '.openwebrx-secondary-demod-listbox', function() {
+ var value = $(this).val();
+ if (value === 'none') {
+ self.disableDigiMode();
+ } else {
+ self.setMode(value);
+ }
+ });
+ el.on('click', '.openwebrx-squelch-default', function() {
+ if (!self.squelchAvailable()) return;
+ el.find('.openwebrx-squelch-slider').val(getLogSmeterValue(smeter_level) + 10);
+ self.updateSquelch();
+ });
+ el.on('change', '.openwebrx-squelch-slider', function() {
+ self.updateSquelch();
+ });
+ window.addEventListener('hashchange', function() {
+ self.onHashChange();
+ });
+};
+
+DemodulatorPanel.prototype.render = function() {
+ var available = Modes.getModes().filter(function(m){ return m.isAvailable(); });
+ var normalModes = available.filter(function(m){ return m.type === 'analog'; });
+ var digiModes = available.filter(function(m){ return m.type === 'digimode'; });
+
+ var html = []
+
+ var buttons = normalModes.map(function(m){
+ return $(
+ '
' + m.name + '
'
+ );
+ });
+
+ var index = 0;
+ var arrayLength = buttons.length;
+ var chunks = [];
+
+ for (index = 0; index < arrayLength; index += 5) {
+ chunks.push(buttons.slice(index, index + 5));
+ }
+
+ html.push.apply(html, chunks.map(function(chunk){
+ $line = $('
');
+ $line.append.apply($line, chunk);
+ return $line
+ }));
+
+ html.push($(
+ '
' +
+ '
DIG
' +
+ '
' +
+ '
'
+ ));
+
+ this.el.find(".openwebrx-modes").html(html);
+};
+
+DemodulatorPanel.prototype.setMode = function(requestedModulation) {
+ var mode = Modes.findByModulation(requestedModulation);
+ if (!mode) {
+ return;
+ }
+ if (this.mode === mode) {
+ return;
+ }
+ if (!mode.isAvailable()) {
+ divlog('Modulation "' + mode.name + '" not supported. Please check requirements', true);
+ return;
+ }
+
+ 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;
+ } else {
+ modulation = mode.modulation;
+ }
+ }
+
+ var current = this.collectParams();
+ if (this.demodulator) {
+ current.offset_frequency = this.demodulator.get_offset_frequency();
+ current.squelch_level = this.demodulator.getSquelch();
+ }
+
+ this.stopDemodulator();
+ this.demodulator = new Demodulator(current.offset_frequency, modulation);
+ this.demodulator.setSquelch(current.squelch_level);
+
+ var self = this;
+ var updateFrequency = function(freq) {
+ self.tuneableFrequencyDisplay.setFrequency(self.center_freq + freq);
+ self.updateHash();
+ };
+ this.demodulator.on("frequencychange", updateFrequency);
+ updateFrequency(this.demodulator.get_offset_frequency());
+ var updateSquelch = function(squelch) {
+ self.el.find('.openwebrx-squelch-slider').val(squelch);
+ self.updateHash();
+ };
+ this.demodulator.on('squelchchange', updateSquelch);
+ updateSquelch(this.demodulator.getSquelch());
+
+ if (mode.type === 'digimode') {
+ this.demodulator.set_secondary_demod(mode.modulation);
+ if (mode.bandpass) {
+ this.demodulator.setBandpass(mode.bandpass);
+ }
+ } else {
+ this.demodulator.set_secondary_demod(false);
+ }
+
+ this.demodulator.start();
+ this.mode = mode;
+
+ this.updateButtons();
+ this.updatePanels();
+ this.updateHash();
+};
+
+DemodulatorPanel.prototype.disableDigiMode = function() {
+ // just a little trick to get out of the digimode
+ delete this.mode;
+ this.setMode(this.getDemodulator().get_modulation());
+};
+
+DemodulatorPanel.prototype.updatePanels = function() {
+ var modulation = this.getDemodulator().get_secondary_demod();
+ $('#openwebrx-panel-digimodes').attr('data-mode', modulation);
+ toggle_panel("openwebrx-panel-digimodes", !!modulation);
+ toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(modulation) >= 0);
+ toggle_panel("openwebrx-panel-js8-message", modulation == "js8");
+ toggle_panel("openwebrx-panel-packet-message", modulation === "packet");
+ toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");
+
+ modulation = this.getDemodulator().get_modulation();
+ var showing = 'openwebrx-panel-metadata-' + modulation;
+ $(".openwebrx-meta-panel").each(function (_, p) {
+ toggle_panel(p.id, p.id === showing);
+ });
+ clear_metadata();
+};
+
+DemodulatorPanel.prototype.getDemodulator = function() {
+ return this.demodulator;
+};
+
+DemodulatorPanel.prototype.collectParams = function() {
+ var defaults = {
+ offset_frequency: 0,
+ squelch_level: -150,
+ mod: 'nfm'
+ }
+ return $.extend(new Object(), defaults, this.initialParams || {}, this.transformHashParams(this.parseHash()));
+};
+
+DemodulatorPanel.prototype.startDemodulator = function() {
+ if (!Modes.initComplete()) return;
+ var params = this.collectParams();
+ this._apply(params);
+};
+
+DemodulatorPanel.prototype.stopDemodulator = function() {
+ if (!this.demodulator) {
+ return;
+ }
+ this.demodulator.stop();
+ this.demodulator = null;
+ this.mode = null;
+}
+
+DemodulatorPanel.prototype._apply = function(params) {
+ this.setMode(params.mod);
+ this.getDemodulator().set_offset_frequency(params.offset_frequency);
+ this.getDemodulator().setSquelch(params.squelch_level);
+ this.updateButtons();
+};
+
+DemodulatorPanel.prototype.setInitialParams = function(params) {
+ this.initialParams = params;
+};
+
+DemodulatorPanel.prototype.onHashChange = function() {
+ this._apply(this.transformHashParams(this.parseHash()));
+};
+
+DemodulatorPanel.prototype.transformHashParams = function(params) {
+ var ret = {
+ mod: params.secondary_mod || params.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;
+};
+
+DemodulatorPanel.prototype.squelchAvailable = function () {
+ return this.mode && this.mode.squelch;
+}
+
+DemodulatorPanel.prototype.updateButtons = function() {
+ var $buttons = this.el.find(".openwebrx-demodulator-button");
+ $buttons.removeClass("highlighted").removeClass('same-mod');
+ var demod = this.getDemodulator()
+ if (!demod) return;
+ this.el.find('[data-modulation=' + demod.get_modulation() + ']').addClass("highlighted");
+ var secondary_demod = demod.get_secondary_demod()
+ if (secondary_demod) {
+ this.el.find(".openwebrx-button-dig").addClass("highlighted");
+ this.el.find('.openwebrx-secondary-demod-listbox').val(secondary_demod);
+ var mode = Modes.findByModulation(secondary_demod);
+ if (mode) {
+ var self = this;
+ mode.underlying.filter(function(m) {
+ return m !== demod.get_modulation();
+ }).forEach(function(m) {
+ self.el.find('[data-modulation=' + m + ']').addClass('same-mod')
+ });
+ }
+ } else {
+ this.el.find('.openwebrx-secondary-demod-listbox').val('none');
+ }
+ var squelch_disabled = !this.squelchAvailable();
+ this.el.find('.openwebrx-squelch-slider').prop('disabled', squelch_disabled);
+ this.el.find('.openwebrx-squelch-default')[squelch_disabled ? 'addClass' : 'removeClass']('disabled');
+}
+
+DemodulatorPanel.prototype.setCenterFrequency = function(center_freq) {
+ if (this.center_freq === center_freq) {
+ return;
+ }
+ this.stopDemodulator();
+ this.center_freq = center_freq;
+ this.startDemodulator();
+};
+
+DemodulatorPanel.prototype.parseHash = function() {
+ if (!window.location.hash) {
+ return {};
+ }
+ var params = window.location.hash.substring(1).split(",").map(function(x) {
+ var harr = x.split('=');
+ return [harr[0], harr.slice(1).join('=')];
+ }).reduce(function(params, p){
+ params[p[0]] = p[1];
+ return params;
+ }, {});
+
+ return this.validateHash(params);
+};
+
+DemodulatorPanel.prototype.validateHash = function(params) {
+ var self = this;
+ params = Object.keys(params).filter(function(key) {
+ if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') {
+ return params.freq && Math.abs(params.freq - self.center_freq) < bandwidth;
+ }
+ return true;
+ }).reduce(function(p, key) {
+ p[key] = params[key];
+ return p;
+ }, {});
+
+ if (params['freq']) {
+ params['offset_frequency'] = params['freq'] - self.center_freq;
+ delete params['freq'];
+ }
+
+ return params;
+};
+
+DemodulatorPanel.prototype.updateHash = function() {
+ var demod = this.getDemodulator();
+ if (!demod) return;
+ var self = this;
+ window.location.hash = $.map({
+ freq: demod.get_offset_frequency() + self.center_freq,
+ mod: demod.get_modulation(),
+ secondary_mod: demod.get_secondary_demod(),
+ sql: demod.getSquelch(),
+ }, function(value, key){
+ if (typeof(value) === 'undefined' || value === false) return undefined;
+ return key + '=' + value;
+ }).filter(function(v) {
+ return !!v;
+ }).join(',');
+};
+
+DemodulatorPanel.prototype.updateSquelch = function() {
+ var sliderValue = parseInt(this.el.find(".openwebrx-squelch-slider").val());
+ var demod = this.getDemodulator();
+ if (demod) demod.setSquelch(sliderValue);
+};
+
+$.fn.demodulatorPanel = function(){
+ if (!this.data('panel')) {
+ this.data('panel', new DemodulatorPanel(this));
+ };
+ return this.data('panel');
+};
\ No newline at end of file
diff --git a/htdocs/lib/FrequencyDisplay.js b/htdocs/lib/FrequencyDisplay.js
index 5aab3ee..8210592 100644
--- a/htdocs/lib/FrequencyDisplay.js
+++ b/htdocs/lib/FrequencyDisplay.js
@@ -50,7 +50,6 @@ TuneableFrequencyDisplay.prototype.setupElements = function() {
TuneableFrequencyDisplay.prototype.setupEvents = function() {
var me = this;
- me.listeners = [];
me.element.on('wheel', function(e){
e.preventDefault();
@@ -63,17 +62,13 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
if (e.originalEvent.deltaY > 0) delta *= -1;
var newFrequency = me.frequency + delta;
- me.listeners.forEach(function(l) {
- l(newFrequency);
- });
+ me.element.trigger('frequencychange', newFrequency);
});
var submit = function(){
var freq = parseInt(me.input.val());
if (!isNaN(freq)) {
- me.listeners.forEach(function(l) {
- l(freq);
- });
+ me.element.trigger('frequencychange', freq);
}
me.input.hide();
me.displayContainer.show();
@@ -96,6 +91,16 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
});
};
-TuneableFrequencyDisplay.prototype.onFrequencyChange = function(listener){
- this.listeners.push(listener);
-};
+$.fn.frequencyDisplay = function() {
+ if (!this.data('frequencyDisplay')) {
+ this.data('frequencyDisplay', new FrequencyDisplay(this));
+ }
+ return this.data('frequencyDisplay');
+}
+
+$.fn.tuneableFrequencyDisplay = function() {
+ if (!this.data('frequencyDisplay')) {
+ this.data('frequencyDisplay', new TuneableFrequencyDisplay(this));
+ }
+ return this.data('frequencyDisplay');
+}
diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js
new file mode 100644
index 0000000..cced6b6
--- /dev/null
+++ b/htdocs/lib/Header.js
@@ -0,0 +1,77 @@
+function Header(el) {
+ this.el = el;
+
+ this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () {
+ toggle_panel($(this).data('toggle-panel'));
+ });
+
+ this.init_rx_photo();
+ this.download_details();
+};
+
+Header.prototype.setDetails = function(details) {
+ this.el.find('#webrx-rx-title').html(details['receiver_name']);
+ var query = encodeURIComponent(details['receiver_gps']['lat'] + ',' + details['receiver_gps']['lon']);
+ this.el.find('#webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m,
[maps]');
+ this.el.find('#webrx-rx-photo-title').html(details['photo_title']);
+ this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']);
+};
+
+Header.prototype.init_rx_photo = function() {
+ this.rx_photo_state = 0;
+
+ $.extend($.easing, {
+ easeOutCubic:function(x) {
+ return 1 - Math.pow( 1 - x, 3 );
+ }
+ });
+
+ $('#webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this));
+};
+
+Header.prototype.close_rx_photo = function() {
+ this.rx_photo_state = 0;
+ this.el.find("#webrx-rx-photo-desc").animate({opacity: 0});
+ this.el.find("#webrx-rx-photo-title").animate({opacity: 0});
+ this.el.find('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'});
+ this.el.find("#openwebrx-rx-details-arrow-down").show();
+ this.el.find("#openwebrx-rx-details-arrow-up").hide();
+}
+
+Header.prototype.open_rx_photo = function() {
+ this.rx_photo_state = 1;
+ this.el.find("#webrx-rx-photo-desc").animate({opacity: 1});
+ this.el.find("#webrx-rx-photo-title").animate({opacity: 1});
+ this.el.find('#webrx-top-photo-clip').animate({maxHeight: 350}, {duration: 1000, easing: 'easeOutCubic'});
+ this.el.find("#openwebrx-rx-details-arrow-down").hide();
+ this.el.find("#openwebrx-rx-details-arrow-up").show();
+}
+
+Header.prototype.toggle_rx_photo = function(ev) {
+ if (ev && ev.target && ev.target.tagName == 'A') {
+ return;
+ }
+ if (this.rx_photo_state) {
+ this.close_rx_photo();
+ } else {
+ this.open_rx_photo();
+ }
+};
+
+Header.prototype.download_details = function() {
+ var self = this;
+ $.ajax('api/receiverdetails').done(function(data){
+ self.setDetails(data);
+ });
+};
+
+$.fn.header = function() {
+ if (!this.data('header')) {
+ this.data('header', new Header(this));
+ }
+ return this.data('header');
+};
+
+$(function(){
+ $('#webrx-top-container').header();
+});
diff --git a/htdocs/lib/Js8Threads.js b/htdocs/lib/Js8Threads.js
new file mode 100644
index 0000000..4cecf46
--- /dev/null
+++ b/htdocs/lib/Js8Threads.js
@@ -0,0 +1,150 @@
+Js8Thread = function(el){
+ this.messages = [];
+ this.el = el;
+};
+
+Js8Thread.prototype.getAverageFrequency = function(){
+ var total = this.messages.map(function(message){
+ return message.freq;
+ }).reduce(function(t, f){
+ return t + f;
+ }, 0);
+ return total / this.messages.length;
+};
+
+Js8Thread.prototype.pushMessage = function(message) {
+ this.messages.push(message);
+ this.render();
+};
+
+Js8Thread.prototype.render = function() {
+ this.el.html(
+ '
' + this.renderTimestamp(this.getLatestTimestamp()) + ' | ' +
+ '
' + Math.round(this.getAverageFrequency()) + ' | ' +
+ '
' + this.renderMessages() + ' | '
+ );
+};
+
+Js8Thread.prototype.getLatestTimestamp = function() {
+ return this.messages[0].timestamp;
+};
+
+Js8Thread.prototype.isOpen = function() {
+ if (!this.messages.length) return true;
+ var last_message = this.messages[this.messages.length - 1];
+ return (last_message.thread_type & 2) === 0;
+};
+
+Js8Thread.prototype.renderMessages = function() {
+ var res = [];
+ for (var i = 0; i < this.messages.length; i++) {
+ var msg = this.messages[i];
+ if (msg.thread_type & 1) {
+ res.push('[ ');
+ } else if (i === 0 || msg.timestamp - this.messages[i - 1].timestamp > this.getMessageDuration()) {
+ res.push(' ... ');
+ }
+ res.push(msg.msg);
+ if (msg.thread_type & 2) {
+ res.push(' ]');
+ } else if (i === this.messages.length -1) {
+ res.push(' ... ');
+ }
+ }
+ return res.join('');
+};
+
+Js8Thread.prototype.getMessageDuration = function() {
+ switch (this.getMode()) {
+ case 'A':
+ return 15000;
+ case 'E':
+ return 30000;
+ case 'B':
+ return 10000;
+ case 'C':
+ return 6000;
+ }
+};
+
+Js8Thread.prototype.getMode = function() {
+ // we filter messages by mode, so the first one is as good as any
+ if (!this.messages.length) return;
+ return this.messages[0].mode;
+};
+
+Js8Thread.prototype.acceptsMode = function(mode) {
+ var currentMode = this.getMode();
+ return typeof(currentMode) === 'undefined' || currentMode === mode;
+};
+
+Js8Thread.prototype.renderTimestamp = function(timestamp) {
+ var t = new Date(timestamp);
+ var pad = function (i) {
+ return ('' + i).padStart(2, "0");
+ };
+ return pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds());
+};
+
+Js8Thread.prototype.purgeOldMessages = function() {
+ var now = new Date().getTime();
+ this.messages = this.messages.filter(function(m) {
+ // keep messages around for 20 minutes
+ return now - m.timestamp < 20 * 60 * 1000;
+ });
+ if (!this.messages.length) {
+ this.el.remove();
+ } else {
+ this.render();
+ }
+ return this.messages.length;
+};
+
+Js8Threader = function(el){
+ this.threads = [];
+ this.tbody = $(el).find('tbody');
+ var me = this;
+ this.interval = setInterval(function(){
+ me.purgeOldMessages();
+ }, 15000);
+};
+
+Js8Threader.prototype.purgeOldMessages = function() {
+ this.threads = this.threads.filter(function(t) {
+ return t.purgeOldMessages();
+ });
+};
+
+Js8Threader.prototype.findThread = function(freq, mode) {
+ var matching = this.threads.filter(function(thread) {
+ // max frequency deviation: 5 Hz. this may be a little tight.
+ return thread.isOpen() && thread.acceptsMode(mode) && Math.abs(thread.getAverageFrequency() - freq) <= 5;
+ });
+ matching.sort(function(a, b){
+ return b.getLatestTimestamp() - a.getLatestTimestamp();
+ });
+ return matching[0] || false;
+};
+
+Js8Threader.prototype.pushMessage = function(message) {
+ var thread;
+ // only look for exising threads if the message is not a starting message
+ if ((message.thread_type & 1) === 0) {
+ thread = this.findThread(message.freq, message.mode);
+ }
+ if (!thread) {
+ var line = $("
|
");
+ this.tbody.append(line);
+ thread = new Js8Thread(line);
+ this.threads.push(thread);
+ }
+ thread.pushMessage(message);
+ this.tbody.scrollTop(this.tbody[0].scrollHeight);
+};
+
+$.fn.js8 = function() {
+ if (!this.data('threader')) {
+ this.data('threader', new Js8Threader(this));
+ }
+ return this.data('threader');
+};
\ No newline at end of file
diff --git a/htdocs/lib/Modes.js b/htdocs/lib/Modes.js
new file mode 100644
index 0000000..c68466a
--- /dev/null
+++ b/htdocs/lib/Modes.js
@@ -0,0 +1,55 @@
+var Modes = {
+ modes: [],
+ features: {},
+ panels: [],
+ setModes:function(json){
+ this.modes = json.map(function(m){ return new Mode(m); });
+ this.updatePanels();
+ $('#openwebrx-dialog-bookmark').bookmarkDialog().setModes(this.modes);
+ },
+ getModes:function(){
+ return this.modes;
+ },
+ setFeatures:function(features){
+ this.features = features;
+ this.updatePanels();
+ },
+ findByModulation:function(modulation){
+ matches = this.modes.filter(function(m) { return m.modulation === modulation; });
+ if (matches.length) return matches[0]
+ },
+ registerModePanel: function(el) {
+ this.panels.push(el);
+ },
+ initComplete: function() {
+ return this.modes.length && Object.keys(this.features).length;
+ },
+ updatePanels: function() {
+ this.panels.forEach(function(p) {
+ p.render();
+ p.startDemodulator();
+ });
+ }
+};
+
+var Mode = function(json){
+ this.modulation = json.modulation;
+ this.name = json.name;
+ this.type = json.type;
+ this.requirements = json.requirements;
+ this.squelch = json.squelch;
+ if (json.bandpass) {
+ this.bandpass = json.bandpass;
+ }
+ if (this.type === 'digimode') {
+ this.underlying = json.underlying;
+ }
+};
+
+Mode.prototype.isAvailable = function(){
+ return this.requirements.map(function(r){
+ return Modes.features[r];
+ }).reduce(function(a, b){
+ return a && b;
+ }, true);
+};
diff --git a/htdocs/lib/ProgressBar.js b/htdocs/lib/ProgressBar.js
index 9d0736d..59d3a91 100644
--- a/htdocs/lib/ProgressBar.js
+++ b/htdocs/lib/ProgressBar.js
@@ -1,10 +1,15 @@
ProgressBar = function(el) {
this.$el = $(el);
- this.$innerText = this.$el.find('.openwebrx-progressbar-text');
- this.$innerBar = this.$el.find('.openwebrx-progressbar-bar');
+ this.$innerText = $('
' + this.getDefaultText() + '');
+ this.$innerBar = $('
');
+ this.$el.empty().append(this.$innerText, this.$innerBar);
this.$innerBar.css('width', '0%');
};
+ProgressBar.prototype.getDefaultText = function() {
+ return '';
+}
+
ProgressBar.prototype.set = function(val, text, over) {
this.setValue(val);
this.setText(text);
@@ -25,13 +30,20 @@ ProgressBar.prototype.setOver = function(over) {
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
};
-AudioBufferProgressBar = function(el, sampleRate) {
+AudioBufferProgressBar = function(el) {
ProgressBar.call(this, el);
- this.sampleRate = sampleRate;
};
AudioBufferProgressBar.prototype = new ProgressBar();
+AudioBufferProgressBar.prototype.getDefaultText = function() {
+ return 'Audio buffer [0 ms]';
+};
+
+AudioBufferProgressBar.prototype.setSampleRate = function(sampleRate) {
+ this.sampleRate = sampleRate;
+};
+
AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) {
var audio_buffer_value = buffersize / this.sampleRate;
var overrun = audio_buffer_value > audio_buffer_maximal_length_sec;
@@ -53,6 +65,10 @@ NetworkSpeedProgressBar = function(el) {
NetworkSpeedProgressBar.prototype = new ProgressBar();
+NetworkSpeedProgressBar.prototype.getDefaultText = function() {
+ return 'Network usage [0 kbps]';
+};
+
NetworkSpeedProgressBar.prototype.setSpeed = function(speed) {
var speedInKilobits = speed * 8 / 1000;
this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false);
@@ -64,18 +80,29 @@ AudioSpeedProgressBar = function(el) {
AudioSpeedProgressBar.prototype = new ProgressBar();
+AudioSpeedProgressBar.prototype.getDefaultText = function() {
+ return 'Audio stream [0 kbps]';
+};
+
AudioSpeedProgressBar.prototype.setSpeed = function(speed) {
this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false);
};
AudioOutputProgressBar = function(el, sampleRate) {
ProgressBar.call(this, el);
- this.maxRate = sampleRate * 1.25;
- this.minRate = sampleRate * .25;
};
AudioOutputProgressBar.prototype = new ProgressBar();
+AudioOutputProgressBar.prototype.getDefaultText = function() {
+ return 'Audio output [0 sps]';
+};
+
+AudioOutputProgressBar.prototype.setSampleRate = function(sampleRate) {
+ this.maxRate = sampleRate * 1.25;
+ this.minRate = sampleRate * .25;
+};
+
AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) {
this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate);
};
@@ -88,6 +115,10 @@ ClientsProgressBar = function(el) {
ClientsProgressBar.prototype = new ProgressBar();
+ClientsProgressBar.prototype.getDefaultText = function() {
+ return 'Clients [1]';
+};
+
ClientsProgressBar.prototype.setClients = function(clients) {
this.clients = clients;
this.render();
@@ -108,6 +139,27 @@ CpuProgressBar = function(el) {
CpuProgressBar.prototype = new ProgressBar();
+CpuProgressBar.prototype.getDefaultText = function() {
+ return 'Server CPU [0%]';
+};
+
CpuProgressBar.prototype.setUsage = function(usage) {
this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85);
};
+
+ProgressBar.types = {
+ cpu: CpuProgressBar,
+ audiobuffer: AudioBufferProgressBar,
+ audiospeed: AudioSpeedProgressBar,
+ audiooutput: AudioOutputProgressBar,
+ clients: ClientsProgressBar,
+ networkspeed: NetworkSpeedProgressBar
+}
+
+$.fn.progressbar = function() {
+ if (!this.data('progressbar')) {
+ var constructor = ProgressBar.types[this.data('type')] || ProgressBar;
+ this.data('progressbar', new constructor(this));
+ }
+ return this.data('progressbar');
+};
diff --git a/htdocs/login.html b/htdocs/login.html
index b86efd9..4f4c554 100644
--- a/htdocs/login.html
+++ b/htdocs/login.html
@@ -3,8 +3,10 @@
OpenWebRX Login
-
+
+
+
@@ -19,7 +21,7 @@
-
+
\ No newline at end of file
diff --git a/htdocs/map.html b/htdocs/map.html
index 23aabeb..08e40b4 100644
--- a/htdocs/map.html
+++ b/htdocs/map.html
@@ -3,9 +3,7 @@
OpenWebRX Map
-
-
-
+
diff --git a/htdocs/map.js b/htdocs/map.js
index 948e83b..0d447e2 100644
--- a/htdocs/map.js
+++ b/htdocs/map.js
@@ -135,7 +135,11 @@
if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
map.panTo(pos);
showMarkerInfoWindow(update.callsign, pos);
- delete(expectedCallsign);
+ expectedCallsign = false;
+ }
+
+ if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign.trim()) {
+ showMarkerInfoWindow(infowindow.callsign, pos);
}
break;
case 'locator':
@@ -176,7 +180,11 @@
if (expectedLocator && expectedLocator == update.location.locator) {
map.panTo(center);
showLocatorInfoWindow(expectedLocator, center);
- delete(expectedLocator);
+ expectedLocator = false;
+ }
+
+ if (infowindow && infowindow.locator && infowindow.locator == update.location.locator) {
+ showLocatorInfoWindow(infowindow.locator, center);
}
break;
}
@@ -250,6 +258,11 @@
case "update":
processUpdates(json.value);
break;
+ case 'receiver_details':
+ $('#webrx-top-container').header().setDetails(json['value']);
+ break;
+ default:
+ console.warn('received message of unknown type: ' + json['type']);
}
} catch (e) {
// don't lose exception
@@ -282,9 +295,21 @@
connect();
+ var getInfoWindow = function() {
+ if (!infowindow) {
+ infowindow = new google.maps.InfoWindow();
+ google.maps.event.addListener(infowindow, 'closeclick', function() {
+ delete infowindow.locator;
+ delete infowindow.callsign;
+ });
+ }
+ return infowindow;
+ }
+
var infowindow;
var showLocatorInfoWindow = function(locator, pos) {
- if (!infowindow) infowindow = new google.maps.InfoWindow();
+ var infowindow = getInfoWindow();
+ infowindow.locator = locator;
var inLocator = $.map(rectangles, function(r, callsign) {
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
}).filter(function(d) {
@@ -310,7 +335,8 @@
};
var showMarkerInfoWindow = function(callsign, pos) {
- if (!infowindow) infowindow = new google.maps.InfoWindow();
+ var infowindow = getInfoWindow();
+ infowindow.callsign = callsign;
var marker = markers[callsign];
var timestring = moment(marker.lastseen).fromNow();
var commentString = "";
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js
index 52770c6..dadd7e3 100644
--- a/htdocs/openwebrx.js
+++ b/htdocs/openwebrx.js
@@ -27,73 +27,15 @@ is_firefox = navigator.userAgent.indexOf("Firefox") >= 0;
var bandwidth;
var center_freq;
var fft_size;
-var fft_fps;
var fft_compression = "none";
var fft_codec;
var waterfall_setup_done = 0;
var secondary_fft_size;
-var rx_photo_state = 1;
function e(what) {
return document.getElementById(what);
}
-var rx_photo_height;
-
-function init_rx_photo() {
- var clip = e("webrx-top-photo-clip");
- rx_photo_height = clip.clientHeight;
- clip.style.maxHeight = rx_photo_height + "px";
-
- $.extend($.easing, {
- easeOutCubic:function(x) {
- return 1 - Math.pow( 1 - x, 3 );
- }
- });
-
- window.setTimeout(function () {
- $('#webrx-rx-photo-title').animate({opacity: 0}, 500);
- }, 1000);
- window.setTimeout(function () {
- $('#webrx-rx-photo-desc').animate({opacity: 0}, 500);
- }, 1500);
- window.setTimeout(function () {
- close_rx_photo()
- }, 2500);
- $('#webrx-top-container').find('.openwebrx-photo-trigger').click(toggle_rx_photo);
-}
-
-var dont_toggle_rx_photo_flag = 0;
-
-function dont_toggle_rx_photo() {
- dont_toggle_rx_photo_flag = 1;
-}
-
-function toggle_rx_photo() {
- if (dont_toggle_rx_photo_flag) {
- dont_toggle_rx_photo_flag = 0;
- return;
- }
- if (rx_photo_state) close_rx_photo();
- else open_rx_photo()
-}
-
-function close_rx_photo() {
- rx_photo_state = 0;
- $('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'});
- e("openwebrx-rx-details-arrow-down").style.display = "block";
- e("openwebrx-rx-details-arrow-up").style.display = "none";
-}
-
-function open_rx_photo() {
- rx_photo_state = 1;
- e("webrx-rx-photo-desc").style.opacity = 1;
- e("webrx-rx-photo-title").style.opacity = 1;
- $('#webrx-top-photo-clip').animate({maxHeight: rx_photo_height}, {duration: 1000, easing: 'easeOutCubic'});
- e("openwebrx-rx-details-arrow-down").style.display = "none";
- e("openwebrx-rx-details-arrow-up").style.display = "block";
-}
-
function updateVolume() {
audioEngine.setVolume(parseFloat(e("openwebrx-panel-volume").value) / 100);
}
@@ -104,14 +46,12 @@ function toggleMute() {
e("openwebrx-mute-on").id = "openwebrx-mute-off";
e("openwebrx-mute-img").src = "static/gfx/openwebrx-speaker.png";
e("openwebrx-panel-volume").disabled = false;
- e("openwebrx-panel-volume").style.opacity = 1.0;
e("openwebrx-panel-volume").value = volumeBeforeMute;
} else {
mute = true;
e("openwebrx-mute-off").id = "openwebrx-mute-on";
e("openwebrx-mute-img").src = "static/gfx/openwebrx-speaker-muted.png";
e("openwebrx-panel-volume").disabled = true;
- e("openwebrx-panel-volume").style.opacity = 0.5;
volumeBeforeMute = e("openwebrx-panel-volume").value;
e("openwebrx-panel-volume").value = 0;
}
@@ -135,16 +75,6 @@ function zoomOutTotal() {
zoom_set(0);
}
-function setSquelchToAuto() {
- e("openwebrx-panel-squelch").value = (getLogSmeterValue(smeter_level) + 10).toString();
- updateSquelch();
-}
-
-function updateSquelch() {
- var sliderValue = parseInt($("#openwebrx-panel-squelch").val());
- ws.send(JSON.stringify({"type": "dspcontrol", "params": {"squelch_level": sliderValue}}));
-}
-
var waterfall_min_level;
var waterfall_max_level;
var waterfall_min_level_default;
@@ -189,7 +119,7 @@ function setSmeterRelativeValue(value) {
}
function setSquelchSliderBackground(val) {
- var $slider = $('#openwebrx-panel-squelch');
+ var $slider = $('#openwebrx-panel-receiver .openwebrx-squelch-slider');
var min = Number($slider.attr('min'));
var max = Number($slider.attr('max'));
var sliderPosition = $slider.val();
@@ -236,344 +166,25 @@ function typeInAnimation(element, timeout, what, onFinish) {
// ================ DEMODULATOR ROUTINES ================
// ========================================================
-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).
- // Envelope is drawn on the scale canvas.
- // A "drag range" object is returned, containing information about the draggable areas of the envelope
- // (beginning, ending and the line showing the offset frequency).
- if (typeof color === "undefined") color = "#ffff00"; //yellow
- var env_bounding_line_w = 5; //
- var env_att_w = 5; // _______ ___env_h2 in px ___|_____
- var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_
- var env_h2 = 5; // |||env_att_line_w |_env_lineplus
- var env_lineplus = 1; // ||env_bounding_line_w
- var env_line_click_area = 6;
- //range=get_visible_freq_range();
- var from_px = scale_px_from_freq(from, range);
- var to_px = scale_px_from_freq(to, range);
- if (to_px < from_px) /* swap'em */ {
- var temp_px = to_px;
- to_px = from_px;
- from_px = temp_px;
- }
-
- /*from_px-=env_bounding_line_w/2;
- to_px+=env_bounding_line_w/2;*/
- from_px -= (env_att_w + env_bounding_line_w);
- to_px += (env_att_w + env_bounding_line_w);
- // do drawing:
- scale_ctx.lineWidth = 3;
- scale_ctx.strokeStyle = color;
- scale_ctx.fillStyle = color;
- var drag_ranges = {envelope_on_screen: false, line_on_screen: false};
- if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen?
- {
- drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w};
- drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px};
- drag_ranges.whole_envelope = {x1: from_px, x2: to_px};
- drag_ranges.envelope_on_screen = true;
- scale_ctx.beginPath();
- scale_ctx.moveTo(from_px, env_h1);
- scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1);
- scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2);
- scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2);
- scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1);
- scale_ctx.lineTo(to_px, env_h1);
- scale_ctx.globalAlpha = 0.3;
- scale_ctx.fill();
- scale_ctx.globalAlpha = 1;
- scale_ctx.stroke();
- }
- if (typeof line !== "undefined") // out of screen?
- {
- var line_px = scale_px_from_freq(line, range);
- if (!(line_px < 0 || line_px > window.innerWidth)) {
- drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2};
- drag_ranges.line_on_screen = true;
- scale_ctx.moveTo(line_px, env_h1 + env_lineplus);
- scale_ctx.lineTo(line_px, env_h2 - env_lineplus);
- scale_ctx.stroke();
- }
- }
- return drag_ranges;
-}
-
-function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw().
- var in_range = function (x, range) {
- return range.x1 <= x && range.x2 >= x;
- };
- var dr = Demodulator.draggable_ranges;
-
- if (key_modifiers.shiftKey) {
- //Check first: shift + center drag emulates BFO knob
- if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo;
- //Check second: shift + envelope drag emulates PBF knob
- if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs;
- }
- if (drag_ranges.envelope_on_screen) {
- // For low and high cut:
- if (in_range(x, drag_ranges.beginning)) return dr.beginning;
- if (in_range(x, drag_ranges.ending)) return dr.ending;
- // Last priority: having clicked anything else on the envelope, without holding the shift key
- if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else;
- }
- 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 () {
- };
+function getDemodulators() {
+ return [
+ $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator()
+ ].filter(function(d) {
+ return !!d;
+ });
};
-//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
- };
- }
- };
- //Subtypes only define some filter parameters and the mod string sent to server,
- //so you may set these parameters in your custom child class.
- //Why? As of demodulation is done on the server, difference is mainly on the server side.
- this.server_mod = subtype;
- if (subtype === "lsb") {
- this.low_cut = -3000;
- this.high_cut = -300;
- this.server_mod = "ssb";
- }
- else if (subtype === "usb") {
- this.low_cut = 300;
- this.high_cut = 3000;
- this.server_mod = "ssb";
- }
- else if (subtype === "cw") {
- this.low_cut = 700;
- this.high_cut = 900;
- this.server_mod = "ssb";
- }
- else if (subtype === "nfm") {
- this.low_cut = -4000;
- this.high_cut = 4000;
- }
- else if (subtype === "dmr" || subtype === "ysf") {
- this.low_cut = -4000;
- this.high_cut = 4000;
- }
- else if (subtype === "dstar" || subtype === "nxdn") {
- this.low_cut = -3250;
- this.high_cut = 3250;
- }
- else if (subtype === "am") {
- this.low_cut = -4000;
- this.high_cut = 4000;
- }
-
- 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.server_mod;
- 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
{
+ var demodulators = getDemodulators();
scale_ctx.clearRect(0, 0, scale_ctx.canvas.width, 22); //clear the upper part of the canvas (where filter envelopes reside)
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);
-}
-
-function demodulator_remove(which) {
- demodulators[which].stop();
- demodulators.splice(which, 1);
-}
-
-function demodulator_add(what) {
- demodulators.push(what);
- mkenvelopes(get_visible_freq_range());
-}
-
-var last_analog_demodulator_subtype = 'nfm';
-var last_digital_demodulator_subtype = 'bpsk31';
-
-function demodulator_analog_replace(subtype, for_digital) { //this function should only exist until the multi-demodulator capability is added
- if (!(typeof for_digital !== "undefined" && for_digital && secondary_demod)) {
- secondary_demod_close_window();
- secondary_demod_listbox_update();
+ if (demodulators.length) {
+ var bandpass = demodulators[0].getBandpass()
+ secondary_demod_waterfall_set_zoom(bandpass.low_cut, bandpass.high_cut);
}
- if (!demodulators || !demodulators[0] || demodulators[0].subtype !== subtype) {
- last_analog_demodulator_subtype = subtype;
- var temp_offset = 0;
- if (demodulators.length) {
- temp_offset = demodulators[0].offset_frequency;
- demodulator_remove(0);
- }
- demodulator_add(new Demodulator_default_analog(temp_offset, subtype));
- }
- 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() {
@@ -587,11 +198,8 @@ function waterfallWidth() {
var scale_ctx;
var scale_canvas;
-var tunedFrequencyDisplay;
-var mouseFrequencyDisplay;
function scale_setup() {
- tunedFrequencyDisplay.setFrequency(canvas_get_frequency(window.innerWidth / 2));
scale_canvas = e("openwebrx-scale-canvas");
scale_ctx = scale_canvas.getContext("2d");
scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false);
@@ -627,6 +235,7 @@ function scale_offset_freq_from_px(x, visible_range) {
function scale_canvas_mousemove(evt) {
var event_handled = false;
var i;
+ var demodulators = getDemodulators();
if (scale_canvas_drag_params.mouse_down && !scale_canvas_drag_params.drag && Math.abs(evt.pageX - scale_canvas_drag_params.start_x) > canvas_drag_min_delta)
//we can use the main drag_min_delta thing of the main canvas
{
@@ -645,7 +254,7 @@ function scale_canvas_mousemove(evt) {
function frequency_container_mousemove(evt) {
var frequency = center_freq + scale_offset_freq_from_px(evt.pageX);
- mouseFrequencyDisplay.setFrequency(frequency);
+ $('.webrx-mouse-freq').frequencyDisplay().setFrequency(frequency);
}
function scale_canvas_end_drag(x) {
@@ -653,6 +262,7 @@ function scale_canvas_end_drag(x) {
scale_canvas_drag_params.drag = false;
scale_canvas_drag_params.mouse_down = false;
var event_handled = false;
+ var demodulators = getDemodulators();
for (var i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_end();
if (!event_handled) demodulators[0].set_offset_frequency(scale_offset_freq_from_px(x));
}
@@ -922,7 +532,7 @@ function canvas_mousemove(evt) {
bookmarks.position();
}
} else {
- mouseFrequencyDisplay.setFrequency(canvas_get_frequency(relativeX));
+ $('.webrx-mouse-freq').frequencyDisplay().setFrequency(canvas_get_frequency(relativeX));
}
}
@@ -935,7 +545,7 @@ function canvas_mouseup(evt) {
var relativeX = get_relative_x(evt);
if (!canvas_drag) {
- demodulators[0].set_offset_frequency(canvas_get_freq_offset(relativeX));
+ $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().set_offset_frequency(canvas_get_freq_offset(relativeX));
}
else {
canvas_end_drag();
@@ -1018,7 +628,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 = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().get_offset_frequency();
zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack
resize_canvases(true);
mkscale();
@@ -1060,25 +670,24 @@ function on_ws_recv(evt) {
var initial_demodulator_params = {
mod: config['start_mod'],
- offset_frequency: config['start_offset_freq']
+ offset_frequency: config['start_offset_freq'],
+ 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'];
- fft_fps = config['fft_fps'];
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") + ".");
- clientProgressBar.setMaxClients(config['max_clients']);
- var sql = Number.isInteger(config['initial_squelch_level']) ? config['initial_squelch_level'] : -150;
- $("#openwebrx-panel-squelch").val(sql);
- updateSquelch();
+ $('#openwebrx-bar-clients').progressbar().setMaxClients(config['max_clients']);
waterfall_init();
- initialize_demodulator(initial_demodulator_params);
+ var demodulatorPanel = $('#openwebrx-panel-receiver').demodulatorPanel();
+ demodulatorPanel.setInitialParams(initial_demodulator_params);
+ demodulatorPanel.setCenterFrequency(center_freq);
bookmarks.loadLocalBookmarks();
waterfall_clear();
@@ -1095,22 +704,17 @@ function on_ws_recv(evt) {
secondary_demod_init_canvases();
break;
case "receiver_details":
- var r = json['value'];
- e('webrx-rx-title').innerHTML = r['receiver_name'];
- var query = encodeURIComponent(r['receiver_gps']['lat'] + ',' + r['receiver_gps']['lon']);
- e('webrx-rx-desc').innerHTML = r['receiver_location'] + ' | Loc: ' + r['locator'] + ', ASL: ' + r['receiver_asl'] + ' m,
[maps]';
- e('webrx-rx-photo-title').innerHTML = r['photo_title'];
- e('webrx-rx-photo-desc').innerHTML = r['photo_desc'];
+ $('#webrx-top-container').header().setDetails(json['value']);
break;
case "smeter":
smeter_level = json['value'];
setSmeterAbsoluteValue(smeter_level);
break;
case "cpuusage":
- cpuProgressBar.setUsage(json['value']);
+ $('#openwebrx-bar-server-cpu').progressbar().setUsage(json['value']);
break;
case "clients":
- clientProgressBar.setClients(json['value']);
+ $('#openwebrx-bar-clients').progressbar().setClients(json['value']);
break;
case "profiles":
var listbox = e("openwebrx-sdr-profiles-listbox");
@@ -1122,16 +726,14 @@ function on_ws_recv(evt) {
}
break;
case "features":
- var features = json['value'];
- for (var feature in features) {
- if (features.hasOwnProperty(feature)) {
- $('[data-feature="' + feature + '"]')[features[feature] ? "show" : "hide"]();
- }
- }
+ Modes.setFeatures(json['value']);
break;
case "metadata":
update_metadata(json['value']);
break;
+ case "js8_message":
+ $("#openwebrx-panel-js8-message").js8().pushMessage(json['value']);
+ break;
case "wsjt_message":
update_wsjt_panel(json['value']);
break;
@@ -1139,7 +741,7 @@ function on_ws_recv(evt) {
var as_bookmarks = json['value'].map(function (d) {
return {
name: d['mode'].toUpperCase(),
- digital_modulation: d['mode'],
+ modulation: d['mode'],
frequency: d['frequency']
};
});
@@ -1174,6 +776,9 @@ function on_ws_recv(evt) {
// set a higher reconnection timeout right away to avoid additional load
reconnect_timeout = 16000;
break;
+ case 'modes':
+ Modes.setModes(json['value']);
+ break;
default:
console.warn('received message of unknown type: ' + json['type']);
}
@@ -1308,14 +913,14 @@ function update_wsjt_panel(msg) {
if (['FT8', 'JT65', 'JT9', 'FT4'].indexOf(msg['mode']) >= 0) {
matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/);
if (matches && matches[2] !== 'RR73') {
- linkedmsg = html_escape(matches[1]) + '
' + matches[2] + '';
+ linkedmsg = html_escape(matches[1]) + '
' + matches[2] + '';
} else {
linkedmsg = html_escape(linkedmsg);
}
} else if (msg['mode'] === 'WSPR') {
matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/);
if (matches) {
- linkedmsg = html_escape(matches[1]) + '
' + matches[2] + '' + html_escape(matches[3]);
+ linkedmsg = html_escape(matches[1]) + '
' + matches[2] + '' + html_escape(matches[3]);
} else {
linkedmsg = html_escape(linkedmsg);
}
@@ -1401,7 +1006,7 @@ function update_packet_panel(msg) {
'style="' + stylesToString(styles) + '"'
].join(' ');
if (msg.lat && msg.lon) {
- link = '
' + overlay + '';
+ link = '
' + overlay + '';
} else {
link = '
' + overlay + '
'
}
@@ -1428,13 +1033,6 @@ function update_pocsag_panel(msg) {
$b.scrollTop($b[0].scrollHeight);
}
-function update_digitalvoice_panels(showing) {
- $(".openwebrx-meta-panel").each(function (_, p) {
- toggle_panel(p.id, p.id === showing);
- });
- clear_metadata();
-}
-
function clear_metadata() {
$(".openwebrx-meta-panel .openwebrx-meta-autoclear").text("");
$(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
@@ -1461,7 +1059,7 @@ function on_ws_opened() {
if (!networkSpeedMeasurement) {
networkSpeedMeasurement = new Measurement();
networkSpeedMeasurement.report(60000, 1000, function(rate){
- networkSpeedProgressBar.setSpeed(rate);
+ $('#openwebrx-bar-network-speed').progressbar().setSpeed(rate);
});
} else {
networkSpeedMeasurement.reset();
@@ -1471,10 +1069,6 @@ function on_ws_opened() {
"type": "connectionproperties",
"params": {"output_rate": audioEngine.getOutputRate()}
}));
- ws.send(JSON.stringify({
- "type": "dspcontrol",
- "action": "start"
- }));
}
var was_error = 0;
@@ -1498,65 +1092,11 @@ 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 {};
- }
- return window.location.hash.substring(1).split(",").map(function(x) {
- var harr = x.split('=');
- return [harr[0], harr.slice(1).join('=')];
- }).reduce(function(params, p){
- params[p[0]] = p[1];
- return params;
- }, {});
-}
-
-function validateHash() {
- var params = parseHash();
- params = Object.keys(params).filter(function(key) {
- if (key == 'freq' || key == 'mod') {
- return params.freq && Math.abs(params.freq - center_freq) < bandwidth;
- }
- return true;
- }).reduce(function(p, key) {
- p[key] = params[key];
- return p;
- }, {});
-
- if (params['freq']) {
- params['offset_frequency'] = params['freq'] - center_freq;
- delete params['freq'];
- }
-
- return params;
-}
-
-function updateHash() {
- var demod = demodulators[0];
- if (!demod) return;
- window.location.hash = $.map({
- freq: demod.get_offset_frequency() + center_freq,
- mod: demod.subtype,
- secondary_mod: secondary_demod
- }, function(value, key){
- if (!value) return undefined;
- return key + '=' + value;
- }).filter(function(v) {
- return !!v;
- }).join(',');
-}
-
function onAudioStart(success, apiType){
divlog('Web Audio API succesfully initialized, using ' + apiType + ' API, sample rate: ' + audioEngine.getSampleRate() + " Hz");
// canvas_container is set after waterfall_init() has been called. we cannot initialize before.
- if (canvas_container) initialize_demodulator();
+ //if (canvas_container) synchronize_demodulator_init();
//hide log panel in a second (if user has not hidden it yet)
window.setTimeout(function () {
@@ -1567,22 +1107,10 @@ function onAudioStart(success, apiType){
updateVolume();
}
-function initialize_demodulator(initialParams) {
- mkscale();
- var params = $.extend(initialParams || {}, validateHash());
- if (params.secondary_mod) {
- demodulator_digital_replace(params.secondary_mod);
- } else if (params.mod) {
- demodulator_analog_replace(params.mod);
- }
- if (params.offset_frequency) {
- demodulators[0].set_offset_frequency(params.offset_frequency);
- }
-}
-
var reconnect_timeout = false;
function on_ws_closed() {
+ $("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator();
if (reconnect_timeout) {
// max value: roundabout 8 and a half minutes
reconnect_timeout = Math.min(reconnect_timeout * 2, 512000);
@@ -1756,39 +1284,26 @@ function openwebrx_resize() {
resize_scale();
}
-function init_header() {
- $('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () {
- toggle_panel($(this).data('toggle-panel'));
- });
-}
-
-var audioBufferProgressBar;
-var networkSpeedProgressBar;
-var audioSpeedProgressBar;
-var audioOutputProgressBar;
-var clientProgressBar;
-var cpuProgressBar;
-
function initProgressBars() {
- audioBufferProgressBar = new AudioBufferProgressBar($('#openwebrx-bar-audio-buffer'), audioEngine.getSampleRate());
- networkSpeedProgressBar = new NetworkSpeedProgressBar($('#openwebrx-bar-network-speed'));
- audioSpeedProgressBar = new AudioSpeedProgressBar($('#openwebrx-bar-audio-speed'));
- audioOutputProgressBar = new AudioOutputProgressBar($('#openwebrx-bar-audio-output'), audioEngine.getSampleRate());
- clientProgressBar = new ClientsProgressBar($('#openwebrx-bar-clients'));
- cpuProgressBar = new CpuProgressBar($('#openwebrx-bar-server-cpu'));
+ $(".openwebrx-progressbar").each(function(){
+ var bar = $(this).progressbar();
+ if ('setSampleRate' in bar) {
+ bar.setSampleRate(audioEngine.getSampleRate());
+ }
+ })
}
function audioReporter(stats) {
if (typeof(stats.buffersize) !== 'undefined') {
- audioBufferProgressBar.setBuffersize(stats.buffersize);
+ $('#openwebrx-bar-audio-buffer').progressbar().setBuffersize(stats.buffersize);
}
if (typeof(stats.audioByteRate) !== 'undefined') {
- audioSpeedProgressBar.setSpeed(stats.audioByteRate * 8);
+ $('#openwebrx-bar-audio-speed').progressbar().setSpeed(stats.audioByteRate * 8);
}
if (typeof(stats.audioRate) !== 'undefined') {
- audioOutputProgressBar.setAudioRate(stats.audioRate);
+ $('#openwebrx-bar-audio-output').progressbar().setAudioRate(stats.audioRate);
}
}
@@ -1806,23 +1321,15 @@ function openwebrx_init() {
}
fft_codec = new ImaAdpcmCodec();
initProgressBars();
- init_rx_photo();
open_websocket();
secondary_demod_init();
digimodes_init();
initPanels();
- tunedFrequencyDisplay = new TuneableFrequencyDisplay($('#webrx-actual-freq'));
- tunedFrequencyDisplay.onFrequencyChange(function(f) {
- demodulators[0].set_offset_frequency(f - center_freq);
- });
- mouseFrequencyDisplay = new FrequencyDisplay($('#webrx-mouse-freq'));
+ $('.webrx-mouse-freq').frequencyDisplay();
+ $('#openwebrx-panel-receiver').demodulatorPanel();
window.addEventListener("resize", openwebrx_resize);
- init_header();
bookmarks = new BookmarkBar();
initSliders();
- window.addEventListener('hashchange', function() {
- initialize_demodulator();
- });
}
function initSliders() {
@@ -1853,7 +1360,7 @@ function update_dmr_timeslot_filtering() {
}).toArray().reduce(function (acc, v) {
return acc | v;
}, 0);
- webrx_set_param("dmr_filter", filter);
+ $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().setDmrFilter(filter);
}
function playButtonClick() {
@@ -1945,37 +1452,6 @@ function initPanels() {
});
}
-function demodulator_buttons_update() {
- $(".openwebrx-demodulator-button").removeClass("highlighted");
- if (!demodulators.length) return;
- if (secondary_demod) {
- $("#openwebrx-button-dig").addClass("highlighted");
- $('#openwebrx-secondary-demod-listbox').val(secondary_demod);
- } else switch (demodulators[0].subtype) {
- case "lsb":
- case "usb":
- case "cw":
- if (demodulators[0].high_cut - demodulators[0].low_cut < 300)
- $("#openwebrx-button-cw").addClass("highlighted");
- else {
- if (demodulators[0].high_cut < 0)
- $("#openwebrx-button-lsb").addClass("highlighted");
- else if (demodulators[0].low_cut > 0)
- $("#openwebrx-button-usb").addClass("highlighted");
- else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted");
- }
- break;
- default:
- var mod = demodulators[0].subtype;
- $("#openwebrx-button-" + mod).addClass("highlighted");
- break;
- }
-}
-
-function demodulator_analog_replace_last() {
- demodulator_analog_replace(last_analog_demodulator_subtype);
-}
-
/*
_____ _ _ _
| __ \(_) (_) | |
@@ -1987,7 +1463,6 @@ function demodulator_analog_replace_last() {
|___/
*/
-var secondary_demod = false;
var secondary_demod_fft_offset_db = 30; //need to calculate that later
var secondary_demod_canvases_initialized = false;
var secondary_demod_listbox_updating = false;
@@ -2004,53 +1479,6 @@ var secondary_demod_current_canvas_context;
var secondary_demod_current_canvas_index;
var secondary_demod_canvases;
-function demodulator_digital_replace_last() {
- demodulator_digital_replace(last_digital_demodulator_subtype);
- secondary_demod_listbox_update();
-}
-
-function demodulator_digital_replace(subtype) {
- if (secondary_demod === subtype) return;
- switch (subtype) {
- case "bpsk31":
- case "bpsk63":
- case "rtty":
- case "ft8":
- case "jt65":
- case "jt9":
- case "ft4":
- secondary_demod_start(subtype);
- demodulator_analog_replace('usb', true);
- break;
- case "wspr":
- secondary_demod_start(subtype);
- demodulator_analog_replace('usb', true);
- // WSPR only samples between 1400 and 1600 Hz
- demodulators[0].low_cut = 1350;
- demodulators[0].high_cut = 1650;
- demodulators[0].set();
- break;
- case "packet":
- secondary_demod_start(subtype);
- demodulator_analog_replace('nfm', true);
- break;
- case "pocsag":
- secondary_demod_start(subtype);
- demodulator_analog_replace('nfm', true);
- demodulators[0].low_cut = -6000;
- demodulators[0].high_cut = 6000;
- demodulators[0].set();
- break;
- }
- demodulator_buttons_update();
- $('#openwebrx-panel-digimodes').attr('data-mode', subtype);
- toggle_panel("openwebrx-panel-digimodes", true);
- toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(subtype) >= 0);
- toggle_panel("openwebrx-panel-packet-message", subtype === "packet");
- toggle_panel("openwebrx-panel-pocsag-message", subtype === "pocsag");
- updateHash();
-}
-
function secondary_demod_create_canvas() {
var new_canvas = document.createElement("canvas");
new_canvas.width = secondary_fft_size;
@@ -2103,17 +1531,6 @@ function secondary_demod_init() {
init_digital_removal_timer();
}
-function secondary_demod_start(subtype) {
- secondary_demod_canvases_initialized = false;
- ws.send(JSON.stringify({"type": "dspcontrol", "params": {"secondary_mod": subtype}}));
- secondary_demod = subtype;
-}
-
-function secondary_demod_stop() {
- ws.send(JSON.stringify({"type": "dspcontrol", "params": {"secondary_mod": false}}));
- secondary_demod = false;
-}
-
function secondary_demod_push_data(x) {
x = Array.from(x).filter(function (y) {
var c = y.charCodeAt(0);
@@ -2133,16 +1550,7 @@ function secondary_demod_push_data(x) {
$("#openwebrx-cursor-blink").before(x);
}
-function secondary_demod_close_window() {
- secondary_demod_stop();
- toggle_panel("openwebrx-panel-digimodes", false);
- toggle_panel("openwebrx-panel-wsjt-message", false);
- toggle_panel("openwebrx-panel-packet-message", false);
- toggle_panel("openwebrx-panel-pocsag-message", false);
-}
-
function secondary_demod_waterfall_add(data) {
- if (!secondary_demod) return;
var w = secondary_fft_size;
//Add line to waterfall image
@@ -2162,22 +1570,6 @@ function secondary_demod_waterfall_add(data) {
if (secondary_demod_current_canvas_actual_line < 0) secondary_demod_swap_canvases();
}
-function secondary_demod_listbox_changed() {
- if (secondary_demod_listbox_updating) return;
- var sdm = $("#openwebrx-secondary-demod-listbox")[0].value;
- if (sdm === "none") {
- demodulator_analog_replace_last();
- } else {
- demodulator_digital_replace(sdm);
- }
-}
-
-function secondary_demod_listbox_update() {
- secondary_demod_listbox_updating = true;
- $("#openwebrx-secondary-demod-listbox").val((secondary_demod) ? secondary_demod : "none");
- secondary_demod_listbox_updating = false;
-}
-
function secondary_demod_update_marker() {
var width = Math.max((secondary_bw / (if_samp_rate / 2)) * secondary_demod_canvas_width, 5);
var center_at = (secondary_demod_channel_freq / (if_samp_rate / 2)) * secondary_demod_canvas_width + secondary_demod_canvas_left;
@@ -2194,10 +1586,7 @@ function secondary_demod_update_channel_freq_from_event(evt) {
if (!secondary_demod_waiting_for_set) {
secondary_demod_waiting_for_set = true;
window.setTimeout(function () {
- ws.send(JSON.stringify({
- "type": "dspcontrol",
- "params": {"secondary_offset_freq": Math.floor(secondary_demod_channel_freq)}
- }));
+ $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().set_secondary_offset_freq(Math.floor(secondary_demod_channel_freq));
secondary_demod_waiting_for_set = false;
},
50
@@ -2230,7 +1619,7 @@ function secondary_demod_canvas_container_mouseup(evt) {
function secondary_demod_waterfall_set_zoom(low_cut, high_cut) {
- if (!secondary_demod || !secondary_demod_canvases_initialized) return;
+ if (!secondary_demod_canvases_initialized) return;
if (low_cut < 0 && high_cut < 0) {
var hctmp = high_cut;
var lctmp = low_cut;
diff --git a/htdocs/sdrsettings.html b/htdocs/sdrsettings.html
new file mode 100644
index 0000000..89e6a8f
--- /dev/null
+++ b/htdocs/sdrsettings.html
@@ -0,0 +1,23 @@
+
+
+
+
OpenWebRX Settings
+
+
+
+
+
+
+
+
+
+${header}
+
+
+
SDR device settings
+
+
+ ${devices}
+
+
+
\ No newline at end of file
diff --git a/htdocs/settings.html b/htdocs/settings.html
new file mode 100644
index 0000000..39cecf9
--- /dev/null
+++ b/htdocs/settings.html
@@ -0,0 +1,29 @@
+
+
+
+
OpenWebRX Settings
+
+
+
+
+
+
+
+
+
+${header}
+
+
\ No newline at end of file
diff --git a/htdocs/settings.js b/htdocs/settings.js
index 89c3eea..278e1c0 100644
--- a/htdocs/settings.js
+++ b/htdocs/settings.js
@@ -1,3 +1,299 @@
+function Input(name, value, options) {
+ this.name = name;
+ this.value = value;
+ this.options = options;
+ this.label = options && options.label || name;
+};
+
+Input.prototype.bootstrapify = function(input) {
+ input.addClass('form-control').addClass('form-control-sm');
+ return [
+ '
'
+ ].join('');
+};
+
+function TextInput() {
+ Input.apply(this, arguments);
+};
+
+TextInput.prototype = new Input();
+
+TextInput.prototype.render = function() {
+ return this.bootstrapify($('
'));
+}
+
+function NumberInput() {
+ Input.apply(this, arguments);
+};
+
+NumberInput.prototype = new Input();
+
+NumberInput.prototype.render = function() {
+ return this.bootstrapify($('
'));
+};
+
+function SoapyGainInput() {
+ Input.apply(this, arguments);
+}
+
+SoapyGainInput.prototype = new Input();
+
+SoapyGainInput.prototype.render = function(){
+ return this.bootstrapify($('
Soapy gain settings go here
'));
+};
+
+function ProfileInput() {
+ Input.apply(this, arguments);
+};
+
+ProfileInput.prototype = new Input();
+
+ProfileInput.prototype.render = function() {
+ return $('
Profiles
');
+};
+
+function SchedulerInput() {
+ Input.apply(this, arguments);
+};
+
+SchedulerInput.prototype = new Input();
+
+SchedulerInput.prototype.render = function() {
+ return $('
Scheduler
');
+};
+
+function SdrDevice(el, data) {
+ this.el = el;
+ this.data = data;
+ this.inputs = {};
+ this.render();
+
+ var self = this;
+ el.on('click', '.fieldselector .btn', function() {
+ var key = el.find('.fieldselector select').val();
+ self.data[key] = self.getInitialValue(key);
+ self.render();
+ });
+};
+
+SdrDevice.create = function(el) {
+ var data = JSON.parse(decodeURIComponent(el.data('config')));
+ var type = data.type;
+ var constructor = SdrDevice.types[type] || SdrDevice;
+ return new constructor(el, data);
+};
+
+SdrDevice.prototype.getData = function() {
+ return $.extend(new Object(), this.getDefaults(), this.data);
+};
+
+SdrDevice.prototype.getDefaults = function() {
+ var defaults = {}
+ $.each(this.getMappings(), function(k, v) {
+ if (!v.includeInDefault) return;
+ defaults[k] = 'initialValue' in v ? v['initialValue'] : false;
+ });
+ return defaults;
+};
+
+SdrDevice.prototype.getMappings = function() {
+ return {
+ "name": {
+ constructor: TextInput,
+ inputOptions: {
+ label: "Name"
+ },
+ initialValue: "",
+ includeInDefault: true
+ },
+ "type": {
+ constructor: TextInput,
+ inputOptions: {
+ label: "Type"
+ },
+ initialValue: '',
+ includeInDefault: true
+ },
+ "ppm": {
+ constructor: NumberInput,
+ inputOptions: {
+ label: "PPM"
+ },
+ initialValue: 0
+ },
+ "profiles": {
+ constructor: ProfileInput,
+ inputOptions: {
+ label: "Profiles"
+ },
+ initialValue: [],
+ includeInDefault: true,
+ position: 100
+ },
+ "scheduler": {
+ constructor: SchedulerInput,
+ inputOptions: {
+ label: "Scheduler",
+ },
+ initialValue: {},
+ position: 101
+ },
+ "rf_gain": {
+ constructor: TextInput,
+ inputOptions: {
+ label: "Gain",
+ },
+ initialValue: 0
+ }
+ };
+};
+
+SdrDevice.prototype.getMapping = function(key) {
+ var mappings = this.getMappings();
+ return mappings[key];
+};
+
+SdrDevice.prototype.getInputClass = function(key) {
+ var mapping = this.getMapping(key);
+ return mapping && mapping.constructor || TextInput;
+};
+
+SdrDevice.prototype.getInitialValue = function(key) {
+ var mapping = this.getMapping(key);
+ return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false;
+};
+
+SdrDevice.prototype.getPosition = function(key) {
+ var mapping = this.getMapping(key);
+ return mapping && mapping.position || 10;
+};
+
+SdrDevice.prototype.getInputOptions = function(key) {
+ var mapping = this.getMapping(key);
+ return mapping && mapping.inputOptions || {};
+};
+
+SdrDevice.prototype.getLabel = function(key) {
+ var options = this.getInputOptions(key);
+ return options && options.label || key;
+};
+
+SdrDevice.prototype.render = function() {
+ var self = this;
+ self.el.empty();
+ var data = this.getData();
+ Object.keys(data).sort(function(a, b){
+ return self.getPosition(a) - self.getPosition(b);
+ }).forEach(function(key){
+ var value = data[key];
+ var inputClass = self.getInputClass(key);
+ var input = new inputClass(key, value, self.getInputOptions(key));
+ self.inputs[key] = input;
+ self.el.append(input.render());
+ });
+ self.el.append(this.renderFieldSelector());
+};
+
+SdrDevice.prototype.renderFieldSelector = function() {
+ var self = this;
+ return '
' +
+ '
Add new configuration options' +
+ '' +
+ '
';
+};
+
+RtlSdrDevice = function() {
+ SdrDevice.apply(this, arguments);
+};
+
+RtlSdrDevice.prototype = Object.create(SdrDevice.prototype);
+RtlSdrDevice.prototype.constructor = RtlSdrDevice;
+
+RtlSdrDevice.prototype.getMappings = function() {
+ var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
+ return $.extend(new Object(), mappings, {
+ "device": {
+ constructor: TextInput,
+ inputOptions:{
+ label: "Serial number"
+ },
+ initialValue: ""
+ }
+ });
+};
+
+SoapySdrDevice = function() {
+ SdrDevice.apply(this, arguments);
+};
+
+SoapySdrDevice.prototype = Object.create(SdrDevice.prototype);
+SoapySdrDevice.prototype.constructor = SoapySdrDevice;
+
+SoapySdrDevice.prototype.getMappings = function() {
+ var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
+ return $.extend(new Object(), mappings, {
+ "device": {
+ constructor: TextInput,
+ inputOptions:{
+ label: "Soapy device selector"
+ },
+ initialValue: ""
+ }
+ });
+};
+
+SdrplaySdrDevice = function() {
+ SoapySdrDevice.apply(this, arguments);
+};
+
+SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
+SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice;
+
+SdrplaySdrDevice.prototype.getMappings = function() {
+ var mappings = SoapySdrDevice.prototype.getMappings.apply(this, arguments);
+ return $.extend(new Object(), mappings, {
+ "rf_gain": {
+ constructor: SoapyGainInput,
+ initialValue: 0,
+ inputOptions: {
+ label: "Gain",
+ gains: ['RFGR', 'IFGR']
+ }
+ }
+ });
+};
+
+SdrDevice.types = {
+ 'rtl_sdr': RtlSdrDevice,
+ 'sdrplay': SdrplaySdrDevice
+};
+
+$.fn.sdrdevice = function() {
+ return this.map(function(){
+ var el = $(this);
+ if (!el.data('sdrdevice')) {
+ el.data('sdrdevice', SdrDevice.create(el));
+ }
+ return el.data('sdrdevice');
+ });
+};
+
$(function(){
$(".map-input").each(function(el) {
var $el = $(this);
@@ -19,5 +315,7 @@ $(function(){
$lon.val(pos.lng);
});
});
- })
+ });
+
+ $(".sdrdevice").sdrdevice();
});
\ No newline at end of file
diff --git a/owrx/__main__.py b/owrx/__main__.py
index 8e082e9..84bca23 100644
--- a/owrx/__main__.py
+++ b/owrx/__main__.py
@@ -1,3 +1,8 @@
+import logging
+
+logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+logger = logging.getLogger(__name__)
+
from http.server import HTTPServer
from owrx.http import RequestHandler
from owrx.config import Config
@@ -10,11 +15,6 @@ from owrx.websocket import WebSocketConnection
from owrx.pskreporter import PskReporter
from owrx.version import openwebrx_version
-import logging
-
-logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
-logger = logging.getLogger(__name__)
-
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
pass
diff --git a/owrx/audio.py b/owrx/audio.py
new file mode 100644
index 0000000..a8c2880
--- /dev/null
+++ b/owrx/audio.py
@@ -0,0 +1,244 @@
+from abc import ABC, ABCMeta, abstractmethod
+from owrx.config import Config
+from owrx.metrics import Metrics, CounterMetric, DirectMetric
+import threading
+import wave
+import subprocess
+import os
+from multiprocessing.connection import Pipe, wait
+from datetime import datetime, timedelta
+from queue import Queue, Full
+
+
+import logging
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+
+class QueueJob(object):
+ def __init__(self, decoder, file, freq):
+ self.decoder = decoder
+ self.file = file
+ self.freq = freq
+
+ def run(self):
+ self.decoder.decode(self)
+
+ def unlink(self):
+ try:
+ os.unlink(self.file)
+ except FileNotFoundError:
+ pass
+
+
+class QueueWorker(threading.Thread):
+ def __init__(self, queue):
+ self.queue = queue
+ self.doRun = True
+ super().__init__(daemon=True)
+
+ def run(self) -> None:
+ while self.doRun:
+ job = self.queue.get()
+ try:
+ job.run()
+ except Exception:
+ logger.exception("failed to decode job")
+ self.queue.onError()
+ finally:
+ job.unlink()
+
+ self.queue.task_done()
+
+
+class DecoderQueue(Queue):
+ sharedInstance = None
+ creationLock = threading.Lock()
+
+ @staticmethod
+ def getSharedInstance():
+ with DecoderQueue.creationLock:
+ if DecoderQueue.sharedInstance is None:
+ pm = Config.get()
+ DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"])
+ return DecoderQueue.sharedInstance
+
+ def __init__(self, maxsize, workers):
+ super().__init__(maxsize)
+ metrics = Metrics.getSharedInstance()
+ metrics.addMetric("decoding.queue.length", DirectMetric(self.qsize))
+ self.inCounter = CounterMetric()
+ metrics.addMetric("decoding.queue.in", self.inCounter)
+ self.outCounter = CounterMetric()
+ metrics.addMetric("decoding.queue.out", self.outCounter)
+ self.overflowCounter = CounterMetric()
+ metrics.addMetric("decoding.queue.overflow", self.overflowCounter)
+ self.errorCounter = CounterMetric()
+ metrics.addMetric("decoding.queue.error", self.errorCounter)
+ self.workers = [self.newWorker() for _ in range(0, workers)]
+
+ def put(self, item, **kwars):
+ self.inCounter.inc()
+ try:
+ super(DecoderQueue, self).put(item, block=False)
+ except Full:
+ self.overflowCounter.inc()
+ raise
+
+ def get(self, **kwargs):
+ # super.get() is blocking, so it would mess up the stats to inc() first
+ out = super(DecoderQueue, self).get(**kwargs)
+ self.outCounter.inc()
+ return out
+
+ def newWorker(self):
+ worker = QueueWorker(self)
+ worker.start()
+ return worker
+
+ def onError(self):
+ self.errorCounter.inc()
+
+
+class AudioChopperProfile(ABC):
+ @abstractmethod
+ def getInterval(self):
+ pass
+
+ @abstractmethod
+ def getFileTimestampFormat(self):
+ pass
+
+ @abstractmethod
+ def decoder_commandline(self, file):
+ pass
+
+
+class AudioWriter(object):
+ def __init__(self, dsp, source, profile: AudioChopperProfile):
+ self.dsp = dsp
+ self.source = source
+ self.profile = profile
+ self.tmp_dir = Config.get()["temporary_directory"]
+ self.wavefile = None
+ self.wavefilename = None
+ self.switchingLock = threading.Lock()
+ self.timer = None
+ (self.outputReader, self.outputWriter) = Pipe()
+
+ def getWaveFile(self):
+ filename = "{tmp_dir}/openwebrx-audiochopper-{id}-{timestamp}.wav".format(
+ tmp_dir=self.tmp_dir,
+ id=id(self),
+ timestamp=datetime.utcnow().strftime(self.profile.getFileTimestampFormat()),
+ )
+ wavefile = wave.open(filename, "wb")
+ wavefile.setnchannels(1)
+ wavefile.setsampwidth(2)
+ wavefile.setframerate(12000)
+ return filename, wavefile
+
+ def getNextDecodingTime(self):
+ t = datetime.utcnow()
+ zeroed = t.replace(minute=0, second=0, microsecond=0)
+ delta = t - zeroed
+ interval = self.profile.getInterval()
+ seconds = (int(delta.total_seconds() / interval) + 1) * interval
+ t = zeroed + timedelta(seconds=seconds)
+ logger.debug("scheduling: {0}".format(t))
+ return t
+
+ def cancelTimer(self):
+ if self.timer:
+ self.timer.cancel()
+ self.timer = None
+
+ def _scheduleNextSwitch(self):
+ self.cancelTimer()
+ delta = self.getNextDecodingTime() - datetime.utcnow()
+ self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
+ self.timer.start()
+
+ def switchFiles(self):
+ self.switchingLock.acquire()
+ file = self.wavefile
+ filename = self.wavefilename
+ (self.wavefilename, self.wavefile) = self.getWaveFile()
+ self.switchingLock.release()
+
+ file.close()
+ job = QueueJob(self, filename, self.dsp.get_operating_freq())
+ try:
+ DecoderQueue.getSharedInstance().put(job)
+ except Full:
+ logger.warning("decoding queue overflow; dropping one file")
+ job.unlink()
+ self._scheduleNextSwitch()
+
+ def decode(self, job: QueueJob):
+ logger.debug("processing file %s", job.file)
+ decoder = subprocess.Popen(
+ ["nice", "-n", "10"] + self.profile.decoder_commandline(job.file),
+ stdout=subprocess.PIPE,
+ cwd=self.tmp_dir,
+ close_fds=True,
+ )
+ for line in decoder.stdout:
+ self.outputWriter.send((job.freq, line))
+ try:
+ rc = decoder.wait(timeout=10)
+ if rc != 0:
+ logger.warning("decoder return code: %i", rc)
+ except subprocess.TimeoutExpired:
+ logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
+ decoder.kill()
+
+ def start(self):
+ (self.wavefilename, self.wavefile) = self.getWaveFile()
+ self._scheduleNextSwitch()
+
+ def write(self, data):
+ self.switchingLock.acquire()
+ self.wavefile.writeframes(data)
+ self.switchingLock.release()
+
+ def stop(self):
+ self.outputReader.close()
+ self.outputWriter.close()
+ self.cancelTimer()
+ try:
+ os.unlink(self.wavefilename)
+ except Exception:
+ logger.exception("error removing undecoded file")
+
+
+class AudioChopper(threading.Thread, metaclass=ABCMeta):
+ def __init__(self, dsp, source, *profiles: AudioChopperProfile):
+ self.source = source
+ self.writers = [AudioWriter(dsp, source, p) for p in profiles]
+ self.doRun = True
+ super().__init__()
+
+ def run(self) -> None:
+ logger.debug("Audio chopper starting up")
+ for w in self.writers:
+ w.start()
+ while self.doRun:
+ data = self.source.read(256)
+ if data is None or (isinstance(data, bytes) and len(data) == 0):
+ self.doRun = False
+ else:
+ for w in self.writers:
+ w.write(data)
+
+ logger.debug("Audio chopper shutting down")
+ for w in self.writers:
+ w.stop()
+
+ def read(self):
+ try:
+ readers = wait([w.outputReader for w in self.writers])
+ return [r.recv() for r in readers]
+ except EOFError:
+ return None
diff --git a/owrx/config.py b/owrx/config.py
index 4aa1c8e..bdecd04 100644
--- a/owrx/config.py
+++ b/owrx/config.py
@@ -26,6 +26,11 @@ class ConfigMigrator(ABC):
def migrate(self, config):
pass
+ def renameKey(self, config, old, new):
+ if old in config and not new in config:
+ config[new] = config[old]
+ del config[old]
+
class ConfigMigratorVersion1(ConfigMigrator):
def migrate(self, config):
@@ -37,6 +42,9 @@ class ConfigMigratorVersion1(ConfigMigrator):
levels = config["waterfall_auto_level_margin"]
config["waterfall_auto_level_margin"] = {"min": levels[0], "max": levels[1]}
+ self.renameKey(config, "wsjt_queue_workers", "decoding_queue_workers")
+ self.renameKey(config, "wsjt_queue_length", "decoding_queue_length")
+
config["version"] = 2
return config
diff --git a/owrx/connection.py b/owrx/connection.py
index 75b89f4..e8f5172 100644
--- a/owrx/connection.py
+++ b/owrx/connection.py
@@ -1,4 +1,5 @@
from owrx.config import Config
+from owrx.details import ReceiverDetails
from owrx.dsp import DspManager
from owrx.cpu import CpuUsageThread
from owrx.sdr import SdrService
@@ -9,10 +10,12 @@ from owrx.version import openwebrx_version
from owrx.bands import Bandplan
from owrx.bookmarks import Bookmarks
from owrx.map import Map
-from owrx.locator import Locator
from owrx.property import PropertyStack
+from owrx.modes import Modes, DigitalMode
from multiprocessing import Queue
from queue import Full
+from js8py import Js8Frame
+from abc import ABC, ABCMeta, abstractmethod
import json
import threading
@@ -21,7 +24,7 @@ import logging
logger = logging.getLogger(__name__)
-class Client(object):
+class Client(ABC):
def __init__(self, conn):
self.conn = conn
self.multiprocessingPipe = Queue(100)
@@ -50,6 +53,7 @@ class Client(object):
except Full:
self.close()
+ @abstractmethod
def handleTextMessage(self, conn, message):
pass
@@ -60,7 +64,25 @@ class Client(object):
self.close()
-class OpenWebRxReceiverClient(Client):
+class OpenWebRxClient(Client, metaclass=ABCMeta):
+ def __init__(self, conn):
+ super().__init__(conn)
+
+ receiver_details = ReceiverDetails()
+
+ def send_receiver_info(*args):
+ receiver_info = receiver_details.__dict__()
+ self.write_receiver_details(receiver_info)
+
+ # TODO unsubscribe
+ receiver_details.wire(send_receiver_info)
+ send_receiver_info()
+
+ def write_receiver_details(self, details):
+ self.send({"type": "receiver_details", "value": details})
+
+
+class OpenWebRxReceiverClient(OpenWebRxClient):
config_keys = [
"waterfall_colors",
"waterfall_min_level",
@@ -68,7 +90,6 @@ class OpenWebRxReceiverClient(Client):
"waterfall_auto_level_margin",
"samp_rate",
"fft_size",
- "fft_fps",
"audio_compression",
"fft_compression",
"max_clients",
@@ -94,33 +115,16 @@ class OpenWebRxReceiverClient(Client):
self.close()
raise
- pm = Config.get()
-
self.setSdr()
- receiver_details = pm.filter(
- "receiver_name",
- "receiver_location",
- "receiver_asl",
- "receiver_gps",
- "photo_title",
- "photo_desc",
- )
-
- def send_receiver_info(*args):
- receiver_info = receiver_details.__dict__()
- receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
- self.write_receiver_details(receiver_info)
-
- # TODO unsubscribe
- receiver_details.wire(send_receiver_info)
- send_receiver_info()
-
- self.__sendProfiles()
-
features = FeatureDetector().feature_availability()
self.write_features(features)
+ modes = Modes.getModes()
+ self.write_modes(modes)
+
+ self.__sendProfiles()
+
CpuUsageThread.getSharedInstance().add_client(self)
def __sendProfiles(self):
@@ -134,14 +138,19 @@ 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":
self.startDsp()
if "params" in message:
- params = message["params"]
- self.setDspProperties(params)
+ dsp = self.getDsp()
+ if dsp is None:
+ logger.warning("DSP not available; discarding client data")
+ else:
+ params = message["params"]
+ dsp.setProperties(params)
elif message["type"] == "config":
if "params" in message:
@@ -158,7 +167,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))
@@ -175,6 +184,7 @@ class OpenWebRxReceiverClient(Client):
next = SdrService.getFirstSource()
if next is None:
# exit condition: no sdrs available
+ logger.warning("no more SDR devices available")
self.handleNoSdrsAvailable()
return
@@ -190,16 +200,17 @@ class OpenWebRxReceiverClient(Client):
self.sdr = next
- self.startDsp()
+ self.getDsp()
- # 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:
+ # found a working sdr, exit the loop
+ if self.sdr.getState() != SdrSource.STATE_FAILED:
break
+ logger.warning('SDR device "%s" has failed, selecing new device', self.sdr.getName())
+ self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
+
# send initial config
- self.setDspProperties(self.connectionProperties)
+ self.getDsp().setProperties(self.connectionProperties)
stack = PropertyStack()
stack.addLayer(0, self.sdr.getProps())
@@ -231,9 +242,7 @@ 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()
+ self.getDsp().start()
def close(self):
self.stopDsp()
@@ -254,6 +263,8 @@ class OpenWebRxReceiverClient(Client):
def setParams(self, params):
config = Config.get()
# allow direct configuration only if enabled in the config
+ if "configurable_keys" not in config:
+ return
keys = config["configurable_keys"]
if not keys:
return
@@ -263,11 +274,15 @@ class OpenWebRxReceiverClient(Client):
stack.addLayer(1, config)
protected = stack.filter(*keys)
for key, value in params.items():
- protected[key] = value
+ try:
+ protected[key] = value
+ except KeyError:
+ pass
- def setDspProperties(self, params):
- for key, value in params.items():
- self.dsp.setProperty(key, value)
+ def getDsp(self):
+ if self.dsp is None and self.sdr is not None:
+ self.dsp = DspManager(self, self.sdr)
+ return self.dsp
def write_spectrum_data(self, data):
self.mp_send(bytes([0x01]) + data)
@@ -297,9 +312,6 @@ class OpenWebRxReceiverClient(Client):
def write_config(self, cfg):
self.send({"type": "config", "value": cfg})
- def write_receiver_details(self, details):
- self.send({"type": "receiver_details", "value": details})
-
def write_profiles(self, profiles):
self.send({"type": "profiles", "value": profiles})
@@ -333,8 +345,39 @@ class OpenWebRxReceiverClient(Client):
def write_backoff_message(self, reason):
self.send({"type": "backoff", "reason": reason})
+ def write_js8_message(self, frame: Js8Frame, freq: int):
+ self.send({"type": "js8_message", "value": {
+ "msg": str(frame),
+ "timestamp": frame.timestamp,
+ "db": frame.db,
+ "dt": frame.dt,
+ "freq": freq + frame.freq,
+ "thread_type": frame.thread_type,
+ "mode": frame.mode
+ }})
-class MapConnection(Client):
+ def write_modes(self, modes):
+ def to_json(m):
+ res = {
+ "modulation": m.modulation,
+ "name": m.name,
+ "type": "digimode" if isinstance(m, DigitalMode) else "analog",
+ "requirements": m.requirements,
+ "squelch": m.squelch,
+ }
+ if m.bandpass is not None:
+ res["bandpass"] = {
+ "low_cut": m.bandpass.low_cut,
+ "high_cut": m.bandpass.high_cut
+ }
+ if isinstance(m, DigitalMode):
+ res["underlying"] = m.underlying
+ return res
+
+ self.send({"type": "modes", "value": [to_json(m) for m in modes]})
+
+
+class MapConnection(OpenWebRxClient):
def __init__(self, conn):
super().__init__(conn)
diff --git a/owrx/controllers/admin.py b/owrx/controllers/admin.py
index 6aa2a4b..3879141 100644
--- a/owrx/controllers/admin.py
+++ b/owrx/controllers/admin.py
@@ -1,6 +1,11 @@
from .template import WebpageController
from .session import SessionStorage
from owrx.config import Config
+from urllib import parse
+
+import logging
+
+logger = logging.getLogger(__name__)
class Authentication(object):
@@ -18,10 +23,11 @@ class AdminController(WebpageController):
def handle_request(self):
config = Config.get()
- if not config["webadmin_enabled"]:
+ if "webadmin_enabled" not in config or not config["webadmin_enabled"]:
self.send_response("Web Admin is disabled", code=403)
return
if self.authentication.isAuthenticated(self.request):
super().handle_request()
else:
- self.send_redirect("/login")
+ target = "/login?{0}".format(parse.urlencode({"ref": self.request.path}))
+ self.send_redirect(target)
diff --git a/owrx/controllers/api.py b/owrx/controllers/api.py
index 4e7a966..4dcde14 100644
--- a/owrx/controllers/api.py
+++ b/owrx/controllers/api.py
@@ -1,5 +1,6 @@
from . import Controller
from owrx.feature import FeatureDetector
+from owrx.details import ReceiverDetails
import json
@@ -7,3 +8,8 @@ class ApiController(Controller):
def indexAction(self):
data = json.dumps(FeatureDetector().feature_report())
self.send_response(data, content_type="application/json")
+
+ def receiverDetails(self):
+ receiver_details = ReceiverDetails()
+ data = json.dumps(receiver_details.__dict__())
+ self.send_response(data, content_type="application/json")
diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py
index 9a8ab24..59a532f 100644
--- a/owrx/controllers/assets.py
+++ b/owrx/controllers/assets.py
@@ -4,13 +4,18 @@ from datetime import datetime
import mimetypes
import os
import pkg_resources
+from abc import ABCMeta, abstractmethod
-class AssetsController(Controller):
+class AssetsController(Controller, metaclass=ABCMeta):
def getModified(self, file):
- return None
+ return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)))
def openFile(self, file):
+ return open(self.getFilePath(file), "rb")
+
+ @abstractmethod
+ def getFilePath(self, file):
pass
def serve_file(self, file, content_type=None):
@@ -41,8 +46,8 @@ class AssetsController(Controller):
class OwrxAssetsController(AssetsController):
- def openFile(self, file):
- return pkg_resources.resource_stream("htdocs", file)
+ def getFilePath(self, file):
+ return pkg_resources.resource_filename("htdocs", file)
class AprsSymbolsController(AssetsController):
@@ -57,8 +62,61 @@ class AprsSymbolsController(AssetsController):
def getFilePath(self, file):
return self.path + file
- def getModified(self, file):
- return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)))
- def openFile(self, file):
- return open(self.getFilePath(file), "rb")
+class CompiledAssetsController(Controller):
+ profiles = {
+ "receiver.js": [
+ "openwebrx.js",
+ "lib/jquery-3.2.1.min.js",
+ "lib/jquery.nanoscroller.js",
+ "lib/Header.js",
+ "lib/Demodulator.js",
+ "lib/DemodulatorPanel.js",
+ "lib/BookmarkBar.js",
+ "lib/BookmarkDialog.js",
+ "lib/AudioEngine.js",
+ "lib/ProgressBar.js",
+ "lib/Measurement.js",
+ "lib/FrequencyDisplay.js",
+ "lib/Js8Threads.js",
+ "lib/Modes.js",
+ ],
+ "map.js": [
+ "lib/jquery-3.2.1.min.js",
+ "lib/chroma.min.js",
+ "lib/Header.js",
+ "map.js",
+ ],
+ }
+
+ def indexAction(self):
+ profileName = self.request.matches.group(1)
+ if profileName not in CompiledAssetsController.profiles:
+ self.send_response("profile not found", code=404)
+ return
+
+ files = CompiledAssetsController.profiles[profileName]
+ files = [pkg_resources.resource_filename("htdocs", f) for f in files]
+
+ modified = self.getModified(files)
+
+ if modified is not None and "If-Modified-Since" in self.handler.headers:
+ client_modified = datetime.strptime(
+ self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
+ )
+ if modified <= client_modified:
+ self.send_response("", code=304)
+ return
+
+ contents = [self.getContents(f) for f in files]
+
+ (content_type, encoding) = mimetypes.MimeTypes().guess_type(profileName)
+ self.send_response("\n".join(contents), content_type=content_type, last_modified=modified, max_age=3600)
+
+ def getContents(self, file):
+ with open(file) as f:
+ return f.read()
+
+ def getModified(self, files):
+ modified = [datetime.fromtimestamp(os.path.getmtime(f)) for f in files]
+ return max(*modified)
diff --git a/owrx/controllers/session.py b/owrx/controllers/session.py
index dd6d0f9..ac38a43 100644
--- a/owrx/controllers/session.py
+++ b/owrx/controllers/session.py
@@ -46,12 +46,12 @@ class SessionController(WebpageController):
if data["user"] in userlist:
user = userlist[data["user"]]
if user.password.is_valid(data["password"]):
- # TODO pass the final destination
# TODO evaluate password force_change and redirect to password change
key = SessionStorage.getSharedInstance().startSession({"user": user.name})
cookie = SimpleCookie()
cookie["owrx-session"] = key
- self.send_redirect("/admin", cookies=cookie)
+ target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings"
+ self.send_redirect(target, cookies=cookie)
return
self.send_redirect("/login")
diff --git a/owrx/controllers/settings.py b/owrx/controllers/settings.py
index c5b6288..1eaceed 100644
--- a/owrx/controllers/settings.py
+++ b/owrx/controllers/settings.py
@@ -11,7 +11,10 @@ from owrx.form import (
DropdownInput,
Option,
ServicesCheckboxInput,
+ Js8ProfileCheckboxInput,
)
+from urllib.parse import quote
+import json
import logging
logger = logging.getLogger(__name__)
@@ -43,6 +46,41 @@ class Section(object):
class SettingsController(AdminController):
+ def indexAction(self):
+ self.serve_template("settings.html", **self.template_variables())
+
+
+class SdrSettingsController(AdminController):
+ def template_variables(self):
+ variables = super().template_variables()
+ variables["devices"] = self.render_devices()
+ return variables
+
+ def render_devices(self):
+ return "".join(self.render_device(key, value) for key, value in Config.get()["sdrs"].items())
+
+ def render_device(self, device_id, config):
+ return """
+
+ """.format(device_name=config["name"], form=self.render_form(device_id, config))
+
+ def render_form(self, device_id, config):
+ return """
+
+ """.format(device_id=device_id, formdata=quote(json.dumps(config)))
+
+ def indexAction(self):
+ self.serve_template("sdrsettings.html", **self.template_variables())
+
+
+class GeneralSettingsController(AdminController):
sections = [
Section(
"General settings",
@@ -145,14 +183,23 @@ class SettingsController(AdminController):
),
),
Section(
- "WSJT-X settings",
- NumberInput("wsjt_queue_workers", "Number of WSJT decoding workers"),
- NumberInput("wsjt_queue_length", "Maximum length of WSJT job queue"),
+ "Decoding settings",
+ NumberInput("decoding_queue_workers", "Number of decoding workers"),
+ NumberInput("decoding_queue_length", "Maximum length of decoding job queue"),
NumberInput(
"wsjt_decoding_depth",
- "WSJT decoding depth",
+ "Default WSJT decoding depth",
infotext="A higher decoding depth will allow more results, but will also consume more cpu",
),
+ NumberInput(
+ "js8_decoding_depth",
+ "Js8Call decoding depth",
+ infotext="A higher decoding depth will allow more results, but will also consume more cpu",
+ ),
+ Js8ProfileCheckboxInput(
+ "js8_enabled_profiles",
+ "Js8Call enabled modes"
+ ),
),
Section(
"Background decoding",
@@ -212,7 +259,7 @@ class SettingsController(AdminController):
]
def render_sections(self):
- sections = "".join(section.render() for section in SettingsController.sections)
+ sections = "".join(section.render() for section in GeneralSettingsController.sections)
return """