Merge branch 'develop' into m17

This commit is contained in:
Jakob Ketterl 2020-12-21 17:04:09 +01:00
commit 06f3499b6d
19 changed files with 514 additions and 296 deletions

View File

@ -269,6 +269,10 @@ waterfall_auto_level_margin = {"min": 3, "max": 10, "min_range": 50}
# \_waterfall_auto_level_margin["min"]_/ |__ current_min_power_level | \_waterfall_auto_level_margin["max"]_/ # \_waterfall_auto_level_margin["min"]_/ |__ current_min_power_level | \_waterfall_auto_level_margin["max"]_/
# current_max_power_level __| # current_max_power_level __|
# This setting allows you to modify the precision of the frequency displays in OpenWebRX.
# Set this to the number of digits you would like to see:
frequency_display_precision = 4
# This setting tells the auto-squelch the offset to add to the current signal level to use as the new squelch level. # This setting tells the auto-squelch the offset to add to the current signal level to use as the new squelch level.
# Lowering this setting will give you a more sensitive squelch, but it may also cause unwanted squelch openings when # Lowering this setting will give you a more sensitive squelch, but it may also cause unwanted squelch openings when
# using the auto squelch. # using the auto squelch.
@ -341,6 +345,8 @@ aprs_symbols_path = "/usr/share/aprs-symbols/png"
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator # this also uses the receiver_gps setting from above, so make sure it contains a correct locator
pskreporter_enabled = False pskreporter_enabled = False
pskreporter_callsign = "N0CALL" pskreporter_callsign = "N0CALL"
# optional antenna information, uncomment to enable
#pskreporter_antenna_information = "Dipole"
# === Web admin settings === # === Web admin settings ===
# this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote # this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote

2
debian/postinst vendored
View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
adduser --system --group --no-create-home --home /nonexistant openwebrx adduser --system --group --no-create-home --home /nonexistent --quiet openwebrx
usermod -aG plugdev openwebrx usermod -aG plugdev openwebrx
#DEBHELPER# #DEBHELPER#

View File

