diff --git a/bands.json b/bands.json index b8d5a6b0..9636471b 100644 --- a/bands.json +++ b/bands.json @@ -192,7 +192,8 @@ "jt9": 50312000, "ft4": 50318000, "js8": 50318000, - "q65": [50211000, 50275000] + "q65": [50211000, 50275000], + "msk144": 50260000 }, "tags": ["hamradio"] }, @@ -201,7 +202,8 @@ "lower_bound": 70150000, "upper_bound": 70200000, "frequencies": { - "wspr": 70091000 + "wspr": 70091000, + "msk144": 70230000 }, "tags": ["hamradio"] }, @@ -215,7 +217,8 @@ "ft4": 144170000, "jt65": 144120000, "packet": [144800000, 145825000], - "q65": 144116000 + "q65": 144116000, + "msk144": 144360000 }, "tags": ["hamradio"] }, @@ -225,7 +228,8 @@ "upper_bound": 440000000, "frequencies": { "pocsag": 439987500, - "q65": 432065000 + "q65": 432065000, + "msk144": 432360000 }, "tags": ["hamradio"] }, diff --git a/csdr/chain/digimodes.py b/csdr/chain/digimodes.py index 75b2f899..a7528f87 100644 --- a/csdr/chain/digimodes.py +++ b/csdr/chain/digimodes.py @@ -1,4 +1,5 @@ from csdr.chain.demodulator import ServiceDemodulator, SecondaryDemodulator, DialFrequencyReceiver, SecondarySelectorChain +from csdr.module.msk144 import Msk144Module, ParserAdapter from owrx.audio.chopper import AudioChopper, AudioChopperParser from owrx.aprs.kiss import KissDeframer from owrx.aprs import Ax25Parser, AprsParser @@ -21,6 +22,23 @@ class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver): self.chopper.setDialFrequency(frequency) +class Msk144Demodulator(ServiceDemodulator, DialFrequencyReceiver): + def __init__(self): + self.parser = ParserAdapter() + workers = [ + Convert(Format.FLOAT, Format.SHORT), + Msk144Module(), + self.parser, + ] + super().__init__(workers) + + def getFixedAudioRate(self) -> int: + return 12000 + + def setDialFrequency(self, frequency: int) -> None: + self.parser.setDialFrequency(frequency) + + class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver): def __init__(self, service: bool = False, ais: bool = False): self.parser = AprsParser() diff --git a/debian/control b/debian/control index c5dddb3d..638feee6 100644 --- a/debian/control +++ b/debian/control @@ -11,6 +11,6 @@ Vcs-Git: https://github.com/luarvique/openwebrx.git Package: openwebrx Architecture: all Depends: adduser, python3 (>= 3.5), python3-pkg-resources, owrx-connector (>= 0.6), soapysdr-tools, python3-csdr (>= 0.18.9), ${python3:Depends}, ${misc:Depends} -Recommends: python3-digiham (>= 0.6), direwolf (>= 1.4), wsjtx, js8call, runds-connector (>= 0.2), hpsdrconnector, aprs-symbols, m17-demod, js8call, python3-js8py (>= 0.1), nmux (>= 0.18), codecserver (>= 0.1) +Recommends: python3-digiham (>= 0.6), direwolf (>= 1.4), wsjtx, js8call, runds-connector (>= 0.2), hpsdrconnector, aprs-symbols, m17-demod, js8call, python3-js8py (>= 0.1), nmux (>= 0.18), codecserver (>= 0.1), msk144decoder Description: multi-user web sdr Open source, multi-user SDR receiver with a web interface diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 19f655f9..6c8169a9 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -60,6 +60,10 @@ mv /wsjtx.patch ${WSJT_DIR} cmakebuild ${WSJT_DIR} rm ${WSJT_TGZ} +git clone https://github.com/alexander-sholohov/msk144decoder.git +# latest from main as of 2023-02-21 +MAKEFLAGS="" cmakebuild msk144decoder fe2991681e455636e258e83c29fd4b2a72d16095 + git clone --depth 1 -b 1.6 https://github.com/wb2osz/direwolf.git cd direwolf # hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need. diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 3a0e4559..b1011f9e 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -1331,6 +1331,7 @@ img.openwebrx-mirror-img #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="msk144"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="sstv"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="fax"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, @@ -1345,6 +1346,7 @@ img.openwebrx-mirror-img #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="q65"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="sstv"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="fax"] #openwebrx-digimode-select-channel { @@ -1363,6 +1365,7 @@ img.openwebrx-mirror-img #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="q65"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="sstv"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="fax"] #openwebrx-digimode-canvas-container { diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index 633bf23a..35cea9c6 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -162,7 +162,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", "q65"].indexOf(modulation) >= 0); + toggle_panel("openwebrx-panel-wsjt-message", ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65", "msk144"].indexOf(modulation) >= 0); toggle_panel("openwebrx-panel-js8-message", modulation == "js8"); toggle_panel("openwebrx-panel-packet-message", ["packet", "ais"].indexOf(modulation) >= 0); toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag"); diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js index aa6bc1f1..a7954fc5 100644 --- a/htdocs/lib/MessagePanel.js +++ b/htdocs/lib/MessagePanel.js @@ -50,7 +50,7 @@ MessagePanel.prototype.initClearButton = function() { function WsjtMessagePanel(el) { MessagePanel.call(this, el); this.initClearTimer(); - this.qsoModes = ['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65']; + this.qsoModes = ['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65', 'MSK144']; this.beaconModes = ['WSPR', 'FST4W']; this.modes = [].concat(this.qsoModes, this.beaconModes); } diff --git a/owrx/dsp.py b/owrx/dsp.py index cf69cbd2..a8bb3eb9 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -600,6 +600,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) from csdr.chain.digimodes import AudioChopperDemodulator from owrx.wsjt import WsjtParser return AudioChopperDemodulator(mod, WsjtParser()) + elif mod == "msk144": + from csdr.chain.digimodes import Msk144Demodulator + return Msk144Demodulator() elif mod == "js8": from csdr.chain.digimodes import AudioChopperDemodulator from owrx.js8 import Js8Parser diff --git a/owrx/feature.py b/owrx/feature.py index acccc98a..6a1dc098 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -80,6 +80,7 @@ class FeatureDetector(object): "wsjt-x": ["wsjtx"], "wsjt-x-2-3": ["wsjtx_2_3"], "wsjt-x-2-4": ["wsjtx_2_4"], + "msk144": ["msk144decoder"], "packet": ["direwolf"], "pocsag": ["digiham"], "js8call": ["js8", "js8py"], @@ -460,6 +461,13 @@ class FeatureDetector(object): """ return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.4")) + def has_msk144decoder(self): + """ + To decode the MSK144 digimode please install the "msk144decoder". See the + [project page](https://github.com/alexander-sholohov/msk144decoder) for more details. + """ + return self.command_is_runnable("msk144decoder") + 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 8b47229b..45b46a33 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -128,6 +128,7 @@ class Modes(object): WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]), WsjtMode("fst4w", "FST4W", bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"]), WsjtMode("q65", "Q65", requirements=["wsjt-x-2-4"]), + DigitalMode("msk144", "MSK144", requirements=["msk144"], underlying=["usb"], service=True), Js8Mode("js8", "JS8Call"), DigitalMode( "packet", diff --git a/owrx/reporting/pskreporter.py b/owrx/reporting/pskreporter.py index a8926ece..bcad4481 100644 --- a/owrx/reporting/pskreporter.py +++ b/owrx/reporting/pskreporter.py @@ -29,7 +29,7 @@ class PskReporter(Reporter): Current version at the time of the last change: https://www.adif.org/312/ADIF_312.htm#Mode_Enumeration """ - return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W"] + return ["FT8", "FT4", "JT9", "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 ff89b2a4..c5dd673f 100644 --- a/owrx/service/__init__.py +++ b/owrx/service/__init__.py @@ -299,6 +299,9 @@ class ServiceHandler(SdrSourceEventClient): from csdr.chain.digimodes import AudioChopperDemodulator from owrx.wsjt import WsjtParser return AudioChopperDemodulator(mod, WsjtParser()) + elif mod == "msk144": + from csdr.chain.digimodes import Msk144Demodulator + return Msk144Demodulator() elif mod == "js8": from csdr.chain.digimodes import AudioChopperDemodulator from owrx.js8 import Js8Parser diff --git a/owrx/wsjt.py b/owrx/wsjt.py index e79abef6..2a23bbbc 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -245,6 +245,17 @@ class Q65Profile(WsjtProfile): return ["jt9", "--q65", "-p", str(self.interval), "-b", self.mode.name, "-d", str(self.decoding_depth()), file] +class Msk144Profile(WsjtProfile): + def getMode(self): + return "MSK144" + + def getInterval(self): + return 15 + + def decoder_commandline(self, file): + return None + + class WsjtParser(AudioChopperParser): def parse(self, profile: WsjtProfile, freq: int, raw_msg: bytes): try: