diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js
index 2a770bb..22a86f3 100644
--- a/htdocs/lib/DemodulatorPanel.js
+++ b/htdocs/lib/DemodulatorPanel.js
@@ -11,6 +11,8 @@ function DemodulatorPanel(el) {
self.getDemodulator().set_offset_frequency(freq - self.center_freq);
});
+ this.mouseFrequencyDisplay = el.find('.webrx-mouse-freq').frequencyDisplay();
+
Modes.registerModePanel(this);
el.on('click', '.openwebrx-demodulator-button', function() {
var modulation = $(this).data('modulation');
@@ -155,7 +157,7 @@ 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-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w'].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");
@@ -332,6 +334,15 @@ DemodulatorPanel.prototype.getSquelchMargin = function() {
return this.squelchMargin;
};
+DemodulatorPanel.prototype.setMouseFrequency = function(freq) {
+ this.mouseFrequencyDisplay.setFrequency(freq);
+};
+
+DemodulatorPanel.prototype.setFrequencyPrecision = function(precision) {
+ this.tuneableFrequencyDisplay.setFrequencyPrecision(precision);
+ this.mouseFrequencyDisplay.setFrequencyPrecision(precision);
+};
+
$.fn.demodulatorPanel = function(){
if (!this.data('panel')) {
this.data('panel', new DemodulatorPanel(this));
diff --git a/htdocs/lib/FrequencyDisplay.js b/htdocs/lib/FrequencyDisplay.js
index d475cf2..ae50fe7 100644
--- a/htdocs/lib/FrequencyDisplay.js
+++ b/htdocs/lib/FrequencyDisplay.js
@@ -1,6 +1,7 @@
function FrequencyDisplay(element) {
this.element = $(element);
this.digits = [];
+ this.precision = 4;
this.setupElements();
this.setFrequency(0);
}
@@ -14,7 +15,10 @@ FrequencyDisplay.prototype.setupElements = function() {
FrequencyDisplay.prototype.setFrequency = function(freq) {
this.frequency = freq;
- var formatted = (freq / 1e6).toLocaleString(undefined, {maximumFractionDigits: 4, minimumFractionDigits: 4});
+ var formatted = (freq / 1e6).toLocaleString(
+ undefined,
+ {maximumFractionDigits: this.precision, minimumFractionDigits: this.precision}
+ );
var children = this.digitContainer.children();
for (var i = 0; i < formatted.length; i++) {
if (!this.digits[i]) {
@@ -34,6 +38,12 @@ FrequencyDisplay.prototype.setFrequency = function(freq) {
}
};
+FrequencyDisplay.prototype.setFrequencyPrecision = function(precision) {
+ if (!precision) return;
+ this.precision = precision;
+ this.setFrequency(this.frequency);
+};
+
function TuneableFrequencyDisplay(element) {
FrequencyDisplay.call(this, element);
this.setupEvents();
diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js
index cced6b6..cd3f4bf 100644
--- a/htdocs/lib/Header.js
+++ b/htdocs/lib/Header.js
@@ -11,8 +11,7 @@ function Header(el) {
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-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m');
this.el.find('#webrx-rx-photo-title').html(details['photo_title']);
this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']);
};
diff --git a/htdocs/lib/Js8Threads.js b/htdocs/lib/Js8Threads.js
index 4cecf46..4fa5595 100644
--- a/htdocs/lib/Js8Threads.js
+++ b/htdocs/lib/Js8Threads.js
@@ -100,7 +100,13 @@ Js8Thread.prototype.purgeOldMessages = function() {
return this.messages.length;
};
+Js8Thread.prototype.purge = function() {
+ this.message = [];
+ this.el.remove();
+};
+
Js8Threader = function(el){
+ MessagePanel.call(this, el);
this.threads = [];
this.tbody = $(el).find('tbody');
var me = this;
@@ -109,6 +115,28 @@ Js8Threader = function(el){
}, 15000);
};
+Js8Threader.prototype = new MessagePanel();
+
+Js8Threader.prototype.render = function() {
+ $(this.el).append($(
+ '
' +
+ '' +
+ 'UTC | ' +
+ 'Freq | ' +
+ 'Message | ' +
+ '
' +
+ '' +
+ '
'
+ ));
+};
+
+Js8Threader.prototype.clearMessages = function() {
+ this.threads.forEach(function(t) {
+ t.purge();
+ });
+ this.threads = [];
+};
+
Js8Threader.prototype.purgeOldMessages = function() {
this.threads = this.threads.filter(function(t) {
return t.purgeOldMessages();
diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js
new file mode 100644
index 0000000..a4c8b40
--- /dev/null
+++ b/htdocs/lib/MessagePanel.js
@@ -0,0 +1,247 @@
+function MessagePanel(el) {
+ this.el = el;
+ this.render();
+ this.initClearButton();
+}
+
+MessagePanel.prototype.render = function() {
+};
+
+MessagePanel.prototype.pushMessage = function(message) {
+};
+
+// automatic clearing is not enabled by default. call this method from the constructor to enable
+MessagePanel.prototype.initClearTimer = function() {
+ var me = this;
+ if (me.removalInterval) clearInterval(me.removalInterval);
+ me.removalInterval = setInterval(function () {
+ me.clearMessages(1000);
+ }, 15000);
+};
+
+MessagePanel.prototype.clearMessages = function(toRemain) {
+ var $elements = $(this.el).find('tbody tr');
+ // limit to 1000 entries in the list since browsers get laggy at some point
+ var toRemove = $elements.length - toRemain;
+ if (toRemove <= 0) return;
+ $elements.slice(0, toRemove).remove();
+};
+
+MessagePanel.prototype.initClearButton = function() {
+ var me = this;
+ me.clearButton = $(
+ '
Clear
'
+ );
+ me.clearButton.css({
+ position: 'absolute',
+ top: '10px',
+ right: '10px'
+ });
+ me.clearButton.on('click', function() {
+ me.clearMessages(0);
+ });
+ $(me.el).append(me.clearButton);
+};
+
+function WsjtMessagePanel(el) {
+ MessagePanel.call(this, el);
+ this.initClearTimer();
+}
+
+WsjtMessagePanel.prototype = new MessagePanel();
+
+WsjtMessagePanel.prototype.render = function() {
+ $(this.el).append($(
+ '
' +
+ '' +
+ 'UTC | ' +
+ 'dB | ' +
+ 'DT | ' +
+ 'Freq | ' +
+ 'Message | ' +
+ '
' +
+ '' +
+ '
'
+ ));
+};
+
+WsjtMessagePanel.prototype.pushMessage = function(msg) {
+ var $b = $(this.el).find('tbody');
+ var t = new Date(msg['timestamp']);
+ var pad = function (i) {
+ return ('' + i).padStart(2, "0");
+ };
+ var linkedmsg = msg['msg'];
+ var matches;
+
+ var html_escape = function(input) {
+ return $('
').text(input).html()
+ };
+
+ if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'FST4W'].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] + '';
+ } 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]);
+ } else {
+ linkedmsg = html_escape(linkedmsg);
+ }
+ }
+ $b.append($(
+ '
' +
+ '' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + ' | ' +
+ '' + msg['db'] + ' | ' +
+ '' + msg['dt'] + ' | ' +
+ '' + msg['freq'] + ' | ' +
+ '' + linkedmsg + ' | ' +
+ '
'
+ ));
+ $b.scrollTop($b[0].scrollHeight);
+}
+
+$.fn.wsjtMessagePanel = function(){
+ if (!this.data('panel')) {
+ this.data('panel', new WsjtMessagePanel(this));
+ };
+ return this.data('panel');
+};
+
+function PacketMessagePanel(el) {
+ MessagePanel.call(this, el);
+ this.initClearTimer();
+}
+
+PacketMessagePanel.prototype = new MessagePanel();
+
+PacketMessagePanel.prototype.render = function() {
+ $(this.el).append($(
+ '
' +
+ '' +
+ 'UTC | ' +
+ 'Callsign | ' +
+ 'Coord | ' +
+ 'Comment | ' +
+ '
' +
+ '' +
+ '
'
+ ));
+};
+
+PacketMessagePanel.prototype.pushMessage = function(msg) {
+ var $b = $(this.el).find('tbody');
+ var pad = function (i) {
+ return ('' + i).padStart(2, "0");
+ };
+
+ if (msg.type && msg.type === 'thirdparty' && msg.data) {
+ msg = msg.data;
+ }
+ var source = msg.source;
+ if (msg.type) {
+ if (msg.type === 'item') {
+ source = msg.item;
+ }
+ if (msg.type === 'object') {
+ source = msg.object;
+ }
+ }
+
+ var timestamp = '';
+ if (msg.timestamp) {
+ var t = new Date(msg.timestamp);
+ timestamp = pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds())
+ }
+
+ var link = '';
+ var classes = [];
+ var styles = {};
+ var overlay = '';
+ var stylesToString = function (s) {
+ return $.map(s, function (value, key) {
+ return key + ':' + value + ';'
+ }).join('')
+ };
+ if (msg.symbol) {
+ classes.push('aprs-symbol');
+ classes.push('aprs-symboltable-' + (msg.symbol.table === '/' ? 'normal' : 'alternate'));
+ styles['background-position-x'] = -(msg.symbol.index % 16) * 15 + 'px';
+ styles['background-position-y'] = -Math.floor(msg.symbol.index / 16) * 15 + 'px';
+ if (msg.symbol.table !== '/' && msg.symbol.table !== '\\') {
+ var s = {};
+ s['background-position-x'] = -(msg.symbol.tableindex % 16) * 15 + 'px';
+ s['background-position-y'] = -Math.floor(msg.symbol.tableindex / 16) * 15 + 'px';
+ overlay = '
';
+ }
+ } else if (msg.lat && msg.lon) {
+ classes.push('openwebrx-maps-pin');
+ }
+ var attrs = [
+ 'class="' + classes.join(' ') + '"',
+ 'style="' + stylesToString(styles) + '"'
+ ].join(' ');
+ if (msg.lat && msg.lon) {
+ link = '
' + overlay + '';
+ } else {
+ link = '
' + overlay + '
'
+ }
+
+ $b.append($(
+ '
' +
+ '' + timestamp + ' | ' +
+ '' + source + ' | ' +
+ '' + link + ' | ' +
+ '' + (msg.comment || msg.message || '') + ' | ' +
+ '
'
+ ));
+ $b.scrollTop($b[0].scrollHeight);
+};
+
+$.fn.packetMessagePanel = function() {
+ if (!this.data('panel')) {
+ this.data('panel', new PacketMessagePanel(this));
+ };
+ return this.data('panel');
+};
+
+PocsagMessagePanel = function(el) {
+ MessagePanel.call(this, el);
+ this.initClearTimer();
+}
+
+PocsagMessagePanel.prototype = new MessagePanel();
+
+PocsagMessagePanel.prototype.render = function() {
+ $(this.el).append($(
+ '
' +
+ '' +
+ 'Address | ' +
+ 'Message | ' +
+ '
' +
+ '' +
+ '
'
+ ));
+};
+
+PocsagMessagePanel.prototype.pushMessage = function(msg) {
+ var $b = $(this.el).find('tbody');
+ $b.append($(
+ '
' +
+ '' + msg.address + ' | ' +
+ '' + msg.message + ' | ' +
+ '
'
+ ));
+ $b.scrollTop($b[0].scrollHeight);
+};
+
+$.fn.pocsagMessagePanel = function() {
+ if (!this.data('panel')) {
+ this.data('panel', new PocsagMessagePanel(this));
+ };
+ return this.data('panel');
+};
\ No newline at end of file
diff --git a/htdocs/map.js b/htdocs/map.js
index 69be2ca..fd3e740 100644
--- a/htdocs/map.js
+++ b/htdocs/map.js
@@ -30,6 +30,7 @@
var map;
var markers = {};
var rectangles = {};
+ var receiverMarker;
var updateQueue = [];
// reasonable default; will be overriden by server
@@ -44,7 +45,12 @@
if (!colorKeys[id]) {
var keys = Object.keys(colorKeys);
keys.push(id);
- keys.sort();
+ keys.sort(function(a, b) {
+ var pa = parseFloat(a);
+ var pb = parseFloat(b);
+ if (isNaN(pa) || isNaN(pb)) return a.localeCompare(b);
+ return pa - pb;
+ });
var colors = colorScale.colors(keys.length);
colorKeys = {};
keys.forEach(function(key, index) {
@@ -195,6 +201,7 @@
var reset = function(callsign, item) { item.setMap(); };
$.each(markers, reset);
$.each(rectangles, reset);
+ receiverMarker.setMap();
markers = {};
rectangles = {};
};
@@ -222,12 +229,13 @@
switch (json.type) {
case "config":
var config = json.value;
+ var receiverPos = {
+ lat: config.receiver_gps.lat,
+ lng: config.receiver_gps.lon
+ };
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
map = new google.maps.Map($('.openwebrx-map')[0], {
- center: {
- lat: config.receiver_gps.lat,
- lng: config.receiver_gps.lon
- },
+ center: receiverPos,
zoom: 5,
});
@@ -240,7 +248,27 @@
updateQueue = [];
});
map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($(".openwebrx-map-legend")[0]);
- });
+
+ if (!receiverMarker) {
+ receiverMarker = new google.maps.Marker();
+ receiverMarker.addListener('click', function() {
+ showReceiverInfoWindow(receiverMarker);
+ });
+ }
+ receiverMarker.setOptions({
+ map: map,
+ position: receiverPos,
+ title: config['receiver_name'],
+ config: config
+ });
+ }); else {
+ receiverMarker.setOptions({
+ map: map,
+ position: receiverPos,
+ title: config['receiver_name'],
+ config: config
+ });
+ }
retention_time = config.map_position_retention_time * 1000;
break;
case "update":
@@ -339,6 +367,15 @@
infowindow.open(map, marker);
}
+ var showReceiverInfoWindow = function(marker) {
+ var infowindow = getInfoWindow()
+ infowindow.setContent(
+ '
' + marker.config['receiver_name'] + '
' +
+ '
Receiver location
'
+ );
+ infowindow.open(map, marker);
+ }
+
var getScale = function(lastseen) {
var age = new Date().getTime() - lastseen;
var scale = 1;
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js
index af88f2f..4ca7b49 100644
--- a/htdocs/openwebrx.js
+++ b/htdocs/openwebrx.js
@@ -292,7 +292,7 @@ function scale_canvas_mousemove(evt) {
function frequency_container_mousemove(evt) {
var frequency = center_freq + scale_offset_freq_from_px(evt.pageX);
- $('.webrx-mouse-freq').frequencyDisplay().setFrequency(frequency);
+ $('#openwebrx-panel-receiver').demodulatorPanel().setMouseFrequency(frequency);
}
function scale_canvas_end_drag(x) {
@@ -570,7 +570,7 @@ function canvas_mousemove(evt) {
bookmarks.position();
}
} else {
- $('.webrx-mouse-freq').frequencyDisplay().setFrequency(canvas_get_frequency(relativeX));
+ $('#openwebrx-panel-receiver').demodulatorPanel().setMouseFrequency(canvas_get_frequency(relativeX));
}
}
@@ -734,6 +734,8 @@ function on_ws_recv(evt) {
currentprofile = config['sdr_id'] + '|' + config['profile_id'];
$('#openwebrx-sdr-profiles-listbox').val(currentprofile);
+ $('#openwebrx-panel-receiver').demodulatorPanel().setFrequencyPrecision(config['frequency_display_precision']);
+
break;
case "secondary_config":
var s = json['value'];
@@ -774,7 +776,7 @@ function on_ws_recv(evt) {
$("#openwebrx-panel-js8-message").js8().pushMessage(json['value']);
break;
case "wsjt_message":
- update_wsjt_panel(json['value']);
+ $("#openwebrx-panel-wsjt-message").wsjtMessagePanel().pushMessage(json['value']);
break;
case "dial_frequencies":
var as_bookmarks = json['value'].map(function (d) {
@@ -787,7 +789,7 @@ function on_ws_recv(evt) {
bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies');
break;
case "aprs_data":
- update_packet_panel(json['value']);
+ $('#openwebrx-panel-packet-message').packetMessagePanel().pushMessage(json['value']);
break;
case "bookmarks":
bookmarks.replace_bookmarks(json['value'], "server");
@@ -805,7 +807,7 @@ function on_ws_recv(evt) {
divlog(json['value'], true);
break;
case 'pocsag_data':
- update_pocsag_panel(json['value']);
+ $('#openwebrx-panel-pocsag-message').pocsagMessagePanel().pushMessage(json['value']);
break;
case 'backoff':
divlog("Server is currently busy: " + json['reason'], true);
@@ -941,141 +943,6 @@ function update_metadata(meta) {
}
-function html_escape(input) {
- return $('
').text(input).html()
-}
-
-function update_wsjt_panel(msg) {
- var $b = $('#openwebrx-panel-wsjt-message').find('tbody');
- var t = new Date(msg['timestamp']);
- var pad = function (i) {
- return ('' + i).padStart(2, "0");
- };
- var linkedmsg = msg['msg'];
- var matches;
- 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] + '';
- } 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]);
- } else {
- linkedmsg = html_escape(linkedmsg);
- }
- }
- $b.append($(
- '
' +
- '' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + ' | ' +
- '' + msg['db'] + ' | ' +
- '' + msg['dt'] + ' | ' +
- '' + msg['freq'] + ' | ' +
- '' + linkedmsg + ' | ' +
- '
'
- ));
- $b.scrollTop($b[0].scrollHeight);
-}
-
-var digital_removal_interval;
-
-// remove old wsjt messages in fixed intervals
-function init_digital_removal_timer() {
- if (digital_removal_interval) clearInterval(digital_removal_interval);
- digital_removal_interval = setInterval(function () {
- ['#openwebrx-panel-wsjt-message', '#openwebrx-panel-packet-message'].forEach(function (root) {
- var $elements = $(root + ' tbody tr');
- // limit to 1000 entries in the list since browsers get laggy at some point
- var toRemove = $elements.length - 1000;
- if (toRemove <= 0) return;
- $elements.slice(0, toRemove).remove();
- });
- }, 15000);
-}
-
-function update_packet_panel(msg) {
- var $b = $('#openwebrx-panel-packet-message').find('tbody');
- var pad = function (i) {
- return ('' + i).padStart(2, "0");
- };
-
- if (msg.type && msg.type === 'thirdparty' && msg.data) {
- msg = msg.data;
- }
- var source = msg.source;
- if (msg.type) {
- if (msg.type === 'item') {
- source = msg.item;
- }
- if (msg.type === 'object') {
- source = msg.object;
- }
- }
-
- var timestamp = '';
- if (msg.timestamp) {
- var t = new Date(msg.timestamp);
- timestamp = pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds())
- }
-
- var link = '';
- var classes = [];
- var styles = {};
- var overlay = '';
- var stylesToString = function (s) {
- return $.map(s, function (value, key) {
- return key + ':' + value + ';'
- }).join('')
- };
- if (msg.symbol) {
- classes.push('aprs-symbol');
- classes.push('aprs-symboltable-' + (msg.symbol.table === '/' ? 'normal' : 'alternate'));
- styles['background-position-x'] = -(msg.symbol.index % 16) * 15 + 'px';
- styles['background-position-y'] = -Math.floor(msg.symbol.index / 16) * 15 + 'px';
- if (msg.symbol.table !== '/' && msg.symbol.table !== '\\') {
- var s = {};
- s['background-position-x'] = -(msg.symbol.tableindex % 16) * 15 + 'px';
- s['background-position-y'] = -Math.floor(msg.symbol.tableindex / 16) * 15 + 'px';
- overlay = '
';
- }
- } else if (msg.lat && msg.lon) {
- classes.push('openwebrx-maps-pin');
- }
- var attrs = [
- 'class="' + classes.join(' ') + '"',
- 'style="' + stylesToString(styles) + '"'
- ].join(' ');
- if (msg.lat && msg.lon) {
- link = '
' + overlay + '';
- } else {
- link = '
' + overlay + '
'
- }
-
- $b.append($(
- '
' +
- '' + timestamp + ' | ' +
- '' + source + ' | ' +
- '' + link + ' | ' +
- '' + (msg.comment || msg.message || '') + ' | ' +
- '
'
- ));
- $b.scrollTop($b[0].scrollHeight);
-}
-
-function update_pocsag_panel(msg) {
- var $b = $('#openwebrx-panel-pocsag-message').find('tbody');
- $b.append($(
- '
' +
- '' + msg.address + ' | ' +
- '' + msg.message + ' | ' +
- '
'
- ));
- $b.scrollTop($b[0].scrollHeight);
-}
-
function clear_metadata() {
$(".openwebrx-meta-panel .openwebrx-meta-autoclear").text("");
$(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
@@ -1376,7 +1243,6 @@ function openwebrx_init() {
secondary_demod_init();
digimodes_init();
initPanels();
- $('.webrx-mouse-freq').frequencyDisplay();
$('#openwebrx-panel-receiver').demodulatorPanel();
window.addEventListener("resize", openwebrx_resize);
bookmarks = new BookmarkBar();
@@ -1588,7 +1454,10 @@ function secondary_demod_init() {
.mousedown(secondary_demod_canvas_container_mousedown)
.mouseenter(secondary_demod_canvas_container_mousein)
.mouseleave(secondary_demod_canvas_container_mouseleave);
- init_digital_removal_timer();
+ $('#openwebrx-panel-wsjt-message').wsjtMessagePanel();
+ $('#openwebrx-panel-packet-message').packetMessagePanel();
+ $('#openwebrx-panel-pocsag-message').pocsagMessagePanel();
+ $('#openwebrx-panel-js8-message').js8();
}
function secondary_demod_push_data(x) {
diff --git a/owrx/__main__.py b/owrx/__main__.py
index 452de85..a0e83dc 100644
--- a/owrx/__main__.py
+++ b/owrx/__main__.py
@@ -56,7 +56,8 @@ Support and info: https://groups.io/g/openwebrx
return
# Get error messages about unknown / unavailable features as soon as possible
- SdrService.loadProps()
+ # start up "always-on" sources right away
+ SdrService.getSources()
Services.start()
diff --git a/owrx/audio.py b/owrx/audio.py
index 89956fd..d6ba3f5 100644
--- a/owrx/audio.py
+++ b/owrx/audio.py
@@ -186,8 +186,8 @@ class AudioWriter(object):
)
try:
for line in decoder.stdout:
- self.outputWriter.send((job.freq, line))
- except OSError:
+ self.outputWriter.send((self.profile, job.freq, line))
+ except (OSError, AttributeError):
decoder.stdout.flush()
# TODO uncouple parsing from the output so that decodes can still go to the map and the spotters
logger.debug("output has gone away while decoding job.")
diff --git a/owrx/connection.py b/owrx/connection.py
index 9ff4201..e0893fc 100644
--- a/owrx/connection.py
+++ b/owrx/connection.py
@@ -124,6 +124,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
"initial_squelch_level",
"profile_id",
"squelch_auto_margin",
+ "frequency_display_precision",
]
def __init__(self, conn):
@@ -425,7 +426,12 @@ class MapConnection(OpenWebRxClient):
super().__init__(conn)
pm = Config.get()
- self.write_config(pm.filter("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__())
+ self.write_config(pm.filter(
+ "google_maps_api_key",
+ "receiver_gps",
+ "map_position_retention_time",
+ "receiver_name",
+ ).__dict__())
Map.getSharedInstance().addClient(self)
@@ -453,7 +459,7 @@ class WebSocketMessageHandler(object):
self.handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)}
conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version))
- logger.debug("client connection intitialized")
+ logger.debug("client connection initialized")
if "type" in self.handshake:
if self.handshake["type"] == "receiver":
diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py
index 771ba77..bac09e6 100644
--- a/owrx/controllers/assets.py
+++ b/owrx/controllers/assets.py
@@ -126,6 +126,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
"lib/ProgressBar.js",
"lib/Measurement.js",
"lib/FrequencyDisplay.js",
+ "lib/MessagePanel.js",
"lib/Js8Threads.js",
"lib/Modes.js",
],
diff --git a/owrx/js8.py b/owrx/js8.py
index 7d3c474..18a6cef 100644
--- a/owrx/js8.py
+++ b/owrx/js8.py
@@ -28,7 +28,7 @@ class Js8Profiles(object):
class Js8Profile(AudioChopperProfile, metaclass=ABCMeta):
- def decoding_depth(self, mode):
+ def decoding_depth(self):
pm = Config.get()
# return global default
if "js8_decoding_depth" in pm:
@@ -40,7 +40,7 @@ class Js8Profile(AudioChopperProfile, metaclass=ABCMeta):
return "%y%m%d_%H%M%S"
def decoder_commandline(self, file):
- return ["js8", "--js8", "-b", self.get_sub_mode(), "-d", str(self.decoding_depth("js8")), file]
+ return ["js8", "--js8", "-b", self.get_sub_mode(), "-d", str(self.decoding_depth()), file]
@abstractmethod
def get_sub_mode(self):
@@ -85,7 +85,7 @@ class Js8Parser(Parser):
def parse(self, messages):
for raw in messages:
try:
- freq, raw_msg = raw
+ profile, freq, raw_msg = raw
self.setDialFrequency(freq)
msg = raw_msg.decode().rstrip()
if Js8Parser.decoderRegex.match(msg):
diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py
index c3182b7..662b2a7 100644
--- a/owrx/pskreporter.py
+++ b/owrx/pskreporter.py
@@ -81,11 +81,12 @@ class PskReporter(object):
else:
self.spotCounter.inc()
self.spots.append(spot)
- self.scheduleNextUpload()
+ self.scheduleNextUpload()
def upload(self):
try:
with self.spotLock:
+ self.timer = None
spots = self.spots
self.spots = []
@@ -94,9 +95,6 @@ class PskReporter(object):
except Exception:
logger.exception("Failed to upload spots")
- self.timer = None
- self.scheduleNextUpload()
-
def cancelTimer(self):
if self.timer:
self.timer.cancel()
@@ -117,6 +115,8 @@ class Uploader(object):
def getPackets(self, spots):
encoded = [self.encodeSpot(spot) for spot in spots]
+ # filter out any erroneous encodes
+ encoded = [e for e in encoded if e is not None]
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
@@ -152,40 +152,63 @@ class Uploader(object):
return [len(s)] + list(s.encode("utf-8"))
def encodeSpot(self, spot):
- return bytes(
- self.encodeString(spot["callsign"])
- + list(int(spot["freq"]).to_bytes(4, "big"))
- + list(int(spot["db"]).to_bytes(1, "big", signed=True))
- + self.encodeString(spot["mode"])
- + self.encodeString(spot["locator"])
- # informationsource. 1 means "automatically extracted
- + [0x01]
- + list(int(spot["timestamp"] / 1000).to_bytes(4, "big"))
- )
+ try:
+ return bytes(
+ self.encodeString(spot["callsign"])
+ + list(int(spot["freq"]).to_bytes(4, "big"))
+ + list(int(spot["db"]).to_bytes(1, "big", signed=True))
+ + self.encodeString(spot["mode"])
+ + self.encodeString(spot["locator"])
+ # informationsource. 1 means "automatically extracted
+ + [0x01]
+ + list(int(spot["timestamp"] / 1000).to_bytes(4, "big"))
+ )
+ except Exception:
+ logger.exception("Error while encoding spot for pskreporter")
+ return None
def getReceiverInformationHeader(self):
+ pm = Config.get()
+ with_antenna = "pskreporter_antenna_information" in pm and pm["pskreporter_antenna_information"] is not None
+ num_fields = 4 if with_antenna else 3
+ length = 12 + num_fields * 8
return bytes(
- # id, length
- [0x00, 0x03, 0x00, 0x24]
+ # id
+ [0x00, 0x03]
+ # length
+ + list(length.to_bytes(2, 'big'))
+ Uploader.receieverDelimiter
# number of fields
- + [0x00, 0x03, 0x00, 0x00]
+ + list(num_fields.to_bytes(2, 'big'))
+ # padding
+ + [0x00, 0x00]
# receiverCallsign
+ [0x80, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# receiverLocator
+ [0x80, 0x04, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# decodingSoftware
+ [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
+ # antennaInformation
+ + (
+ [0x80, 0x09, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] if with_antenna else []
+ )
# padding
+ [0x00, 0x00]
)
def getReceiverInformation(self):
pm = Config.get()
- callsign = pm["pskreporter_callsign"]
- locator = Locator.fromCoordinates(pm["receiver_gps"])
- decodingSoftware = "OpenWebRX " + openwebrx_version
- body = [b for s in [callsign, locator, decodingSoftware] for b in self.encodeString(s)]
+ bodyFields = [
+ # callsign
+ pm["pskreporter_callsign"],
+ # locator
+ Locator.fromCoordinates(pm["receiver_gps"]),
+ # decodingSoftware
+ "OpenWebRX " + openwebrx_version,
+ ]
+ if "pskreporter_antenna_information" in pm and pm["pskreporter_antenna_information"] is not None:
+ bodyFields += [pm["pskreporter_antenna_information"]]
+ body = [b for s in bodyFields for b in self.encodeString(s)]
body = self.pad(body, 4)
body = bytes(Uploader.receieverDelimiter + list((len(body) + 4).to_bytes(2, "big")) + body)
return body
diff --git a/owrx/sdr.py b/owrx/sdr.py
index c52406f..039e914 100644
--- a/owrx/sdr.py
+++ b/owrx/sdr.py
@@ -13,7 +13,7 @@ class SdrService(object):
lastPort = None
@staticmethod
- def loadProps():
+ def _loadProps():
if SdrService.sdrProps is None:
pm = Config.get()
featureDetector = FeatureDetector()
@@ -60,7 +60,6 @@ class SdrService(object):
@staticmethod
def getSource(id):
- SdrService.loadProps()
sources = SdrService.getSources()
if not sources:
return None
@@ -70,7 +69,7 @@ class SdrService(object):
@staticmethod
def getSources():
- SdrService.loadProps()
+ SdrService._loadProps()
for id in SdrService.sdrProps.keys():
if not id in SdrService.sources:
props = SdrService.sdrProps[id]
diff --git a/owrx/wsjt.py b/owrx/wsjt.py
index cb9c15e..90c4d9c 100644
--- a/owrx/wsjt.py
+++ b/owrx/wsjt.py
@@ -14,8 +14,9 @@ logger = logging.getLogger(__name__)
class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta):
- def decoding_depth(self, mode):
+ def decoding_depth(self):
pm = Config.get()
+ mode = self.getMode().lower()
# mode-specific setting?
if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]:
return pm["wsjt_decoding_depths"][mode]
@@ -25,64 +26,76 @@ class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta):
# default when no setting is provided
return 3
+ def getTimestampFormat(self):
+ if self.getInterval() < 60:
+ return "%H%M%S"
+ return "%H%M"
+
+ def getFileTimestampFormat(self):
+ return "%y%m%d_" + self.getTimestampFormat()
+
+ @abstractmethod
+ def getMode(self):
+ pass
+
class Ft8Profile(WsjtProfile):
def getInterval(self):
return 15
- def getFileTimestampFormat(self):
- return "%y%m%d_%H%M%S"
-
def decoder_commandline(self, file):
- return ["jt9", "--ft8", "-d", str(self.decoding_depth("ft8")), file]
+ return ["jt9", "--ft8", "-d", str(self.decoding_depth()), file]
+
+ def getMode(self):
+ return "FT8"
class WsprProfile(WsjtProfile):
def getInterval(self):
return 120
- def getFileTimestampFormat(self):
- return "%y%m%d_%H%M"
-
def decoder_commandline(self, file):
cmd = ["wsprd"]
- if self.decoding_depth("wspr") > 1:
+ if self.decoding_depth() > 1:
cmd += ["-d"]
cmd += [file]
return cmd
+ def getMode(self):
+ return "WSPR"
+
class Jt65Profile(WsjtProfile):
def getInterval(self):
return 60
- def getFileTimestampFormat(self):
- return "%y%m%d_%H%M"
-
def decoder_commandline(self, file):
- return ["jt9", "--jt65", "-d", str(self.decoding_depth("jt65")), file]
+ return ["jt9", "--jt65", "-d", str(self.decoding_depth()), file]
+
+ def getMode(self):
+ return "JT65"
class Jt9Profile(WsjtProfile):
def getInterval(self):
return 60
- def getFileTimestampFormat(self):
- return "%y%m%d_%H%M"
-
def decoder_commandline(self, file):
- return ["jt9", "--jt9", "-d", str(self.decoding_depth("jt9")), file]
+ return ["jt9", "--jt9", "-d", str(self.decoding_depth()), file]
+
+ def getMode(self):
+ return "JT9"
class Ft4Profile(WsjtProfile):
def getInterval(self):
return 7.5
- def getFileTimestampFormat(self):
- return "%y%m%d_%H%M%S"
-
def decoder_commandline(self, file):
- return ["jt9", "--ft4", "-d", str(self.decoding_depth("ft4")), file]
+ return ["jt9", "--ft4", "-d", str(self.decoding_depth()), file]
+
+ def getMode(self):
+ return "FT4"
class Fst4Profile(WsjtProfile):
@@ -94,13 +107,11 @@ class Fst4Profile(WsjtProfile):
def getInterval(self):
return self.interval
- def getFileTimestampFormat(self):
- if self.interval < 60:
- return "%y%m%d_%H%M%S"
- return "%y%m%d_%H%M"
-
def decoder_commandline(self, file):
- return ["jt9", "--fst4", "-p", str(self.interval), "-d", str(self.decoding_depth("fst4")), file]
+ return ["jt9", "--fst4", "-p", str(self.interval), "-d", str(self.decoding_depth()), file]
+
+ def getMode(self):
+ return "FST4"
@staticmethod
def getEnabledProfiles():
@@ -118,13 +129,11 @@ class Fst4wProfile(WsjtProfile):
def getInterval(self):
return self.interval
- def getFileTimestampFormat(self):
- if self.interval < 60:
- return "%y%m%d_%H%M%S"
- return "%y%m%d_%H%M"
-
def decoder_commandline(self, file):
- return ["jt9", "--fst4w", "-p", str(self.interval), "-d", str(self.decoding_depth("fst4w")), file]
+ return ["jt9", "--fst4w", "-p", str(self.interval), "-d", str(self.decoding_depth()), file]
+
+ def getMode(self):
+ return "FST4W"
@staticmethod
def getEnabledProfiles():
@@ -134,12 +143,10 @@ class Fst4wProfile(WsjtProfile):
class WsjtParser(Parser):
- modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4", "`": "FST4"}
-
def parse(self, messages):
for data in messages:
try:
- freq, raw_msg = data
+ profile, freq, raw_msg = data
self.setDialFrequency(freq)
msg = raw_msg.decode().rstrip()
# known debug messages we know to skip
@@ -148,19 +155,20 @@ class WsjtParser(Parser):
if msg.startswith(" EOF on input file"):
return
- modes = list(WsjtParser.modes.keys())
- if msg[21] in modes or msg[19] in modes:
- decoder = Jt9Decoder()
+ mode = profile.getMode()
+ if mode == "WSPR":
+ decoder = WsprDecoder(profile)
else:
- decoder = WsprDecoder()
+ decoder = Jt9Decoder(profile)
out = decoder.parse(msg, freq)
- if "mode" in out:
- self.pushDecode(out["mode"])
- if "callsign" in out and "locator" in out:
- Map.getSharedInstance().updateLocation(
- out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band
- )
- PskReporter.getSharedInstance().spot(out)
+ out["mode"] = mode
+
+ self.pushDecode(mode)
+ if "callsign" in out and "locator" in out:
+ Map.getSharedInstance().updateLocation(
+ out["callsign"], LocatorLocation(out["locator"]), mode, self.band
+ )
+ PskReporter.getSharedInstance().spot(out)
self.handler.write_wsjt_message(out)
except (ValueError, IndexError):
@@ -187,13 +195,21 @@ class WsjtParser(Parser):
class Decoder(ABC):
- locator_pattern = re.compile(".*\\s([A-Z0-9]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$")
+ locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$")
- def parse_timestamp(self, instring, dateformat):
- ts = datetime.strptime(instring, dateformat)
- return int(
- datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000
- )
+ def __init__(self, profile):
+ self.profile = profile
+
+ def parse_timestamp(self, instring):
+ dateformat = self.profile.getTimestampFormat()
+ remain = instring[len(dateformat) + 1:]
+ try:
+ ts = datetime.strptime(instring[0:len(dateformat)], dateformat)
+ return remain, int(
+ datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000
+ )
+ except ValueError:
+ return remain, None
@abstractmethod
def parse(self, msg, dial_freq):
@@ -219,18 +235,7 @@ class Jt9Decoder(Decoder):
# '0003 -4 0.4 1762 # CQ R2ABM KO85'
# fst4 sample
# '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV'
- modes = list(WsjtParser.modes.keys())
- if msg[19] in modes:
- dateformat = "%H%M"
- else:
- dateformat = "%H%M%S"
- try:
- timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat)
- except ValueError:
- timestamp = None
- msg = msg[len(dateformat) + 1:]
- modeChar = msg[14:15]
- mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown"
+ msg, timestamp = self.parse_timestamp(msg)
wsjt_msg = msg[17:53].strip()
result = {
@@ -238,7 +243,6 @@ class Jt9Decoder(Decoder):
"db": float(msg[0:3]),
"dt": float(msg[4:8]),
"freq": dial_freq + int(msg[9:13]),
- "mode": mode,
"msg": wsjt_msg,
}
result.update(self.parseMessage(wsjt_msg))
@@ -246,20 +250,20 @@ class Jt9Decoder(Decoder):
class WsprDecoder(Decoder):
- wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")
+ wspr_splitter_pattern = re.compile("([A-Z0-9/]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")
def parse(self, msg, dial_freq):
# wspr sample
# '2600 -24 0.4 0.001492 -1 G8AXA JO01 33'
# '0052 -29 2.6 0.001486 0 G02CWT IO92 23'
- wsjt_msg = msg[29:].strip()
+ msg, timestamp = self.parse_timestamp(msg)
+ wsjt_msg = msg[24:].strip()
result = {
- "timestamp": self.parse_timestamp(msg[0:4], "%H%M"),
- "db": float(msg[5:8]),
- "dt": float(msg[9:13]),
- "freq": dial_freq + int(float(msg[14:24]) * 1e6),
- "drift": int(msg[25:28]),
- "mode": "WSPR",
+ "timestamp": timestamp,
+ "db": float(msg[0:3]),
+ "dt": float(msg[4:8]),
+ "freq": dial_freq + int(float(msg[10:20]) * 1e6),
+ "drift": int(msg[20:23]),
"msg": wsjt_msg,
}
result.update(self.parseMessage(wsjt_msg))