Merge branch 'develop' into m17
This commit is contained in:
		| @@ -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"]_/ | ||||
| #                                                          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. | ||||
| # Lowering this setting will give you a more sensitive squelch, but it may also cause unwanted squelch openings when | ||||
| # 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 | ||||
| pskreporter_enabled = False | ||||
| pskreporter_callsign = "N0CALL" | ||||
| # optional antenna information, uncomment to enable | ||||
| #pskreporter_antenna_information = "Dipole" | ||||
|  | ||||
| # === Web admin settings === | ||||
| # this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote | ||||
|   | ||||
							
								
								
									
										4
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| #!/bin/bash | ||||
| 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 | ||||
|  | ||||
| #DEBHELPER# | ||||
| #DEBHELPER# | ||||
|   | ||||
| @@ -981,6 +981,7 @@ img.openwebrx-mirror-img | ||||
|  | ||||
| .openwebrx-message-panel { | ||||
|     height: 180px; | ||||
|     position: relative; | ||||
| } | ||||
|  | ||||
| .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="pocsag"] #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="wspr"]   #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="packet"] #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; | ||||
| } | ||||
| @@ -1165,7 +1170,9 @@ img.openwebrx-mirror-img | ||||
| #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="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; | ||||
|     margin: -10px; | ||||
|   | ||||
| @@ -60,40 +60,10 @@ | ||||
|                     </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"> | ||||
|                 <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> | ||||
|             <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-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message"></div> | ||||
|             <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message"></div> | ||||
|             <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"></div> | ||||
|             <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"></div> | ||||
|             <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-slot"> | ||||
|   | ||||
| @@ -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)); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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, <a href="https://www.google.com/maps/search/?api=1&query=' + query + '" target="_blank">[maps]</a>'); | ||||
|     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']); | ||||
| }; | ||||
|   | ||||
| @@ -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($( | ||||
|         '<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() { | ||||
|     this.threads = this.threads.filter(function(t) { | ||||
|         return t.purgeOldMessages(); | ||||
|   | ||||
							
								
								
									
										247
									
								
								htdocs/lib/MessagePanel.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								htdocs/lib/MessagePanel.js
									
									
									
									
									
										Normal 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'); | ||||
| }; | ||||
| @@ -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( | ||||
|             '<h3>' + marker.config['receiver_name'] + '</h3>' + | ||||
|             '<div>Receiver location</div>' | ||||
|         ); | ||||
|         infowindow.open(map, marker); | ||||
|     } | ||||
|  | ||||
|     var getScale = function(lastseen) { | ||||
|         var age = new Date().getTime() - lastseen; | ||||
|         var scale = 1; | ||||
|   | ||||
| @@ -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 $('<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() { | ||||
|     $(".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) { | ||||
|   | ||||
| @@ -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() | ||||
|  | ||||
|   | ||||
| @@ -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.") | ||||
|   | ||||
| @@ -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": | ||||
|   | ||||
| @@ -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", | ||||
|         ], | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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] | ||||
|   | ||||
							
								
								
									
										152
									
								
								owrx/wsjt.py
									
									
									
									
									
								
							
							
						
						
									
										152
									
								
								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)) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jakob Ketterl
					Jakob Ketterl