From 5337c207445241a044abadf1fb7843bf56387b76 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 9 Jan 2021 19:01:39 +0100 Subject: [PATCH 01/42] remove duplicate --- owrx/connection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/owrx/connection.py b/owrx/connection.py index 73e1f6d..37aafa2 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -110,7 +110,6 @@ class OpenWebRxClient(Client, metaclass=ABCMeta): class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient): sdr_config_keys = [ - "waterfall_min_level", "waterfall_min_level", "waterfall_max_level", "samp_rate", From 73b75edc14426cea6cfe0c0f11ebc745a63fa3aa Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 9 Jan 2021 19:10:08 +0100 Subject: [PATCH 02/42] remove duplicate import --- owrx/connection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/owrx/connection.py b/owrx/connection.py index 37aafa2..62718a1 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -1,4 +1,3 @@ -from owrx.config import Config from owrx.details import ReceiverDetails from owrx.dsp import DspManager from owrx.cpu import CpuUsageThread From 113c06fae4f9c3489a3b86eb2bdd2f46307af0b9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 9 Jan 2021 19:19:53 +0100 Subject: [PATCH 03/42] introduce separate wsjt-x version check based on wsjtx_app_version --- owrx/feature.py | 21 +++++++++++++++++++++ owrx/modes.py | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/owrx/feature.py b/owrx/feature.py index fbdb425..1a37494 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -77,6 +77,7 @@ class FeatureDetector(object): "digital_voice_freedv": ["freedv_rx", "sox"], "digital_voice_m17": ["m17_demod", "sox"], "wsjt-x": ["wsjtx", "sox"], + "wsjt-x-2-3": ["wsjtx_2_3", "sox"], "packet": ["direwolf", "sox"], "pocsag": ["digiham", "sox"], "js8call": ["js8", "sox"], @@ -459,6 +460,26 @@ class FeatureDetector(object): """ return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True) + def _has_wsjtx_version(self, required_version): + wsjt_version_regex = re.compile("^WSJT-X (.*)$") + + try: + process = subprocess.Popen(["wsjtx_app_version", "--version"], stdout=subprocess.PIPE) + matches = wsjt_version_regex.match(process.stdout.readline().decode()) + if matches is None: + return False + version = LooseVersion(matches.group(1)) + process.wait(1) + return version >= required_version + except FileNotFoundError: + return False + + def has_wsjtx_2_3(self): + """ + Newer digital modes (e.g. FST4, FST4) require WSJT-X in at least version 2.3. + """ + return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.3")) + def has_js8(self): """ To decode JS8, you will need to install [JS8Call](http://js8call.com/) diff --git a/owrx/modes.py b/owrx/modes.py index 8b642e1..a9ce30f 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -75,8 +75,8 @@ class Modes(object): DigitalMode( "wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True ), - DigitalMode("fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), - DigitalMode("fst4w", "FST4W", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True), + DigitalMode("fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x-2-3"], service=True), + DigitalMode("fst4w", "FST4W", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"], service=True), DigitalMode("js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True), DigitalMode( "packet", From 502546f9d3b616adb8770535381f32464b36526d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 9 Jan 2021 20:01:39 +0100 Subject: [PATCH 04/42] improve cpu usage thread instance protection --- owrx/cpu.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/owrx/cpu.py b/owrx/cpu.py index 6b2b82c..bff3f42 100644 --- a/owrx/cpu.py +++ b/owrx/cpu.py @@ -23,6 +23,7 @@ class CpuUsageThread(threading.Thread): self.last_idletime = 0 self.endEvent = threading.Event() super().__init__() + self.start() def run(self): logger.debug("cpu usage thread starting up") @@ -59,8 +60,6 @@ class CpuUsageThread(threading.Thread): def add_client(self, c): self.clients.append(c) - if not self.is_alive(): - self.start() def remove_client(self, c): try: @@ -71,6 +70,7 @@ class CpuUsageThread(threading.Thread): self.shutdown() def shutdown(self): - CpuUsageThread.sharedInstance = None + with CpuUsageThread.creationLock: + CpuUsageThread.sharedInstance = None self.doRun = False self.endEvent.set() From b27c03c1c464f10828335db91db154dc3a95b809 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 9 Jan 2021 20:08:40 +0100 Subject: [PATCH 05/42] restore autostart to avoid unused thread --- owrx/cpu.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/owrx/cpu.py b/owrx/cpu.py index bff3f42..cad912f 100644 --- a/owrx/cpu.py +++ b/owrx/cpu.py @@ -23,7 +23,6 @@ class CpuUsageThread(threading.Thread): self.last_idletime = 0 self.endEvent = threading.Event() super().__init__() - self.start() def run(self): logger.debug("cpu usage thread starting up") @@ -60,6 +59,8 @@ class CpuUsageThread(threading.Thread): def add_client(self, c): self.clients.append(c) + if not self.is_alive(): + self.start() def remove_client(self, c): try: From a90ef4efec13136f146b138e41cc92f77df228b0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 Jan 2021 02:15:23 +0100 Subject: [PATCH 06/42] add m17-demod as recommended package --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 3538a8d..6654e6a 100644 --- a/debian/control +++ b/debian/control @@ -11,6 +11,6 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git Package: openwebrx Architecture: all Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.17), netcat, owrx-connector (>= 0.4), soapysdr-tools, python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends} -Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, eb200-connector, hpsdrconnector, aprs-symbols +Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, eb200-connector, hpsdrconnector, aprs-symbols, m17-demod Description: multi-user web sdr Open source, multi-user SDR receiver with a web interface \ No newline at end of file From db9859098537abe25a7a8b07e9443e4e8d043fe0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 13 Jan 2021 23:44:00 +0100 Subject: [PATCH 07/42] implement profile validation --- owrx/source/__init__.py | 36 ++++++++++++++++++++++++++++++------ owrx/source/resampler.py | 4 ++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/owrx/source/__init__.py b/owrx/source/__init__.py index eab95ad..39c143d 100644 --- a/owrx/source/__init__.py +++ b/owrx/source/__init__.py @@ -71,9 +71,38 @@ class SdrSource(ABC): self.state = SdrSource.STATE_STOPPED self.busyState = SdrSource.BUSYSTATE_IDLE + self.validateProfiles() + if self.isAlwaysOn(): self.start() + def validateProfiles(self): + props = PropertyStack() + props.addLayer(1, self.props) + for id, p in self.props["profiles"].items(): + props.replaceLayer(0, self._getProfilePropertyLayer(p)) + if "center_freq" not in props: + logger.warning("Profile \"%s\" does not specify a center_freq", id) + continue + if "samp_rate" not in props: + logger.warning("Profile \"%s\" does not specify a samp_rate", id) + continue + if "start_freq" in props: + start_freq = props["start_freq"] + srh = props["samp_rate"] / 2 + center_freq = props["center_freq"] + if start_freq < center_freq - srh or start_freq > center_freq + srh: + logger.warning("start_freq for profile \"%s\" is out of range", id) + + def _getProfilePropertyLayer(self, profile): + layer = PropertyLayer() + for (key, value) in profile.items(): + # skip the name, that would overwrite the source name. + if key == "name": + continue + layer[key] = value + return layer + def isAlwaysOn(self): return "always-on" in self.props and self.props["always-on"] @@ -115,12 +144,7 @@ class SdrSource(ABC): profile = profiles[profile_id] self.profile_id = profile_id - layer = PropertyLayer() - for (key, value) in profile.items(): - # skip the name, that would overwrite the source name. - if key == "name": - continue - layer[key] = value + layer = self._getProfilePropertyLayer(profile) self.props.replaceLayer(0, layer) def getId(self): diff --git a/owrx/source/resampler.py b/owrx/source/resampler.py index a367894..03c2097 100644 --- a/owrx/source/resampler.py +++ b/owrx/source/resampler.py @@ -32,3 +32,7 @@ class Resampler(DirectSource): def activateProfile(self, profile_id=None): logger.warning("Resampler does not support setting profiles") pass + + def validateProfiles(self): + # resampler does not support profiles + pass From 7f3071336b45584ef7c107ea72b64e76705077c0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 13 Jan 2021 23:50:36 +0100 Subject: [PATCH 08/42] check if new value is undefined --- htdocs/lib/Demodulator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js index de6f3c5..8192a04 100644 --- a/htdocs/lib/Demodulator.js +++ b/htdocs/lib/Demodulator.js @@ -236,7 +236,7 @@ Demodulator.prototype.emit = function(event, params) { }; Demodulator.prototype.set_offset_frequency = function(to_what) { - if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return; + if (typeof(to_what) == 'undefined' || to_what > bandwidth / 2 || to_what < -bandwidth / 2) return; to_what = Math.round(to_what); if (this.offset_frequency === to_what) { return; From c5323f8d5470f86a77c2b0f449fff62b1f3aba5c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 Jan 2021 00:12:53 +0100 Subject: [PATCH 09/42] validate start_freq, use center_freq if invalid --- htdocs/lib/DemodulatorPanel.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index 2cdca68..b98044b 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -181,7 +181,7 @@ DemodulatorPanel.prototype.collectParams = function() { squelch_level: -150, mod: 'nfm' } - return $.extend(new Object(), defaults, this.initialParams, this.transformHashParams(this.parseHash())); + return $.extend(new Object(), defaults, this.validateInitialParams(this.initialParams), this.transformHashParams(this.parseHash())); }; DemodulatorPanel.prototype.startDemodulator = function() { @@ -287,7 +287,7 @@ DemodulatorPanel.prototype.validateHash = function(params) { var self = this; params = Object.keys(params).filter(function(key) { if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') { - return params.freq && Math.abs(params.freq - self.center_freq) < bandwidth / 2; + return params.freq && Math.abs(params.freq - self.center_freq) <= bandwidth / 2; } return true; }).reduce(function(p, key) { @@ -303,6 +303,17 @@ DemodulatorPanel.prototype.validateHash = function(params) { return params; }; +DemodulatorPanel.prototype.validateInitialParams = function(params) { + return Object.fromEntries( + Object.entries(params).filter(function(a) { + if (a[0] == "offset_frequency") { + return Math.abs(a[1]) <= bandwidth / 2; + } + return true; + }) + ); +}; + DemodulatorPanel.prototype.updateHash = function() { var demod = this.getDemodulator(); if (!demod) return; From 57efdff43ec4bf8a303e92922846a2407a4af190 Mon Sep 17 00:00:00 2001 From: dl9rdz Date: Wed, 6 Jan 2021 09:35:00 +0100 Subject: [PATCH 10/42] try enforcing 44100 samples/s for audio to avoid problems with odd defautl sampling rates --- htdocs/lib/AudioEngine.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/htdocs/lib/AudioEngine.js b/htdocs/lib/AudioEngine.js index 9434c83..eb7fa4f 100644 --- a/htdocs/lib/AudioEngine.js +++ b/htdocs/lib/AudioEngine.js @@ -14,7 +14,11 @@ function AudioEngine(maxBufferLength, audioReporter) { this.onStartCallbacks = []; this.started = false; - this.audioContext = new ctx(); + try { + this.audioContext = new ctx({sampleRate: 44100}); + } catch (error) { + this.audioContext = new ctx(); + } var me = this; this.audioContext.onstatechange = function() { if (me.audioContext.state !== 'running') return; From 2334ad1d5b2f2b22325f2773927d120e5f95f096 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 Jan 2021 17:07:43 +0100 Subject: [PATCH 11/42] try a list of sample rates; prefer 48kHz --- htdocs/lib/AudioEngine.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/htdocs/lib/AudioEngine.js b/htdocs/lib/AudioEngine.js index eb7fa4f..fb08bfc 100644 --- a/htdocs/lib/AudioEngine.js +++ b/htdocs/lib/AudioEngine.js @@ -14,9 +14,17 @@ function AudioEngine(maxBufferLength, audioReporter) { this.onStartCallbacks = []; this.started = false; - try { - this.audioContext = new ctx({sampleRate: 44100}); - } catch (error) { + // try common working sample rates + if (![48000, 44100].some(function(sr) { + try { + this.audioContext = new ctx({sampleRate: sr}); + return true; + } catch (e) { + return false; + } + }, this)) { + // fallback: let the browser decide + // this may cause playback problems down the line this.audioContext = new ctx(); } var me = this; From 132bd2b44538103b3eff9db20de478c84cdc67ea Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 Jan 2021 20:52:56 +0100 Subject: [PATCH 12/42] create reporting engine to distribute spots --- owrx/__main__.py | 4 ++-- owrx/js8.py | 4 ++-- owrx/pskreporter.py | 38 ++++++--------------------------- owrx/reporting.py | 51 +++++++++++++++++++++++++++++++++++++++++++++ owrx/wsjt.py | 4 ++-- 5 files changed, 63 insertions(+), 38 deletions(-) create mode 100644 owrx/reporting.py diff --git a/owrx/__main__.py b/owrx/__main__.py index a0e83dc..6afcb93 100644 --- a/owrx/__main__.py +++ b/owrx/__main__.py @@ -11,7 +11,7 @@ from owrx.sdr import SdrService from socketserver import ThreadingMixIn from owrx.service import Services from owrx.websocket import WebSocketConnection -from owrx.pskreporter import PskReporter +from owrx.reporting import ReportingEngine from owrx.version import openwebrx_version @@ -67,4 +67,4 @@ Support and info: https://groups.io/g/openwebrx except KeyboardInterrupt: WebSocketConnection.closeAll() Services.stop() - PskReporter.stop() + ReportingEngine.stop() diff --git a/owrx/js8.py b/owrx/js8.py index 18a6cef..79e1850 100644 --- a/owrx/js8.py +++ b/owrx/js8.py @@ -4,10 +4,10 @@ import re from js8py import Js8 from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound from .map import Map, LocatorLocation -from .pskreporter import PskReporter from .metrics import Metrics, CounterMetric from .config import Config from abc import ABCMeta, abstractmethod +from owrx.reporting import ReportingEngine import logging @@ -102,7 +102,7 @@ class Js8Parser(Parser): Map.getSharedInstance().updateLocation( frame.callsign, LocatorLocation(frame.grid), "JS8", self.band ) - PskReporter.getSharedInstance().spot({ + ReportingEngine.getSharedInstance().spot({ "callsign": frame.callsign, "mode": "JS8", "locator": frame.grid, diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 662b2a7..2b8f69f 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -9,43 +9,19 @@ from owrx.config import Config from owrx.version import openwebrx_version from owrx.locator import Locator from owrx.metrics import Metrics, CounterMetric +from owrx.reporting import Reporter logger = logging.getLogger(__name__) -class PskReporterDummy(object): - """ - used in place of the PskReporter when reporting is disabled. - does nothing. - """ - - def spot(self, spot): - pass - - def cancelTimer(self): - pass - - -class PskReporter(object): - sharedInstance = None - creationLock = threading.Lock() +class PskReporter(Reporter): interval = 300 - supportedModes = ["FT8", "FT4", "JT9", "JT65", "FST4", "FST4W", "JS8"] - @staticmethod - def getSharedInstance(): - with PskReporter.creationLock: - if PskReporter.sharedInstance is None: - if Config.get()["pskreporter_enabled"]: - PskReporter.sharedInstance = PskReporter() - else: - PskReporter.sharedInstance = PskReporterDummy() - return PskReporter.sharedInstance + def getSupportedModes(self): + return ["FT8", "FT4", "JT9", "JT65", "FST4", "FST4W", "JS8"] - @staticmethod - def stop(): - if PskReporter.sharedInstance: - PskReporter.sharedInstance.cancelTimer() + def stop(self): + self.cancelTimer() def __init__(self): self.spots = [] @@ -72,8 +48,6 @@ class PskReporter(object): return reduce(and_, map(lambda key: s1[key] == s2[key], keys)) def spot(self, spot): - if not spot["mode"] in PskReporter.supportedModes: - return with self.spotLock: if any(x for x in self.spots if self.spotEquals(spot, x)): # dupe diff --git a/owrx/reporting.py b/owrx/reporting.py new file mode 100644 index 0000000..3b14935 --- /dev/null +++ b/owrx/reporting.py @@ -0,0 +1,51 @@ +import threading +from abc import ABC, abstractmethod +from owrx.config import Config + + +class Reporter(ABC): + @abstractmethod + def stop(self): + pass + + @abstractmethod + def spot(self, spot): + pass + + @abstractmethod + def getSupportedModes(self): + return [] + + +class ReportingEngine(object): + creationLock = threading.Lock() + sharedInstance = None + + @staticmethod + def getSharedInstance(): + with ReportingEngine.creationLock: + if ReportingEngine.sharedInstance is None: + ReportingEngine.sharedInstance = ReportingEngine() + return ReportingEngine.sharedInstance + + @staticmethod + def stopAll(): + with ReportingEngine.creationLock: + if ReportingEngine.sharedInstance is not None: + ReportingEngine.sharedInstance.stop() + + def __init__(self): + self.reporters = [] + if Config.get()["pskreporter_enabled"]: + # inline import due to circular dependencies + from owrx.pskreporter import PskReporter + self.reporters += [PskReporter()] + + def stop(self): + for r in self.reporters: + r.stop() + + def spot(self, spot): + for r in self.reporters: + if spot["mode"] in r.getSupportedModes(): + r.spot(spot) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 90c4d9c..fd23550 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -2,7 +2,7 @@ from datetime import datetime, timezone from owrx.map import Map, LocatorLocation import re from owrx.metrics import Metrics, CounterMetric -from owrx.pskreporter import PskReporter +from owrx.reporting import ReportingEngine from owrx.parser import Parser from owrx.audio import AudioChopperProfile from abc import ABC, ABCMeta, abstractmethod @@ -168,7 +168,7 @@ class WsjtParser(Parser): Map.getSharedInstance().updateLocation( out["callsign"], LocatorLocation(out["locator"]), mode, self.band ) - PskReporter.getSharedInstance().spot(out) + ReportingEngine.getSharedInstance().spot(out) self.handler.write_wsjt_message(out) except (ValueError, IndexError): From e3aa3fa4c65009c11a2c6982a78dfcd7756518f4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 Jan 2021 22:54:59 +0100 Subject: [PATCH 13/42] implement wsprnet reporting, refs #62 --- owrx/reporting.py | 7 ++++- owrx/wsjt.py | 2 +- owrx/wsprnet.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 owrx/wsprnet.py diff --git a/owrx/reporting.py b/owrx/reporting.py index 3b14935..d6e0831 100644 --- a/owrx/reporting.py +++ b/owrx/reporting.py @@ -36,10 +36,15 @@ class ReportingEngine(object): def __init__(self): self.reporters = [] - if Config.get()["pskreporter_enabled"]: + config = Config.get() + if config["pskreporter_enabled"]: # inline import due to circular dependencies from owrx.pskreporter import PskReporter self.reporters += [PskReporter()] + if config["wsprnet_enabled"]: + # inline import due to circular dependencies + from owrx.wsprnet import WsprnetReporter + self.reporters += [WsprnetReporter()] def stop(self): for r in self.reporters: diff --git a/owrx/wsjt.py b/owrx/wsjt.py index fd23550..6d2c800 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -273,4 +273,4 @@ class WsprDecoder(Decoder): m = WsprDecoder.wspr_splitter_pattern.match(msg) if m is None: return {} - return {"callsign": m.group(1), "locator": m.group(2)} + return {"callsign": m.group(1), "locator": m.group(2), "dbm": m.group(3)} diff --git a/owrx/wsprnet.py b/owrx/wsprnet.py new file mode 100644 index 0000000..5ba8535 --- /dev/null +++ b/owrx/wsprnet.py @@ -0,0 +1,75 @@ +from owrx.reporting import Reporter +from owrx.version import openwebrx_version +from owrx.config import Config +from owrx.locator import Locator +from queue import Queue, Full +from urllib import request, parse +import threading +import logging +from datetime import datetime, timezone + +logger = logging.getLogger(__name__) + +class Worker(threading.Thread): + def __init__(self, queue: Queue): + self.queue = queue + self.doRun = True + # some constants that we don't expect to change + config = Config.get() + self.callsign = config["wsprnet_callsign"] + self.locator = Locator.fromCoordinates(config["receiver_gps"]) + + super().__init__(daemon=True) + + def run(self): + while self.doRun: + try: + spot = self.queue.get() + self.uploadSpot(spot) + self.queue.task_done() + except Exception: + logger.exception("Exception while uploading WSPRNet spot") + + def uploadSpot(self, spot): + # function=wspr&date=210114&time=1732&sig=-15&dt=0.5&drift=0&tqrg=7.040019&tcall=DF2UU&tgrid=JN48&dbm=37&version=2.3.0-rc3&rcall=DD5JFK&rgrid=JN58SC&rqrg=7.040047&mode=2 + # {'timestamp': 1610655960000, 'db': -23.0, 'dt': 0.3, 'freq': 7040048, 'drift': -1, 'msg': 'LA3JJ JO59 37', 'callsign': 'LA3JJ', 'locator': 'JO59', 'mode': 'WSPR'} + date = datetime.fromtimestamp(spot["timestamp"] / 1000, tz=timezone.utc) + data = parse.urlencode({ + "function": "wspr", + "date": date.strftime("%y%m%d"), + "time": date.strftime("%H%M"), + "sig": spot["db"], + "dt": spot["dt"], + "drift": spot["drift"], + "tqrg": spot["freq"] / 1E6, + "tcall": spot["callsign"], + "tgrid": spot["locator"], + "dbm": spot["dbm"], + "version": openwebrx_version, + "rcall": self.callsign, + "rgrid": self.locator, + # mode 2 = WSPR 2 minutes + # TODO implement FST4W mode codes + "mode": 2 + }).encode() + request.urlopen("http://wsprnet.org/post/", data) + + +class WsprnetReporter(Reporter): + def __init__(self): + # max 100 entries + self.queue = Queue(100) + # single worker + Worker(self.queue).start() + + def stop(self): + pass + + def spot(self, spot): + try: + self.queue.put(spot, block=False) + except Full: + logger.warning("WSPRNet Queue overflow, one spot lost") + + def getSupportedModes(self): + return ["WSPR"] From 747a5ce7efcb8dcc09757f2c9174d161cfa84a3f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 Jan 2021 22:55:35 +0100 Subject: [PATCH 14/42] fix reporting system shutdown --- owrx/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/__main__.py b/owrx/__main__.py index 6afcb93..517c5ad 100644 --- a/owrx/__main__.py +++ b/owrx/__main__.py @@ -67,4 +67,4 @@ Support and info: https://groups.io/g/openwebrx except KeyboardInterrupt: WebSocketConnection.closeAll() Services.stop() - ReportingEngine.stop() + ReportingEngine.stopAll() From 74a4f5b2724b352ae1d1c610909af007131a9820 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 Jan 2021 22:56:52 +0100 Subject: [PATCH 15/42] add wsprnet config variables --- config_webrx.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config_webrx.py b/config_webrx.py index 2134508..dc1c1f7 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -369,6 +369,9 @@ pskreporter_callsign = "N0CALL" # optional antenna information, uncomment to enable #pskreporter_antenna_information = "Dipole" +wsprnet_enabled = False +wsprnet_callsign = "N0CALL" + # === Web admin settings === # this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote # changes to the receiver settings. enable for testing in controlled environment only. From 2c3586a92a77346d71f36597b833909d198cb481 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 Jan 2021 22:58:40 +0100 Subject: [PATCH 16/42] add changelog --- CHANGELOG.md | 1 + debian/changelog | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4af0dc6..e21e2ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Removed `port` configuration option; `rtltcp_compat` takes the port number with the new connectors - Added support for new WSJT-X modes FST4 and FST4W (only available with WSJT-X 2.3) - Added support for demodulating M17 digital voice signals using m17-cxx-demod +- New reporting infrastructur, allowing WSPR spots to be sent to wsprnet.org - New devices supported: - HPSDR devices (Hermes Lite 2) - BBRF103 / RX666 / RX888 devices supported by libsddc diff --git a/debian/changelog b/debian/changelog index b4f9544..b953785 100644 --- a/debian/changelog +++ b/debian/changelog @@ -7,6 +7,7 @@ openwebrx (0.21.0) UNRELEASED; urgency=low WSJT-X 2.3) * Added support for demodulating M17 digital voice signals using m17-cxx-demod + * New reporting infrastructur, allowing WSPR spots to be sent to wsprnet.org * New devices supported: - HPSDR devices (Hermes Lite 2) (`"type": "hpsdr"`) - BBRF103 / RX666 / RX888 devices supported by libsddc (`"type": "sddc"`) From 327371670664c754db56bad5d5f857d5acc3d168 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 Jan 2021 23:02:34 +0100 Subject: [PATCH 17/42] add some info to the config --- config_webrx.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config_webrx.py b/config_webrx.py index dc1c1f7..2d68c81 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -361,7 +361,7 @@ aprs_symbols_path = "/usr/share/aprs-symbols/png" # Antenna direction (N, NE, E, SE, S, SW, W, NW). Omnidirectional by default # aprs_igate_dir = "NE" -# === PSK Reporter setting === +# === PSK Reporter settings === # enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info # this also uses the receiver_gps setting from above, so make sure it contains a correct locator pskreporter_enabled = False @@ -369,6 +369,9 @@ pskreporter_callsign = "N0CALL" # optional antenna information, uncomment to enable #pskreporter_antenna_information = "Dipole" +# === WSPRNet reporting settings +# enable this if you want to upload WSPR spots to wsprnet.ort +# in addition to these settings also make sure that receiver_gps contains your correct location wsprnet_enabled = False wsprnet_callsign = "N0CALL" From 1b36baad88b34a394727c7b98c94c20c8d017683 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 Jan 2021 23:47:12 +0100 Subject: [PATCH 18/42] extend default WFM bandwidth to 150kHz, allowing up to 200kHz --- csdr/csdr.py | 6 +++--- htdocs/lib/Demodulator.js | 2 +- owrx/modes.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/csdr/csdr.py b/csdr/csdr.py index 95d3ca7..4deaac6 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -536,12 +536,12 @@ class dsp(object): # wideband fm has a much higher frequency deviation (75kHz). # we cannot cover this if we immediately decimate to the sample rate the audio will have later on, so we need # to compensate here. - # the factor of 5 is by experimentation only, with a minimum audio rate of 36kHz (enforced by the client) - # this allows us to cover at least +/- 80kHz of frequency spectrum (may be higher, but that's the worst case). + # the factor of 6 is by experimentation only, with a minimum audio rate of 36kHz (enforced by the client) + # this allows us to cover at least +/- 108kHz of frequency spectrum (may be higher, but that's the worst case). # the correction factor is automatically compensated for by the secondary decimation stage, which comes # after the demodulator. if self.get_demodulator() == "wfm": - correction = 5 + correction = 6 while input_rate / (decimation + 1) >= output_rate * correction: decimation += 1 fraction = float(input_rate / decimation) / output_rate diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js index 8192a04..bd0e579 100644 --- a/htdocs/lib/Demodulator.js +++ b/htdocs/lib/Demodulator.js @@ -8,7 +8,7 @@ Filter.prototype.getLimits = function() { if (this.demodulator.get_secondary_demod() === 'pocsag') { max_bw = 12500; } else if (this.demodulator.get_modulation() === 'wfm') { - max_bw = 80000; + max_bw = 100000; } else if (this.demodulator.get_modulation() === 'drm') { max_bw = 100000; } else if (this.demodulator.get_secondary_demod() === 'packet') { diff --git a/owrx/modes.py b/owrx/modes.py index a9ce30f..0fcf366 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -54,7 +54,7 @@ class DigitalMode(Mode): class Modes(object): mappings = [ AnalogMode("nfm", "FM", bandpass=Bandpass(-4000, 4000)), - AnalogMode("wfm", "WFM", bandpass=Bandpass(-50000, 50000)), + AnalogMode("wfm", "WFM", bandpass=Bandpass(-75000, 75000)), AnalogMode("am", "AM", bandpass=Bandpass(-4000, 4000)), AnalogMode("lsb", "LSB", bandpass=Bandpass(-3000, -300)), AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)), From a65f15869becc52eb3ac44297310f00e1676b0c2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 15 Jan 2021 00:11:20 +0100 Subject: [PATCH 19/42] add wsprnet metrics --- owrx/wsprnet.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/owrx/wsprnet.py b/owrx/wsprnet.py index 5ba8535..b1b8cd5 100644 --- a/owrx/wsprnet.py +++ b/owrx/wsprnet.py @@ -2,6 +2,7 @@ from owrx.reporting import Reporter from owrx.version import openwebrx_version from owrx.config import Config from owrx.locator import Locator +from owrx.metrics import Metrics, CounterMetric from queue import Queue, Full from urllib import request, parse import threading @@ -10,6 +11,7 @@ from datetime import datetime, timezone logger = logging.getLogger(__name__) + class Worker(threading.Thread): def __init__(self, queue: Queue): self.queue = queue @@ -62,12 +64,18 @@ class WsprnetReporter(Reporter): # single worker Worker(self.queue).start() + # metrics + metrics = Metrics.getSharedInstance() + self.spotCounter = CounterMetric() + metrics.addMetric("wsprnet.spots", self.spotCounter) + def stop(self): pass def spot(self, spot): try: self.queue.put(spot, block=False) + self.spotCounter.inc() except Full: logger.warning("WSPRNet Queue overflow, one spot lost") From 885e361bab496040980d4de2f9329fa286ddfb8a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 15 Jan 2021 16:19:45 +0100 Subject: [PATCH 20/42] implement reporting of FST4W spots (in theory) --- htdocs/lib/MessagePanel.js | 4 +-- owrx/wsjt.py | 54 +++++++++++++++++++++++++------------- owrx/wsprnet.py | 15 ++++++++--- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js index a4c8b40..597475b 100644 --- a/htdocs/lib/MessagePanel.js +++ b/htdocs/lib/MessagePanel.js @@ -78,14 +78,14 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) { return $('
').text(input).html() }; - if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'FST4W'].indexOf(msg['mode']) >= 0) { + if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4'].indexOf(msg['mode']) >= 0) { matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); if (matches && matches[2] !== 'RR73') { linkedmsg = html_escape(matches[1]) + '' + matches[2] + ''; } else { linkedmsg = html_escape(linkedmsg); } - } else if (msg['mode'] === 'WSPR') { + } else if (['WSPR', 'FST4W'].indexOf(msg['mode']) >= 0) { matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/); if (matches) { linkedmsg = html_escape(matches[1]) + '' + matches[2] + '' + html_escape(matches[3]); diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 6d2c800..c432831 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -156,12 +156,17 @@ class WsjtParser(Parser): return mode = profile.getMode() - if mode == "WSPR": - decoder = WsprDecoder(profile) + if mode in ["WSPR", "FST4W"]: + messageParser = BeaconMessageParser() else: - decoder = Jt9Decoder(profile) + messageParser = QsoMessageParser() + if mode == "WSPR": + decoder = WsprDecoder(profile, messageParser) + else: + decoder = Jt9Decoder(profile, messageParser) out = decoder.parse(msg, freq) out["mode"] = mode + out["interval"] = profile.getInterval() self.pushDecode(mode) if "callsign" in out and "locator" in out: @@ -195,10 +200,9 @@ class WsjtParser(Parser): class Decoder(ABC): - locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$") - - def __init__(self, profile): + def __init__(self, profile, messageParser): self.profile = profile + self.messageParser = messageParser def parse_timestamp(self, instring): dateformat = self.profile.getTimestampFormat() @@ -215,8 +219,19 @@ class Decoder(ABC): def parse(self, msg, dial_freq): pass - def parseMessage(self, msg): - m = Decoder.locator_pattern.match(msg) + +class MessageParser(ABC): + @abstractmethod + def parse(self, msg): + pass + + +# Used in QSO-style modes (FT8, FT4, FST4) +class QsoMessageParser(MessageParser): + locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$") + + def parse(self, msg): + m = QsoMessageParser.locator_pattern.match(msg) if m is None: return {} # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very @@ -226,6 +241,17 @@ class Decoder(ABC): return {"callsign": m.group(1), "locator": m.group(3)} +# Used in propagation reporting / beacon modes (WSPR / FST4W) +class BeaconMessageParser(MessageParser): + wspr_splitter_pattern = re.compile("([A-Z0-9/]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") + + def parse(self, msg): + m = BeaconMessageParser.wspr_splitter_pattern.match(msg) + if m is None: + return {} + return {"callsign": m.group(1), "locator": m.group(2), "dbm": m.group(3)} + + class Jt9Decoder(Decoder): def parse(self, msg, dial_freq): # ft8 sample @@ -245,13 +271,11 @@ class Jt9Decoder(Decoder): "freq": dial_freq + int(msg[9:13]), "msg": wsjt_msg, } - result.update(self.parseMessage(wsjt_msg)) + result.update(self.messageParser.parse(wsjt_msg)) return result class WsprDecoder(Decoder): - 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' @@ -266,11 +290,5 @@ class WsprDecoder(Decoder): "drift": int(msg[20:23]), "msg": wsjt_msg, } - result.update(self.parseMessage(wsjt_msg)) + result.update(self.messageParser.parse(wsjt_msg)) return result - - def parseMessage(self, msg): - m = WsprDecoder.wspr_splitter_pattern.match(msg) - if m is None: - return {} - return {"callsign": m.group(1), "locator": m.group(2), "dbm": m.group(3)} diff --git a/owrx/wsprnet.py b/owrx/wsprnet.py index b1b8cd5..35bbe44 100644 --- a/owrx/wsprnet.py +++ b/owrx/wsprnet.py @@ -32,6 +32,13 @@ class Worker(threading.Thread): except Exception: logger.exception("Exception while uploading WSPRNet spot") + def _getMode(self, spot): + interval = round(spot["interval"] / 60) + # FST4W modes are mapped not to conflict with WSPR modes 2 and 15: + if spot["mode"] != "WSPR" and interval in [2, 15]: + return interval + 1 + return interval + def uploadSpot(self, spot): # function=wspr&date=210114&time=1732&sig=-15&dt=0.5&drift=0&tqrg=7.040019&tcall=DF2UU&tgrid=JN48&dbm=37&version=2.3.0-rc3&rcall=DD5JFK&rgrid=JN58SC&rqrg=7.040047&mode=2 # {'timestamp': 1610655960000, 'db': -23.0, 'dt': 0.3, 'freq': 7040048, 'drift': -1, 'msg': 'LA3JJ JO59 37', 'callsign': 'LA3JJ', 'locator': 'JO59', 'mode': 'WSPR'} @@ -42,7 +49,8 @@ class Worker(threading.Thread): "time": date.strftime("%H%M"), "sig": spot["db"], "dt": spot["dt"], - "drift": spot["drift"], + # FST4W does not have drift + "drift": spot["drift"] if "drift" in spot else 0, "tqrg": spot["freq"] / 1E6, "tcall": spot["callsign"], "tgrid": spot["locator"], @@ -51,8 +59,7 @@ class Worker(threading.Thread): "rcall": self.callsign, "rgrid": self.locator, # mode 2 = WSPR 2 minutes - # TODO implement FST4W mode codes - "mode": 2 + "mode": self._getMode(spot) }).encode() request.urlopen("http://wsprnet.org/post/", data) @@ -80,4 +87,4 @@ class WsprnetReporter(Reporter): logger.warning("WSPRNet Queue overflow, one spot lost") def getSupportedModes(self): - return ["WSPR"] + return ["WSPR", "FST4W"] From 966a40470066fd715064e9a0a1b4458733a2499c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 15 Jan 2021 16:27:15 +0100 Subject: [PATCH 21/42] don't spot FST4W on pskreporter (same as WSPR?) --- owrx/pskreporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 2b8f69f..533a251 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -18,7 +18,7 @@ class PskReporter(Reporter): interval = 300 def getSupportedModes(self): - return ["FT8", "FT4", "JT9", "JT65", "FST4", "FST4W", "JS8"] + return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8"] def stop(self): self.cancelTimer() From b9f0c91ced256a212c7c8626580e96eb74394ce4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 15 Jan 2021 16:28:38 +0100 Subject: [PATCH 22/42] update changelog --- CHANGELOG.md | 2 +- debian/changelog | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e21e2ce..f3df74f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ - Removed `port` configuration option; `rtltcp_compat` takes the port number with the new connectors - Added support for new WSJT-X modes FST4 and FST4W (only available with WSJT-X 2.3) - Added support for demodulating M17 digital voice signals using m17-cxx-demod -- New reporting infrastructur, allowing WSPR spots to be sent to wsprnet.org +- New reporting infrastructur, allowing WSPR and FST4W spots to be sent to wsprnet.org - New devices supported: - HPSDR devices (Hermes Lite 2) - BBRF103 / RX666 / RX888 devices supported by libsddc diff --git a/debian/changelog b/debian/changelog index b953785..3f4e41b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -7,7 +7,8 @@ openwebrx (0.21.0) UNRELEASED; urgency=low WSJT-X 2.3) * Added support for demodulating M17 digital voice signals using m17-cxx-demod - * New reporting infrastructur, allowing WSPR spots to be sent to wsprnet.org + * New reporting infrastructur, allowing WSPR and FST4W spots to be sent to + wsprnet.org * New devices supported: - HPSDR devices (Hermes Lite 2) (`"type": "hpsdr"`) - BBRF103 / RX666 / RX888 devices supported by libsddc (`"type": "sddc"`) From a8ef3a0e6a6837554049018a2b4022d37082e966 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 15 Jan 2021 18:09:18 +0100 Subject: [PATCH 23/42] get rid of the e() function --- htdocs/css/openwebrx.css | 8 +++---- htdocs/index.html | 2 +- htdocs/openwebrx.js | 46 +++++++++++++++++----------------------- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index c2e87fc..689d0bc 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -1233,12 +1233,12 @@ img.openwebrx-mirror-img height: 15px; } -#openwebrx-mute-on .sprite-speaker { - background-position: -117px -38px; +.openwebrx-mute-button .sprite-speaker { + background-position: -103px -38px; } -#openwebrx-mute-off .sprite-speaker { - background-position: -103px -38px; +.openwebrx-mute-button.muted .sprite-speaker { + background-position: -117px -38px; } .sprite-squelch { diff --git a/htdocs/index.html b/htdocs/index.html index 3932923..61df568 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -132,7 +132,7 @@
-
+
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 05bce97..cbef892 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -32,26 +32,20 @@ var fft_codec; var waterfall_setup_done = 0; var secondary_fft_size; -function e(what) { - return document.getElementById(what); -} - function updateVolume() { - audioEngine.setVolume(parseFloat(e("openwebrx-panel-volume").value) / 100); + audioEngine.setVolume(parseFloat($("#openwebrx-panel-volume").val()) / 100); } function toggleMute() { - if (mute) { - mute = false; - e("openwebrx-mute-on").id = "openwebrx-mute-off"; - e("openwebrx-panel-volume").disabled = false; - e("openwebrx-panel-volume").value = volumeBeforeMute; + var $muteButton = $('.openwebrx-mute-button'); + var $volumePanel = $('#openwebrx-panel-volume'); + if ($muteButton.hasClass('muted')) { + $muteButton.removeClass('muted'); + $volumePanel.prop('disabled', false).val(volumeBeforeMute); } else { - mute = true; - e("openwebrx-mute-off").id = "openwebrx-mute-on"; - e("openwebrx-panel-volume").disabled = true; - volumeBeforeMute = e("openwebrx-panel-volume").value; - e("openwebrx-panel-volume").value = 0; + $muteButton.addClass('muted'); + volumeBeforeMute = $volumePanel.val(); + $volumePanel.prop('disabled', true).val(0); } updateVolume(); @@ -191,7 +185,7 @@ function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch var highLevel = waterfall_max_level + 20; var percent = (logValue - lowLevel) / (highLevel - lowLevel); setSmeterRelativeValue(percent); - e("openwebrx-smeter-db").innerHTML = logValue.toFixed(1) + " dB"; + $("#openwebrx-smeter-db").html(logValue.toFixed(1) + " dB"); } function typeInAnimation(element, timeout, what, onFinish) { @@ -244,14 +238,14 @@ var scale_ctx; var scale_canvas; function scale_setup() { - scale_canvas = e("openwebrx-scale-canvas"); + scale_canvas = $("#openwebrx-scale-canvas")[0]; scale_ctx = scale_canvas.getContext("2d"); scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false); scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false); scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false); resize_scale(); - var frequency_container = e("openwebrx-frequency-container"); - frequency_container.addEventListener("mousemove", frequency_container_mousemove, false); + var frequency_container = $("#openwebrx-frequency-container"); + frequency_container.on("mousemove", frequency_container_mousemove, false); } var scale_canvas_drag_params = { @@ -784,10 +778,10 @@ function on_ws_recv(evt) { $('#openwebrx-bar-clients').progressbar().setClients(json['value']); break; case "profiles": - var listbox = e("openwebrx-sdr-profiles-listbox"); - listbox.innerHTML = json['value'].map(function (profile) { + var listbox = $("#openwebrx-sdr-profiles-listbox"); + listbox.html(json['value'].map(function (profile) { return '"; - }).join(""); + }).join("")); if (currentprofile) { $('#openwebrx-sdr-profiles-listbox').val(currentprofile); } @@ -1019,7 +1013,7 @@ function divlog(what, is_error) { what = "" + what + ""; toggle_panel("openwebrx-panel-log", true); //show panel if any error is present } - e("openwebrx-debugdiv").innerHTML += what + "
"; + $('#openwebrx-debugdiv')[0].innerHTML += what + "
"; var nano = $('.nano'); nano.nanoScroller(); nano.nanoScroller({scroll: 'bottom'}); @@ -1145,14 +1139,14 @@ function add_canvas() { function init_canvas_container() { - canvas_container = e("webrx-canvas-container"); + canvas_container = $("#webrx-canvas-container")[0]; canvas_container.addEventListener("mouseleave", canvas_container_mouseleave, false); canvas_container.addEventListener("mousemove", canvas_mousemove, false); canvas_container.addEventListener("mouseup", canvas_mouseup, false); canvas_container.addEventListener("mousedown", canvas_mousedown, false); canvas_container.addEventListener("wheel", canvas_mousewheel, false); - var frequency_container = e("openwebrx-frequency-container"); - frequency_container.addEventListener("wheel", canvas_mousewheel, false); + var frequency_container = $("#openwebrx-frequency-container"); + frequency_container.on("wheel", canvas_mousewheel, false); add_canvas(); } From a31b24692416e300c3239af369bb5bb84bde7151 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 15 Jan 2021 19:06:00 +0100 Subject: [PATCH 24/42] restructure header --- htdocs/css/openwebrx-header.css | 131 ++++++++++++----------------- htdocs/include/header.include.html | 39 +++++---- htdocs/lib/Header.js | 8 +- 3 files changed, 74 insertions(+), 104 deletions(-) diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css index b22da87..e2b094f 100644 --- a/htdocs/css/openwebrx-header.css +++ b/htdocs/css/openwebrx-header.css @@ -1,32 +1,36 @@ -#webrx-top-container -{ +#webrx-top-container { position: relative; z-index:1000; background-color: #575757; -} -#webrx-top-photo -{ - width: 100%; - display: block; -} + background-image: url(../gfx/openwebrx-top-photo.jpg); + background-position-x: center; + background-position-y: top; + background-repeat: no-repeat; + background-size: cover; -#webrx-top-photo-clip -{ - min-height: 67px; - max-height: 67px; - height: 350px; overflow: hidden; - position: relative; } -.webrx-top-bar-parts -{ +#openwebrx-description-container { + transition-property: height, opacity; + transition-duration: 1s; + transition-timing-function: ease-out; + opacity: 0; + height: 0; + /* originally, top-bar + description was 350px */ + max-height: 283px; + overflow: hidden; +} + +#openwebrx-description-container.expanded { + opacity: 1; + height: 283px; +} + +.webrx-top-bar-parts { height:67px; -} -#webrx-top-bar -{ background: rgba(128, 128, 128, 0.15); margin:0; padding:0; @@ -37,31 +41,25 @@ -moz-user-select: none; -ms-user-select: none; overflow: hidden; - position: absolute; - left: 0; - top: 0; - right: 0; + + display: flex; } -#webrx-tob-container, #webrx-top-container * { +.webrx-top-bar-parts > * { + flex: 0; +} + +#webrx-top-container, #webrx-top-container * { line-height: initial; box-sizing: initial; } -#webrx-top-container img { - vertical-align: initial; -} - -#webrx-top-logo -{ +#webrx-top-logo { padding: 12px; - float: left; } -#webrx-rx-avatar -{ +#webrx-rx-avatar { background-color: rgba(154, 154, 154, .5); - float: left; margin: 7px; cursor:pointer; @@ -73,49 +71,37 @@ } #webrx-rx-texts { - float: left; + flex: 1; padding: 10px; } #webrx-rx-texts div { padding: 3px; -} - -#webrx-rx-title -{ white-space:nowrap; overflow: hidden; cursor:pointer; - font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; color: #909090; +} + +#webrx-rx-title { + font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; font-size: 11pt; font-weight: bold; } -#webrx-rx-desc -{ - white-space:nowrap; - overflow: hidden; - cursor:pointer; +#webrx-rx-desc { font-size: 10pt; - color: #909090; } -#webrx-rx-desc a -{ - color: #909090; -} - -#openwebrx-rx-details-arrow -{ +#openwebrx-rx-details-arrow { cursor:pointer; position: absolute; - left: 470px; - top: 55px; + bottom: 0; + left: 50%; + transform: translate(-50%, 0); } -#openwebrx-rx-details-arrow a -{ +#openwebrx-rx-details-arrow a { margin: 0; padding: 0; line-height: 0; @@ -137,23 +123,19 @@ text-decoration: inherit; } -#openwebrx-main-buttons .button:hover -{ +#openwebrx-main-buttons .button:hover { background-color: rgba(255, 255, 255, 0.3); } -#openwebrx-main-buttons .button:active -{ +#openwebrx-main-buttons .button:active { background-color: rgba(255, 255, 255, 0.55); } -#openwebrx-main-buttons -{ +#openwebrx-main-buttons { padding: 5px 15px; display: flex; list-style: none; - float: right; margin:0; color: white; text-shadow: 0px 0px 4px #000000; @@ -162,23 +144,17 @@ font-weight: bold; } -#webrx-rx-photo-title -{ - position: absolute; - left: 15px; - top: 78px; - color: White; +#webrx-rx-photo-title { + margin: 10px 15px; + color: white; font-size: 16pt; text-shadow: 1px 1px 4px #444; opacity: 1; } -#webrx-rx-photo-desc -{ - position: absolute; - left: 15px; - top: 109px; - color: White; +#webrx-rx-photo-desc { + margin: 10px 15px; + color: white; font-size: 10pt; font-weight: bold; text-shadow: 0px 0px 6px #444; @@ -186,8 +162,7 @@ line-height: 1.5em; } -#webrx-rx-photo-desc a -{ +#webrx-rx-photo-desc a { color: #5ca8ff; text-shadow: none; } diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html index c66aed1..ec31619 100644 --- a/htdocs/include/header.include.html +++ b/htdocs/include/header.include.html @@ -1,26 +1,25 @@
-
- Receiver panorama -
- - Receiver avatar -
-
-
-
-
- - -
-
-

Status
-

Log
-

Receiver
-
Map
- ${settingslink} -
+
+ + Receiver avatar +
+
+
+
+

Status
+

Log
+

Receiver
+
Map
+ ${settingslink} +
+
+
+
+ + +
diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js index cd3f4bf..469df1e 100644 --- a/htdocs/lib/Header.js +++ b/htdocs/lib/Header.js @@ -30,18 +30,14 @@ Header.prototype.init_rx_photo = function() { Header.prototype.close_rx_photo = function() { this.rx_photo_state = 0; - this.el.find("#webrx-rx-photo-desc").animate({opacity: 0}); - this.el.find("#webrx-rx-photo-title").animate({opacity: 0}); - this.el.find('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'}); + this.el.find('#openwebrx-description-container').removeClass('expanded'); this.el.find("#openwebrx-rx-details-arrow-down").show(); this.el.find("#openwebrx-rx-details-arrow-up").hide(); } Header.prototype.open_rx_photo = function() { this.rx_photo_state = 1; - this.el.find("#webrx-rx-photo-desc").animate({opacity: 1}); - this.el.find("#webrx-rx-photo-title").animate({opacity: 1}); - this.el.find('#webrx-top-photo-clip').animate({maxHeight: 350}, {duration: 1000, easing: 'easeOutCubic'}); + this.el.find('#openwebrx-description-container').addClass('expanded'); this.el.find("#openwebrx-rx-details-arrow-down").hide(); this.el.find("#openwebrx-rx-details-arrow-up").show(); } From 163ebcd327284033d122de8e1f0d869096cdb2e3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 15 Jan 2021 19:33:55 +0100 Subject: [PATCH 25/42] actually position text in the center --- htdocs/css/openwebrx.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 689d0bc..2ed969a 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -573,9 +573,10 @@ img.openwebrx-mirror-img .openwebrx-progressbar-text { position: absolute; - left:0px; - top:4px; - width: inherit; + left:50; + top:50%; + transform: translate(-50%, -50%); + white-space: nowrap; z-index: 1; } From 00631d7349c7fd87d0ec8f430a393a538699994c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 15 Jan 2021 19:43:16 +0100 Subject: [PATCH 26/42] hide map overlay until map is loaded --- htdocs/css/map.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/htdocs/css/map.css b/htdocs/css/map.css index de6ef3e..44601e0 100644 --- a/htdocs/css/map.css +++ b/htdocs/css/map.css @@ -25,12 +25,19 @@ ul { padding-inline-start: 25px; } +/* don't show the filter in it's initial position */ .openwebrx-map-legend { + display: none; background-color: #fff; padding: 10px; margin: 10px; } +/* show it as soon as google maps has moved it to its container */ +.openwebrx-map .openwebrx-map-legend { + display: block; +} + .openwebrx-map-legend ul { list-style-type: none; padding: 0; From ae217f9deda687a9f5e0a4142a3d22b369af227c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 15 Jan 2021 19:55:37 +0100 Subject: [PATCH 27/42] specify flex-direction explicitly --- htdocs/css/openwebrx-header.css | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css index e2b094f..c110f9c 100644 --- a/htdocs/css/openwebrx-header.css +++ b/htdocs/css/openwebrx-header.css @@ -43,6 +43,7 @@ overflow: hidden; display: flex; + flex-direction: row; } .webrx-top-bar-parts > * { From 992a5c33a223d285dbf94cf3cb32b0c729ef7ee5 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 16 Jan 2021 15:45:33 +0100 Subject: [PATCH 28/42] check for keys' existence --- owrx/reporting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/owrx/reporting.py b/owrx/reporting.py index d6e0831..faa427c 100644 --- a/owrx/reporting.py +++ b/owrx/reporting.py @@ -37,11 +37,11 @@ class ReportingEngine(object): def __init__(self): self.reporters = [] config = Config.get() - if config["pskreporter_enabled"]: + if "pskreporter_enabled" in config and config["pskreporter_enabled"]: # inline import due to circular dependencies from owrx.pskreporter import PskReporter self.reporters += [PskReporter()] - if config["wsprnet_enabled"]: + if "wsprnet_enabled" in config and config["wsprnet_enabled"]: # inline import due to circular dependencies from owrx.wsprnet import WsprnetReporter self.reporters += [WsprnetReporter()] From 9f702f5d1460ebdc35a5b271c9f4e0a5c3b90692 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 16 Jan 2021 17:34:17 +0100 Subject: [PATCH 29/42] let's try to make the header somewhat responsive --- htdocs/css/openwebrx-header.css | 38 ++++++++++++++++++++++++++++++++- htdocs/map.html | 3 +++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css index c110f9c..f28b52e 100644 --- a/htdocs/css/openwebrx-header.css +++ b/htdocs/css/openwebrx-header.css @@ -57,6 +57,8 @@ #webrx-top-logo { padding: 12px; + /* overwritten by media queries */ + display: none; } #webrx-rx-avatar { @@ -72,11 +74,15 @@ } #webrx-rx-texts { + /* minimum layout width */ + width: 0; + /* will be getting wider with flex */ flex: 1; - padding: 10px; + overflow: hidden; } #webrx-rx-texts div { + margin: 0 10px; padding: 3px; white-space:nowrap; overflow: hidden; @@ -84,6 +90,10 @@ color: #909090; } +#webrx-rx-texts div:first-child { + margin-top: 10px; +} + #webrx-rx-title { font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; font-size: 11pt; @@ -168,6 +178,32 @@ text-shadow: none; } +/* + * Responsive stuff + */ + +@media (min-width: 576px) { + #webrx-rx-texts { + display: initial; + } +} + +@media (min-width: 768px) { +} + +@media (min-width: 992px) { + #webrx-top-logo { + display: initial; + } +} + +@media (min-width: 1200px) { +} + +/* + * Sprites (images) + */ + .sprite-panel-status { background-position: 0 0; width: 44px; diff --git a/htdocs/map.html b/htdocs/map.html index 08e40b4..f93d2e2 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -2,6 +2,9 @@ OpenWebRX Map + + + From 13215960c44ce8a3f272e16b4b39635f10f61f37 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 16 Jan 2021 18:06:37 +0100 Subject: [PATCH 30/42] show header buttons conditionally --- htdocs/css/openwebrx-header.css | 5 +++++ htdocs/lib/Header.js | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css index f28b52e..83061f3 100644 --- a/htdocs/css/openwebrx-header.css +++ b/htdocs/css/openwebrx-header.css @@ -125,6 +125,11 @@ cursor:pointer; } +#openwebrx-main-buttons .button[data-toggle-panel] { + /* will be enabled by javascript if the panel is present in the DOM */ + display: none; +} + #openwebrx-main-buttons .button img { height: 38px; } diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js index 469df1e..746c282 100644 --- a/htdocs/lib/Header.js +++ b/htdocs/lib/Header.js @@ -1,7 +1,12 @@ function Header(el) { this.el = el; - this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () { + var $buttons = this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').filter(function(){ + // ignore buttons when the corresponding panel is not in the DOM + return $('#' + $(this).data('toggle-panel'))[0]; + }); + + $buttons.css({display: 'block'}).click(function () { toggle_panel($(this).data('toggle-panel')); }); From 41f94070246bcfd24c01c420ec8db94d577d4208 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 16 Jan 2021 19:40:22 +0100 Subject: [PATCH 31/42] re-package code for meta panels into classes --- htdocs/css/openwebrx.css | 7 +- htdocs/index.html | 42 ++++++------ htdocs/lib/DemodulatorPanel.js | 7 +- htdocs/lib/MetaPanel.js | 119 +++++++++++++++++++++++++++++++++ htdocs/openwebrx.js | 77 ++------------------- owrx/controllers/assets.py | 1 + 6 files changed, 156 insertions(+), 97 deletions(-) create mode 100644 htdocs/lib/MetaPanel.js diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 2ed969a..7111e02 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -908,10 +908,15 @@ img.openwebrx-mirror-img border-color: Red; } +.openwebrx-meta-panel { + display: flex; + flex-direction: row; +} + .openwebrx-meta-slot { + flex: 1; width: 145px; height: 196px; - float: left; margin-right: 10px; background-color: #676767; diff --git a/htdocs/index.html b/htdocs/index.html index 61df568..742cfab 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -65,32 +65,28 @@