@ -981,6 +981,7 @@ img.openwebrx-mirror-img
.openwebrx-message-panel { .openwebrx-message-panel {
height: 180px; height: 180px;
position: relative;
} }
.openwebrx-message-panel tbody { .openwebrx-message-panel tbody {
@ -1146,6 +1147,8 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
@ -1153,7 +1156,9 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel
{ {
display: none; display: none;
} }
@ -1165,7 +1170,9 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container
{ {
height: 200px; height: 200px;
margin: -10px; margin: -10px;

View File

@ -60,40 +60,10 @@
</div> </div>
</div> </div>
</div> </div>
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message"> <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message"></div>
<thead><tr> <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message"></div>
<th>UTC</th> <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"></div>
<th class="decimal">dB</th> <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"></div>
<th class="decimal">DT</th>
<th class="decimal freq">Freq</th>
<th class="message">Message</th>
</tr></thead>
<tbody></tbody>
</table>
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message">
<thead><tr>
<th>UTC</th>
<th class="decimal freq">Freq</th>
<th class="message">Message</th>
</tr></thead>
<tbody></tbody>
</table>
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message">
<thead><tr>
<th>UTC</th>
<th class="callsign">Callsign</th>
<th class="coord">Coord</th>
<th class="message">Comment</th>
</tr></thead>
<tbody></tbody>
</table>
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message">
<thead><tr>
<th class="address">Address</th>
<th class="message">Message</th>
</tr></thead>
<tbody></tbody>
</table>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" style="display: none;" data-panel-name="metadata-ysf"> <div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" style="display: none;" data-panel-name="metadata-ysf">
<div class="openwebrx-meta-frame"> <div class="openwebrx-meta-frame">
<div class="openwebrx-meta-slot"> <div class="openwebrx-meta-slot">

View File

@ -11,6 +11,8 @@ function DemodulatorPanel(el) {
self.getDemodulator().set_offset_frequency(freq - self.center_freq); self.getDemodulator().set_offset_frequency(freq - self.center_freq);
}); });
this.mouseFrequencyDisplay = el.find('.webrx-mouse-freq').frequencyDisplay();
Modes.registerModePanel(this); Modes.registerModePanel(this);
el.on('click', '.openwebrx-demodulator-button', function() { el.on('click', '.openwebrx-demodulator-button', function() {
var modulation = $(this).data('modulation'); var modulation = $(this).data('modulation');
@ -155,7 +157,7 @@ DemodulatorPanel.prototype.updatePanels = function() {
var modulation = this.getDemodulator().get_secondary_demod(); var modulation = this.getDemodulator().get_secondary_demod();
$('#openwebrx-panel-digimodes').attr('data-mode', modulation); $('#openwebrx-panel-digimodes').attr('data-mode', modulation);
toggle_panel("openwebrx-panel-digimodes", !!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-js8-message", modulation == "js8");
toggle_panel("openwebrx-panel-packet-message", modulation === "packet"); toggle_panel("openwebrx-panel-packet-message", modulation === "packet");
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag"); toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");
@ -332,6 +334,15 @@ DemodulatorPanel.prototype.getSquelchMargin = function() {
return this.squelchMargin; 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(){ $.fn.demodulatorPanel = function(){
if (!this.data('panel')) { if (!this.data('panel')) {
this.data('panel', new DemodulatorPanel(this)); this.data('panel', new DemodulatorPanel(this));

View File

@ -1,6 +1,7 @@
function FrequencyDisplay(element) { function FrequencyDisplay(element) {
this.element = $(element); this.element = $(element);
this.digits = []; this.digits = [];
this.precision = 4;
this.setupElements(); this.setupElements();
this.setFrequency(0); this.setFrequency(0);
} }
@ -14,7 +15,10 @@ FrequencyDisplay.prototype.setupElements = function() {
FrequencyDisplay.prototype.setFrequency = function(freq) { FrequencyDisplay.prototype.setFrequency = function(freq) {
this.frequency = 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(); var children = this.digitContainer.children();
for (var i = 0; i < formatted.length; i++) { for (var i = 0; i < formatted.length; i++) {
if (!this.digits[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) { function TuneableFrequencyDisplay(element) {
FrequencyDisplay.call(this, element); FrequencyDisplay.call(this, element);
this.setupEvents(); this.setupEvents();

View File

@ -11,8 +11,7 @@ function Header(el) {
Header.prototype.setDetails = function(details) { Header.prototype.setDetails = function(details) {
this.el.find('#webrx-rx-title').html(details['receiver_name']); 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');
this.el.find('#webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m, <a href="https://www.google.com/maps/search/?api=1&query=' + query + '" target="_blank">[maps]</a>');
this.el.find('#webrx-rx-photo-title').html(details['photo_title']); this.el.find('#webrx-rx-photo-title').html(details['photo_title']);
this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']); this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']);
}; };

View File

@ -100,7 +100,13 @@ Js8Thread.prototype.purgeOldMessages = function() {
return this.messages.length; return this.messages.length;
}; };
Js8Thread.prototype.purge = function() {
this.message = [];
this.el.remove();
};
Js8Threader = function(el){ Js8Threader = function(el){
MessagePanel.call(this, el);
this.threads = []; this.threads = [];
this.tbody = $(el).find('tbody'); this.tbody = $(el).find('tbody');
var me = this; var me = this;
@ -109,6 +115,28 @@ Js8Threader = function(el){
}, 15000); }, 15000);
}; };
Js8Threader.prototype = new MessagePanel();
Js8Threader.prototype.render = function() {
$(this.el).append($(
'<table>' +
'<thead><tr>' +
'<th>UTC</th>' +
'<th class="decimal freq">Freq</th>' +
'<th class="message">Message</th>' +
'</tr></thead>' +
'<tbody></tbody>' +
'</table>'
));
};
Js8Threader.prototype.clearMessages = function() {
this.threads.forEach(function(t) {
t.purge();
});
this.threads = [];
};
Js8Threader.prototype.purgeOldMessages = function() { Js8Threader.prototype.purgeOldMessages = function() {
this.threads = this.threads.filter(function(t) { this.threads = this.threads.filter(function(t) {
return t.purgeOldMessages(); return t.purgeOldMessages();

247
htdocs/lib/MessagePanel.js Normal file
View File

@ -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 = $(
'<div class="openwebrx-button">Clear</div>'
);
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($(
'<table>' +
'<thead><tr>' +
'<th>UTC</th>' +
'<th class="decimal">dB</th>' +
'<th class="decimal">DT</th>' +
'<th class="decimal freq">Freq</th>' +
'<th class="message">Message</th>' +
'</tr></thead>' +
'<tbody></tbody>' +
'</table>'
));
};
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 $('<div/>').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]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
} 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]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + html_escape(matches[3]);
} else {
linkedmsg = html_escape(linkedmsg);
}
}
$b.append($(
'<tr data-timestamp="' + msg['timestamp'] + '">' +
'<td>' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '</td>' +
'<td class="decimal">' + msg['db'] + '</td>' +
'<td class="decimal">' + msg['dt'] + '</td>' +
'<td class="decimal freq">' + msg['freq'] + '</td>' +
'<td class="message">' + linkedmsg + '</td>' +
'</tr>'
));
$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($(
'<table>' +
'<thead><tr>' +
'<th>UTC</th>' +
'<th class="callsign">Callsign</th>' +
'<th class="coord">Coord</th>' +
'<th class="message">Comment</th>' +
'</tr></thead>' +
'<tbody></tbody>' +
'</table>'
));
};
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 = '<div class="aprs-symbol aprs-symboltable-overlay" style="' + stylesToString(s) + '"></div>';
}
} 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 = '<a ' + attrs + ' href="map?callsign=' + source + '" target="openwebrx-map">' + overlay + '</a>';
} else {
link = '<div ' + attrs + '>' + overlay + '</div>'
}
$b.append($(
'<tr>' +
'<td>' + timestamp + '</td>' +
'<td class="callsign">' + source + '</td>' +
'<td class="coord">' + link + '</td>' +
'<td class="message">' + (msg.comment || msg.message || '') + '</td>' +
'</tr>'
));
$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($(
'<table>' +
'<thead><tr>' +
'<th class="address">Address</th>' +
'<th class="message">Message</th>' +
'</tr></thead>' +
'<tbody></tbody>' +
'</table>'
));
};
PocsagMessagePanel.prototype.pushMessage = function(msg) {
var $b = $(this.el).find('tbody');
$b.append($(
'<tr>' +
'<td class="address">' + msg.address + '</td>' +
'<td class="message">' + msg.message + '</td>' +
'</tr>'
));
$b.scrollTop($b[0].scrollHeight);
};
$.fn.pocsagMessagePanel = function() {
if (!this.data('panel')) {
this.data('panel', new PocsagMessagePanel(this));
};
return this.data('panel');
};

View File

@ -30,6 +30,7 @@
var map; var map;
var markers = {}; var markers = {};
var rectangles = {}; var rectangles = {};
var receiverMarker;
var updateQueue = []; var updateQueue = [];
// reasonable default; will be overriden by server // reasonable default; will be overriden by server
@ -44,7 +45,12 @@
if (!colorKeys[id]) { if (!colorKeys[id]) {
var keys = Object.keys(colorKeys); var keys = Object.keys(colorKeys);
keys.push(id); 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); var colors = colorScale.colors(keys.length);
colorKeys = {}; colorKeys = {};
keys.forEach(function(key, index) { keys.forEach(function(key, index) {
@ -195,6 +201,7 @@
var reset = function(callsign, item) { item.setMap(); }; var reset = function(callsign, item) { item.setMap(); };
$.each(markers, reset); $.each(markers, reset);
$.each(rectangles, reset); $.each(rectangles, reset);
receiverMarker.setMap();
markers = {}; markers = {};
rectangles = {}; rectangles = {};
}; };
@ -222,12 +229,13 @@
switch (json.type) { switch (json.type) {
case "config": case "config":
var config = json.value; 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(){ 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], { map = new google.maps.Map($('.openwebrx-map')[0], {
center: { center: receiverPos,
lat: config.receiver_gps.lat,
lng: config.receiver_gps.lon
},
zoom: 5, zoom: 5,
}); });
@ -240,7 +248,27 @@
updateQueue = []; updateQueue = [];
}); });
map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($(".openwebrx-map-legend")[0]); 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; retention_time = config.map_position_retention_time * 1000;
break; break;
case "update": case "update":
@ -339,6 +367,15 @@
infowindow.open(map, marker); infowindow.open(map, marker);
} }
var showReceiverInfoWindow = function(marker) {
var infowindow = getInfoWindow()
infowindow.setContent(
'<h3>' + marker.config['receiver_name'] + '</h3>' +
'<div>Receiver location</div>'
);
infowindow.open(map, marker);
}
var getScale = function(lastseen) { var getScale = function(lastseen) {
var age = new Date().getTime() - lastseen; var age = new Date().getTime() - lastseen;
var scale = 1; var scale = 1;

View File

@ -292,7 +292,7 @@ function scale_canvas_mousemove(evt) {
function frequency_container_mousemove(evt) { function frequency_container_mousemove(evt) {
var frequency = center_freq + scale_offset_freq_from_px(evt.pageX); 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) { function scale_canvas_end_drag(x) {
@ -570,7 +570,7 @@ function canvas_mousemove(evt) {
bookmarks.position(); bookmarks.position();
} }
} else { } 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']; currentprofile = config['sdr_id'] + '|' + config['profile_id'];
$('#openwebrx-sdr-profiles-listbox').val(currentprofile); $('#openwebrx-sdr-profiles-listbox').val(currentprofile);
$('#openwebrx-panel-receiver').demodulatorPanel().setFrequencyPrecision(config['frequency_display_precision']);
break; break;
case "secondary_config": case "secondary_config":
var s = json['value']; var s = json['value'];
@ -774,7 +776,7 @@ function on_ws_recv(evt) {
$("#openwebrx-panel-js8-message").js8().pushMessage(json['value']); $("#openwebrx-panel-js8-message").js8().pushMessage(json['value']);
break; break;
case "wsjt_message": case "wsjt_message":
update_wsjt_panel(json['value']); $("#openwebrx-panel-wsjt-message").wsjtMessagePanel().pushMessage(json['value']);
break; break;
case "dial_frequencies": case "dial_frequencies":
var as_bookmarks = json['value'].map(function (d) { var as_bookmarks = json['value'].map(function (d) {
@ -787,7 +789,7 @@ function on_ws_recv(evt) {
bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies'); bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies');
break; break;
case "aprs_data": case "aprs_data":
update_packet_panel(json['value']); $('#openwebrx-panel-packet-message').packetMessagePanel().pushMessage(json['value']);
break; break;
case "bookmarks": case "bookmarks":
bookmarks.replace_bookmarks(json['value'], "server"); bookmarks.replace_bookmarks(json['value'], "server");
@ -805,7 +807,7 @@ function on_ws_recv(evt) {
divlog(json['value'], true); divlog(json['value'], true);
break; break;
case 'pocsag_data': case 'pocsag_data':
update_pocsag_panel(json['value']); $('#openwebrx-panel-pocsag-message').pocsagMessagePanel().pushMessage(json['value']);
break; break;
case 'backoff': case 'backoff':
divlog("Server is currently busy: " + json['reason'], true); divlog("Server is currently busy: " + json['reason'], true);
@ -941,141 +943,6 @@ function update_metadata(meta) {
} }
function html_escape(input) {
return $('<div/>').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]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
} 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]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + html_escape(matches[3]);
} else {
linkedmsg = html_escape(linkedmsg);
}
}
$b.append($(
'<tr data-timestamp="' + msg['timestamp'] + '">' +
'<td>' + pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()) + '</td>' +
'<td class="decimal">' + msg['db'] + '</td>' +
'<td class="decimal">' + msg['dt'] + '</td>' +
'<td class="decimal freq">' + msg['freq'] + '</td>' +
'<td class="message">' + linkedmsg + '</td>' +
'</tr>'
));
$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 = '<div class="aprs-symbol aprs-symboltable-overlay" style="' + stylesToString(s) + '"></div>';
}
} 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 = '<a ' + attrs + ' href="map?callsign=' + source + '" target="openwebrx-map">' + overlay + '</a>';
} else {
link = '<div ' + attrs + '>' + overlay + '</div>'
}
$b.append($(
'<tr>' +
'<td>' + timestamp + '</td>' +
'<td class="callsign">' + source + '</td>' +
'<td class="coord">' + link + '</td>' +
'<td class="message">' + (msg.comment || msg.message || '') + '</td>' +
'</tr>'
));
$b.scrollTop($b[0].scrollHeight);
}
function update_pocsag_panel(msg) {
var $b = $('#openwebrx-panel-pocsag-message').find('tbody');
$b.append($(
'<tr>' +
'<td class="address">' + msg.address + '</td>' +
'<td class="message">' + msg.message + '</td>' +
'</tr>'
));
$b.scrollTop($b[0].scrollHeight);
}
function clear_metadata() { function clear_metadata() {
$(".openwebrx-meta-panel .openwebrx-meta-autoclear").text(""); $(".openwebrx-meta-panel .openwebrx-meta-autoclear").text("");
$(".openwebrx-meta-slot").removeClass("active").removeClass("sync"); $(".openwebrx-meta-slot").removeClass("active").removeClass("sync");
@ -1376,7 +1243,6 @@ function openwebrx_init() {
secondary_demod_init(); secondary_demod_init();
digimodes_init(); digimodes_init();
initPanels(); initPanels();
$('.webrx-mouse-freq').frequencyDisplay();
$('#openwebrx-panel-receiver').demodulatorPanel(); $('#openwebrx-panel-receiver').demodulatorPanel();
window.addEventListener("resize", openwebrx_resize); window.addEventListener("resize", openwebrx_resize);
bookmarks = new BookmarkBar(); bookmarks = new BookmarkBar();
@ -1588,7 +1454,10 @@ function secondary_demod_init() {
.mousedown(secondary_demod_canvas_container_mousedown) .mousedown(secondary_demod_canvas_container_mousedown)
.mouseenter(secondary_demod_canvas_container_mousein) .mouseenter(secondary_demod_canvas_container_mousein)
.mouseleave(secondary_demod_canvas_container_mouseleave); .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) { function secondary_demod_push_data(x) {

View File

@ -56,7 +56,8 @@ Support and info: https://groups.io/g/openwebrx
return return
# Get error messages about unknown / unavailable features as soon as possible # Get error messages about unknown / unavailable features as soon as possible
SdrService.loadProps() # start up "always-on" sources right away
SdrService.getSources()
Services.start() Services.start()

View File

@ -186,8 +186,8 @@ class AudioWriter(object):
) )
try: try:
for line in decoder.stdout: for line in decoder.stdout:
self.outputWriter.send((job.freq, line)) self.outputWriter.send((self.profile, job.freq, line))
except OSError: except (OSError, AttributeError):
decoder.stdout.flush() decoder.stdout.flush()
# TODO uncouple parsing from the output so that decodes can still go to the map and the spotters # 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.") logger.debug("output has gone away while decoding job.")

View File

@ -124,6 +124,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
"initial_squelch_level", "initial_squelch_level",
"profile_id", "profile_id",
"squelch_auto_margin", "squelch_auto_margin",
"frequency_display_precision",
] ]
def __init__(self, conn): def __init__(self, conn):
@ -425,7 +426,12 @@ class MapConnection(OpenWebRxClient):
super().__init__(conn) super().__init__(conn)
pm = Config.get() 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) 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)} 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)) 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 "type" in self.handshake:
if self.handshake["type"] == "receiver": if self.handshake["type"] == "receiver":

View File

@ -126,6 +126,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
"lib/ProgressBar.js", "lib/ProgressBar.js",
"lib/Measurement.js", "lib/Measurement.js",
"lib/FrequencyDisplay.js", "lib/FrequencyDisplay.js",
"lib/MessagePanel.js",
"lib/Js8Threads.js", "lib/Js8Threads.js",
"lib/Modes.js", "lib/Modes.js",
], ],

View File

@ -28,7 +28,7 @@ class Js8Profiles(object):
class Js8Profile(AudioChopperProfile, metaclass=ABCMeta): class Js8Profile(AudioChopperProfile, metaclass=ABCMeta):
def decoding_depth(self, mode): def decoding_depth(self):
pm = Config.get() pm = Config.get()
# return global default # return global default
if "js8_decoding_depth" in pm: if "js8_decoding_depth" in pm:
@ -40,7 +40,7 @@ class Js8Profile(AudioChopperProfile, metaclass=ABCMeta):
return "%y%m%d_%H%M%S" return "%y%m%d_%H%M%S"
def decoder_commandline(self, file): 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 @abstractmethod
def get_sub_mode(self): def get_sub_mode(self):
@ -85,7 +85,7 @@ class Js8Parser(Parser):
def parse(self, messages): def parse(self, messages):
for raw in messages: for raw in messages:
try: try:
freq, raw_msg = raw profile, freq, raw_msg = raw
self.setDialFrequency(freq) self.setDialFrequency(freq)
msg = raw_msg.decode().rstrip() msg = raw_msg.decode().rstrip()
if Js8Parser.decoderRegex.match(msg): if Js8Parser.decoderRegex.match(msg):

View File

@ -81,11 +81,12 @@ class PskReporter(object):
else: else:
self.spotCounter.inc() self.spotCounter.inc()
self.spots.append(spot) self.spots.append(spot)
self.scheduleNextUpload() self.scheduleNextUpload()
def upload(self): def upload(self):
try: try:
with self.spotLock: with self.spotLock:
self.timer = None
spots = self.spots spots = self.spots
self.spots = [] self.spots = []
@ -94,9 +95,6 @@ class PskReporter(object):
except Exception: except Exception:
logger.exception("Failed to upload spots") logger.exception("Failed to upload spots")
self.timer = None
self.scheduleNextUpload()
def cancelTimer(self): def cancelTimer(self):
if self.timer: if self.timer:
self.timer.cancel() self.timer.cancel()
@ -117,6 +115,8 @@ class Uploader(object):
def getPackets(self, spots): def getPackets(self, spots):
encoded = [self.encodeSpot(spot) for spot in 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): def chunks(l, n):
"""Yield successive n-sized chunks from l.""" """Yield successive n-sized chunks from l."""
@ -152,40 +152,63 @@ class Uploader(object):
return [len(s)] + list(s.encode("utf-8")) return [len(s)] + list(s.encode("utf-8"))
def encodeSpot(self, spot): def encodeSpot(self, spot):
return bytes( try:
self.encodeString(spot["callsign"]) return bytes(
+ list(int(spot["freq"]).to_bytes(4, "big")) self.encodeString(spot["callsign"])
+ list(int(spot["db"]).to_bytes(1, "big", signed=True)) + list(int(spot["freq"]).to_bytes(4, "big"))
+ self.encodeString(spot["mode"]) + list(int(spot["db"]).to_bytes(1, "big", signed=True))
+ self.encodeString(spot["locator"]) + self.encodeString(spot["mode"])
# informationsource. 1 means "automatically extracted + self.encodeString(spot["locator"])
+ [0x01] # informationsource. 1 means "automatically extracted
+ list(int(spot["timestamp"] / 1000).to_bytes(4, "big")) + [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): 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( return bytes(
# id, length # id
[0x00, 0x03, 0x00, 0x24] [0x00, 0x03]
# length
+ list(length.to_bytes(2, 'big'))
+ Uploader.receieverDelimiter + Uploader.receieverDelimiter
# number of fields # number of fields
+ [0x00, 0x03, 0x00, 0x00] + list(num_fields.to_bytes(2, 'big'))
# padding
+ [0x00, 0x00]
# receiverCallsign # receiverCallsign
+ [0x80, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# receiverLocator # receiverLocator
+ [0x80, 0x04, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x04, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# decodingSoftware # decodingSoftware
+ [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# antennaInformation
+ (
[0x80, 0x09, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] if with_antenna else []
)
# padding # padding
+ [0x00, 0x00] + [0x00, 0x00]
) )
def getReceiverInformation(self): def getReceiverInformation(self):
pm = Config.get() pm = Config.get()
callsign = pm["pskreporter_callsign"] bodyFields = [
locator = Locator.fromCoordinates(pm["receiver_gps"]) # callsign
decodingSoftware = "OpenWebRX " + openwebrx_version pm["pskreporter_callsign"],
body = [b for s in [callsign, locator, decodingSoftware] for b in self.encodeString(s)] # 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 = self.pad(body, 4)
body = bytes(Uploader.receieverDelimiter + list((len(body) + 4).to_bytes(2, "big")) + body) body = bytes(Uploader.receieverDelimiter + list((len(body) + 4).to_bytes(2, "big")) + body)
return body return body

View File

@ -13,7 +13,7 @@ class SdrService(object):
lastPort = None lastPort = None
@staticmethod @staticmethod
def loadProps(): def _loadProps():
if SdrService.sdrProps is None: if SdrService.sdrProps is None:
pm = Config.get() pm = Config.get()
featureDetector = FeatureDetector() featureDetector = FeatureDetector()
@ -60,7 +60,6 @@ class SdrService(object):
@staticmethod @staticmethod
def getSource(id): def getSource(id):
SdrService.loadProps()
sources = SdrService.getSources() sources = SdrService.getSources()
if not sources: if not sources:
return None return None
@ -70,7 +69,7 @@ class SdrService(object):
@staticmethod @staticmethod
def getSources(): def getSources():
SdrService.loadProps() SdrService._loadProps()
for id in SdrService.sdrProps.keys(): for id in SdrService.sdrProps.keys():
if not id in SdrService.sources: if not id in SdrService.sources:
props = SdrService.sdrProps[id] props = SdrService.sdrProps[id]

View File

@ -14,8 +14,9 @@ logger = logging.getLogger(__name__)
class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta): class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta):
def decoding_depth(self, mode): def decoding_depth(self):
pm = Config.get() pm = Config.get()
mode = self.getMode().lower()
# mode-specific setting? # mode-specific setting?
if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]: if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]:
return pm["wsjt_decoding_depths"][mode] return pm["wsjt_decoding_depths"][mode]
@ -25,64 +26,76 @@ class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta):
# default when no setting is provided # default when no setting is provided
return 3 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): class Ft8Profile(WsjtProfile):
def getInterval(self): def getInterval(self):
return 15 return 15
def getFileTimestampFormat(self):
return "%y%m%d_%H%M%S"
def decoder_commandline(self, file): 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): class WsprProfile(WsjtProfile):
def getInterval(self): def getInterval(self):
return 120 return 120
def getFileTimestampFormat(self):
return "%y%m%d_%H%M"
def decoder_commandline(self, file): def decoder_commandline(self, file):
cmd = ["wsprd"] cmd = ["wsprd"]
if self.decoding_depth("wspr") > 1: if self.decoding_depth() > 1:
cmd += ["-d"] cmd += ["-d"]
cmd += [file] cmd += [file]
return cmd return cmd
def getMode(self):
return "WSPR"
class Jt65Profile(WsjtProfile): class Jt65Profile(WsjtProfile):
def getInterval(self): def getInterval(self):
return 60 return 60
def getFileTimestampFormat(self):
return "%y%m%d_%H%M"
def decoder_commandline(self, file): 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): class Jt9Profile(WsjtProfile):
def getInterval(self): def getInterval(self):
return 60 return 60
def getFileTimestampFormat(self):
return "%y%m%d_%H%M"
def decoder_commandline(self, file): 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): class Ft4Profile(WsjtProfile):
def getInterval(self): def getInterval(self):
return 7.5 return 7.5
def getFileTimestampFormat(self):
return "%y%m%d_%H%M%S"
def decoder_commandline(self, file): 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): class Fst4Profile(WsjtProfile):
@ -94,13 +107,11 @@ class Fst4Profile(WsjtProfile):
def getInterval(self): def getInterval(self):
return self.interval 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): 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 @staticmethod
def getEnabledProfiles(): def getEnabledProfiles():
@ -118,13 +129,11 @@ class Fst4wProfile(WsjtProfile):
def getInterval(self): def getInterval(self):
return self.interval 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): 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 @staticmethod
def getEnabledProfiles(): def getEnabledProfiles():
@ -134,12 +143,10 @@ class Fst4wProfile(WsjtProfile):
class WsjtParser(Parser): class WsjtParser(Parser):
modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4", "`": "FST4"}
def parse(self, messages): def parse(self, messages):
for data in messages: for data in messages:
try: try:
freq, raw_msg = data profile, freq, raw_msg = data
self.setDialFrequency(freq) self.setDialFrequency(freq)
msg = raw_msg.decode().rstrip() msg = raw_msg.decode().rstrip()
# known debug messages we know to skip # known debug messages we know to skip
@ -148,19 +155,20 @@ class WsjtParser(Parser):
if msg.startswith(" EOF on input file"): if msg.startswith(" EOF on input file"):
return return
modes = list(WsjtParser.modes.keys()) mode = profile.getMode()
if msg[21] in modes or msg[19] in modes: if mode == "WSPR":
decoder = Jt9Decoder() decoder = WsprDecoder(profile)
else: else:
decoder = WsprDecoder() decoder = Jt9Decoder(profile)
out = decoder.parse(msg, freq) out = decoder.parse(msg, freq)
if "mode" in out: out["mode"] = mode
self.pushDecode(out["mode"])
if "callsign" in out and "locator" in out: self.pushDecode(mode)
Map.getSharedInstance().updateLocation( if "callsign" in out and "locator" in out:
out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band Map.getSharedInstance().updateLocation(
) out["callsign"], LocatorLocation(out["locator"]), mode, self.band
PskReporter.getSharedInstance().spot(out) )
PskReporter.getSharedInstance().spot(out)
self.handler.write_wsjt_message(out) self.handler.write_wsjt_message(out)
except (ValueError, IndexError): except (ValueError, IndexError):
@ -187,13 +195,21 @@ class WsjtParser(Parser):
class Decoder(ABC): 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): def __init__(self, profile):
ts = datetime.strptime(instring, dateformat) self.profile = profile
return int(
datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000 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 @abstractmethod
def parse(self, msg, dial_freq): def parse(self, msg, dial_freq):
@ -219,18 +235,7 @@ class Jt9Decoder(Decoder):
# '0003 -4 0.4 1762 # CQ R2ABM KO85' # '0003 -4 0.4 1762 # CQ R2ABM KO85'
# fst4 sample # fst4 sample
# '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV' # '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV'
modes = list(WsjtParser.modes.keys()) msg, timestamp = self.parse_timestamp(msg)
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"
wsjt_msg = msg[17:53].strip() wsjt_msg = msg[17:53].strip()
result = { result = {
@ -238,7 +243,6 @@ class Jt9Decoder(Decoder):
"db": float(msg[0:3]), "db": float(msg[0:3]),
"dt": float(msg[4:8]), "dt": float(msg[4:8]),
"freq": dial_freq + int(msg[9:13]), "freq": dial_freq + int(msg[9:13]),
"mode": mode,
"msg": wsjt_msg, "msg": wsjt_msg,
} }
result.update(self.parseMessage(wsjt_msg)) result.update(self.parseMessage(wsjt_msg))
@ -246,20 +250,20 @@ class Jt9Decoder(Decoder):
class WsprDecoder(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): def parse(self, msg, dial_freq):
# wspr sample # wspr sample
# '2600 -24 0.4 0.001492 -1 G8AXA JO01 33' # '2600 -24 0.4 0.001492 -1 G8AXA JO01 33'
# '0052 -29 2.6 0.001486 0 G02CWT IO92 23' # '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 = { result = {
"timestamp": self.parse_timestamp(msg[0:4], "%H%M"), "timestamp": timestamp,
"db": float(msg[5:8]), "db": float(msg[0:3]),
"dt": float(msg[9:13]), "dt": float(msg[4:8]),
"freq": dial_freq + int(float(msg[14:24]) * 1e6), "freq": dial_freq + int(float(msg[10:20]) * 1e6),
"drift": int(msg[25:28]), "drift": int(msg[20:23]),
"mode": "WSPR",
"msg": wsjt_msg, "msg": wsjt_msg,
} }
result.update(self.parseMessage(wsjt_msg)) result.update(self.parseMessage(wsjt_msg))