diff --git a/CHANGELOG.md b/CHANGELOG.md index a583fd0f..041f527a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Added support for the MSK144 digimode - Added support for decoding ADS-B with dump1090 - Added support for decoding HFDL and VDL2 aircraft communications +- Added support for decoding JT4 modes - Added decoding of ISM band transmissions using rtl_433 - Added support for decoding RDS data on WFM broadcasts using redsea decoder - Added decoding for DAB broadcast stations using csdr-eti and dablin diff --git a/README.md b/README.md index f8ed6b87..ebd29d1a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It has the following features: - Multiple SDR devices can be used simultaneously - [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag, D-Star, NXDN) - [wsjt-x](https://wsjt.sourceforge.io/) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4, - FST4W) + FST4W, JT4) - [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets - [JS8Call](http://js8call.com/) support - [DRM](https://github.com/jketterl/openwebrx/wiki/DRM-demodulator-notes) support diff --git a/debian/changelog b/debian/changelog index aa95d2f4..dcdbd1cc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -4,6 +4,7 @@ openwebrx (1.3.0) UNRELEASED; urgency=low * Added support for the MSK144 digimode * Added support for decoding ADS-B with dump1090 * Added support for decoding HFDL and VDL2 aircraft communications + * Added support for decoding JT4 modes * Added decoding of ISM band transmissions using rtl_433 * Added support for decoding RDS data on WFM broadcasts using redsea decoder * Added decoding for DAB broadcast stations using csdr-eti and dablin diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index 6f35e6b7..fda0929d 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -167,7 +167,7 @@ DemodulatorPanel.prototype.updatePanels = function() { var mode = Modes.findByModulation(modulation); toggle_panel("openwebrx-panel-digimodes", modulation && (!mode || mode.secondaryFft)); // WSJT-X modes share the same panel - toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65", "msk144"].indexOf(modulation) >= 0); + toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'jt4', 'ft4', 'fst4', 'fst4w', "q65", "msk144"].indexOf(modulation) >= 0); // these modes come with their own ['js8', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl', 'vdl2'].forEach(function(m) { toggle_panel('openwebrx-panel-' + m + '-message', modulation === m); @@ -382,4 +382,4 @@ $.fn.demodulatorPanel = function(){ this.data('panel', new DemodulatorPanel(this)); } return this.data('panel'); -}; \ No newline at end of file +}; diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js index 0bb2d5ac..ac3e089a 100644 --- a/htdocs/lib/MessagePanel.js +++ b/htdocs/lib/MessagePanel.js @@ -59,7 +59,7 @@ MessagePanel.prototype.scrollToBottom = function() { function WsjtMessagePanel(el) { MessagePanel.call(this, el); this.initClearTimer(); - this.qsoModes = ['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65', 'MSK144']; + this.qsoModes = ['FT8', 'JT65', 'JT9', 'JT4', 'FT4', 'FST4', 'Q65', 'MSK144']; this.beaconModes = ['WSPR', 'FST4W']; this.modes = [].concat(this.qsoModes, this.beaconModes); } @@ -809,4 +809,4 @@ $.fn.vdl2MessagePanel = function() { this.data('panel', new Vdl2MessagePanel(this)); } return this.data('panel'); -}; \ No newline at end of file +}; diff --git a/owrx/config/defaults.py b/owrx/config/defaults.py index 3f9be5d0..0f97a199 100644 --- a/owrx/config/defaults.py +++ b/owrx/config/defaults.py @@ -155,6 +155,7 @@ wsjt_decoding_depths=PropertyLayer(jt65=1), fst4_enabled_intervals=[15, 30], fst4w_enabled_intervals=[120, 300], + jt4_enabled_submodes=["F", "G"], q65_enabled_combinations=["A30", "E120", "C60"], js8_enabled_profiles=["normal", "slow"], js8_decoding_depth=3, diff --git a/owrx/controllers/settings/decoding.py b/owrx/controllers/settings/decoding.py index 480b0e9b..24f32ae8 100644 --- a/owrx/controllers/settings/decoding.py +++ b/owrx/controllers/settings/decoding.py @@ -4,7 +4,7 @@ from owrx.form.input.wfm import WfmTauValues from owrx.form.input.wsjt import Q65ModeMatrix, WsjtDecodingDepthsInput from owrx.form.input.converter import OptionalConverter -from owrx.wsjt import Fst4Profile, Fst4wProfile +from owrx.wsjt import Fst4Profile, Fst4wProfile, JT4Profile from owrx.breadcrumb import Breadcrumb, BreadcrumbItem @@ -90,6 +90,11 @@ def getSections(self): "Enabled FST4W intervals", [Option(v, "{}s".format(v)) for v in Fst4wProfile.availableIntervals], ), + MultiCheckboxInput( + "jt4_enabled_submodes", + "Enabled JT4 Submodes", + [Option(v, "{}".format(v)) for v in JT4Profile.availableSubmodes], + ), Q65ModeMatrix("q65_enabled_combinations", "Enabled Q65 Mode combinations"), ), ] diff --git a/owrx/dsp.py b/owrx/dsp.py index 625254ea..08eefc5f 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -611,7 +611,7 @@ def setDemodulator(self, mod): def _getSecondaryDemodulator(self, mod) -> Optional[SecondaryDemodulator]: if isinstance(mod, SecondaryDemodulator): return mod - if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: + if mod in ["ft8", "wspr", "jt65", "jt9", "jt4", "ft4", "fst4", "fst4w", "q65"]: from csdr.chain.digimodes import AudioChopperDemodulator from owrx.wsjt import WsjtParser return AudioChopperDemodulator(mod, WsjtParser()) diff --git a/owrx/modes.py b/owrx/modes.py index f5ea047e..14e7e542 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -147,6 +147,7 @@ class Modes(object): WsjtMode("wspr", "WSPR", bandpass=Bandpass(1350, 1650)), WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]), WsjtMode("fst4w", "FST4W", bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"]), + WsjtMode("jt4", "JT4"), WsjtMode("q65", "Q65", requirements=["wsjt-x-2-4"]), DigitalMode("msk144", "MSK144", requirements=["msk144"], underlying=["usb"], service=True), Js8Mode("js8", "JS8Call"), diff --git a/owrx/reporting/pskreporter.py b/owrx/reporting/pskreporter.py index 76a93d6b..ca50b179 100644 --- a/owrx/reporting/pskreporter.py +++ b/owrx/reporting/pskreporter.py @@ -29,7 +29,7 @@ def getSupportedModes(self): Current version at the time of the last change: https://www.adif.org/314/ADIF_314.htm#Mode_Enumeration """ - return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W", "MSK144"] + return ["FT8", "FT4", "JT9", "JT4", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W", "MSK144"] def stop(self): self.cancelTimer() diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py index d78b03e3..813b42ce 100644 --- a/owrx/service/__init__.py +++ b/owrx/service/__init__.py @@ -298,7 +298,7 @@ def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]): def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]: if isinstance(mod, ServiceDemodulatorChain): return mod - if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: + if mod in ["ft8", "wspr", "jt65", "jt9", "jt4", "ft4", "fst4", "fst4w", "q65"]: from csdr.chain.digimodes import AudioChopperDemodulator from owrx.wsjt import WsjtParser return AudioChopperDemodulator(mod, WsjtParser()) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index b1fdd9c9..38ce7c8f 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -62,6 +62,16 @@ def getProfiles(self) -> List[AudioChopperProfile]: return [Fst4wProfile(i) for i in profiles if i in Fst4wProfile.availableIntervals] +class JT4ProfileSource(ConfigWiredProfileSource): + def getPropertiesToWire(self) -> List[str]: + return ["jt4_enabled_submodes"] + + def getProfiles(self) -> List[AudioChopperProfile]: + config = Config.get() + profiles = config["jt4_enabled_submodes"] if "jt4_enabled_submodes" in config else [] + return [JT4Profile(i) for i in profiles if i in JT4Profile.availableSubmodes] + + class Q65ProfileSource(ConfigWiredProfileSource): def getPropertiesToWire(self) -> List[str]: return ["q65_enabled_combinations"] @@ -102,6 +112,8 @@ def getSource(mode: str): return Fst4ProfileSource() elif mode == "fst4w": return Fst4wProfileSource() + elif mode == "jt4": + return JT4ProfileSource() elif mode == "q65": return Q65ProfileSource() @@ -197,6 +209,25 @@ def getMode(self): return "FST4W" +class JT4Profile(WsjtProfile): + availableSubmodes = ["A", "B", "C", "D", "E", "F", "G"] + + def __init__(self, submode): + self.submode = submode + + def getInterval(self): + return 60 + + def getSubmode(self): + return self.submode + + def decoder_commandline(self, file): + return ["jt9", "-4", "-b", str(self.submode), "-d", str(self.decoding_depth()), file] + + def getMode(self): + return "JT4" + + class Q65Mode(Enum): # value is the bandwidth multiplier according to https://physics.princeton.edu/pulsar/k1jt/Q65_Quick_Start.pdf A = 1