diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a5e3bb..8d258e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ **unreleased** - Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level - 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 new WSJT-X modes FST4, FST4W (only available with WSJT-X 2.3) and Q65 (only avilable with + WSJT-X 2.4) - Added support for demodulating M17 digital voice signals using m17-cxx-demod - New reporting infrastructure, allowing WSPR and FST4W spots to be sent to wsprnet.org - Add some basic filtering capabilities to the map diff --git a/config_webrx.py b/config_webrx.py index b5b37a8..f85f2d5 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -318,6 +318,10 @@ fst4_enabled_intervals = [15, 30] # available values (in seconds): 120, 300, 900, 1800 fst4w_enabled_intervals = [120, 300] +# Q65 allows many combinations of intervals and submodes. This setting determines which combinations will be decoded. +# Please use python tuples of (interval: int, mode: str) to specify the combinations. For example: +q65_enabled_combinations = [(30, "A"), (120, "E"), (60, "C")] + # JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled. js8_enabled_profiles = ["normal", "slow"] # JS8 decoding depth; higher value will get more results, but will also consume more cpu diff --git a/csdr/csdr.py b/csdr/csdr.py index 7ea2690..7e50267 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -29,7 +29,7 @@ import math from functools import partial from owrx.kiss import KissClient, DirewolfConfig -from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile, Fst4Profile, Fst4wProfile +from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile, Fst4Profile, Fst4wProfile, Q65Profile from owrx.js8 import Js8Profiles from owrx.audio import AudioChopper @@ -433,6 +433,8 @@ class dsp(object): chopper_profiles = Fst4Profile.getEnabledProfiles() elif smd == "fst4w": chopper_profiles = Fst4wProfile.getEnabledProfiles() + elif smd == "q65": + chopper_profiles = Q65Profile.getEnabledProfiles() if chopper_profiles is not None and len(chopper_profiles): chopper = AudioChopper(self, self.secondary_process_demod.stdout, *chopper_profiles) chopper.start() @@ -573,7 +575,7 @@ class dsp(object): def isWsjtMode(self, demodulator=None): if demodulator is None: demodulator = self.get_secondary_demodulator() - return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w"] + return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"] def isJs8(self, demodulator=None): if demodulator is None: diff --git a/debian/changelog b/debian/changelog index 0bb4e95..549d7b8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -3,8 +3,8 @@ openwebrx (0.21.0) UNRELEASED; urgency=low auto squelch level * 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 new WSJT-X modes FST4, FST4W (only available with WSJT-X + 2.3) and Q65 (only available with WSJT-X 2.4) * Added support for demodulating M17 digital voice signals using m17-cxx-demod * New reporting infrastructure, allowing WSPR and FST4W spots to be sent to diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index bb0bb39..ffd0419 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -1192,6 +1192,7 @@ img.openwebrx-mirror-img #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="q65"] #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, @@ -1201,7 +1202,8 @@ img.openwebrx-mirror-img #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="fst4"] #openwebrx-digimode-select-channel, -#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel +#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-select-channel { display: none; } @@ -1215,7 +1217,8 @@ img.openwebrx-mirror-img #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="fst4"] #openwebrx-digimode-canvas-container, -#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container +#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container { height: 200px; margin: -10px; diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index aed1b17..be3269f 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -158,7 +158,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', 'fst4', 'fst4w'].indexOf(modulation) >= 0); + toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65"].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"); diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js index dc8995e..83ec91d 100644 --- a/htdocs/lib/MessagePanel.js +++ b/htdocs/lib/MessagePanel.js @@ -78,7 +78,7 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) { return $('
').text(input).html() }; - if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4'].indexOf(msg['mode']) >= 0) { + if (['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65'].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] + ''; diff --git a/owrx/modes.py b/owrx/modes.py index f544e19..0df4747 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -98,6 +98,9 @@ class Modes(object): requirements=["wsjt-x-2-3"], service=True, ), + DigitalMode( + "q65", "Q65", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x-2-4"], service=True + ), DigitalMode( "js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True ), diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index 4106648..6f0ee11 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", "JS8"] + return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65"] def stop(self): self.cancelTimer() diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 6d9a919..7903b72 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -7,6 +7,7 @@ from owrx.parser import Parser from owrx.audio import AudioChopperProfile from abc import ABC, ABCMeta, abstractmethod from owrx.config import Config +from enum import Enum import logging @@ -142,6 +143,37 @@ class Fst4wProfile(WsjtProfile): return [Fst4wProfile(i) for i in profiles if i in Fst4wProfile.availableIntervals] +class Q65Mode(Enum): + A = 1 + B = 2 + C = 3 + D = 4 + E = 5 + + +class Q65Profile(WsjtProfile): + availableIntervals = [15, 30, 60, 120, 300] + + def __init__(self, interval, mode: Q65Mode): + self.interval = interval + self.mode = mode + + def getMode(self): + return "Q65" + + def getInterval(self): + return self.interval + + def decoder_commandline(self, file): + return ["jt9", "--q65", "-p", str(self.interval), "-b", self.mode.name, "-d", str(self.decoding_depth()), file] + + @staticmethod + def getEnabledProfiles(): + config = Config.get() + profiles = config["q65_enabled_combinations"] if "q65_enabled_combinations" in config else [] + return [Q65Profile(i, Q65Mode[m]) for i, m in profiles if i in Fst4wProfile.availableIntervals] + + class WsjtParser(Parser): def parse(self, messages): for data in messages: