Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions htdocs/lib/DemodulatorPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -382,4 +382,4 @@ $.fn.demodulatorPanel = function(){
this.data('panel', new DemodulatorPanel(this));
}
return this.data('panel');
};
};
4 changes: 2 additions & 2 deletions htdocs/lib/MessagePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -809,4 +809,4 @@ $.fn.vdl2MessagePanel = function() {
this.data('panel', new Vdl2MessagePanel(this));
}
return this.data('panel');
};
};
2 changes: 2 additions & 0 deletions owrx/config/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@
wsjt_decoding_depths=PropertyLayer(jt65=1),
fst4_enabled_intervals=[15, 30],
fst4w_enabled_intervals=[120, 300],
jt4_enabled_submodes=["F", "G"],
jt4_frequency_tolerance=20,
q65_enabled_combinations=["A30", "E120", "C60"],
js8_enabled_profiles=["normal", "slow"],
js8_decoding_depth=3,
Expand Down
12 changes: 11 additions & 1 deletion owrx/controllers/settings/decoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -70,6 +70,11 @@ def getSections(self):
"Default WSJT decoding depth",
infotext="A higher decoding depth will allow more results, but will also consume more cpu",
),
NumberInput(
"jt4_frequency_tolerance",
"Default JT4 frequency tolerance",
infotext="A higher frequency tolerance will allow more decodes, but will also consume more cpu",
),
WsjtDecodingDepthsInput(
"wsjt_decoding_depths",
"Individual decoding depths",
Expand All @@ -90,6 +95,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"),
),
]
2 changes: 1 addition & 1 deletion owrx/dsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
1 change: 1 addition & 0 deletions owrx/modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
2 changes: 1 addition & 1 deletion owrx/reporting/pskreporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion owrx/service/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
38 changes: 38 additions & 0 deletions owrx/wsjt.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ def decoding_depth(self):
# default when no setting is provided
return 3

def jt4_frequency_tolerance(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has no business being on WsjtProfile if it's only used in Jt4Profile. Please move.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getInterval is used - as this mode is decoded every 60 seconds, if removed:

Traceback (most recent call last):
  File "/usr/lib/python3.11/threading.py", line 1038, in _bootstrap_inner
    self.run()
  File "/opt/openwebrx/owrx/audio/chopper.py", line 59, in run
    self.setup_writers()
  File "/opt/openwebrx/owrx/audio/chopper.py", line 48, in setup_writers
    sorted_profiles = sorted(self.profile_source.getProfiles(), key=lambda p: p.getInterval())
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/openwebrx/owrx/wsjt.py", line 79, in getProfiles
    return [JT4Profile(i) for i in profiles if i in JT4Profile.availableSubmodes]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/openwebrx/owrx/wsjt.py", line 79, in <listcomp>
    return [JT4Profile(i) for i in profiles if i in JT4Profile.availableSubmodes]
            ^^^^^^^^^^^^^
TypeError: Can't instantiate abstract class JT4Profile with abstract method getInterval

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has no business being on WsjtProfile if it's only used in Jt4Profile. Please move.

Corrected.

pm = Config.get()
if "jt4_frequency_tolerance" in pm:
return pm["jt4_frequency_tolerance"]
# default when no setting is provided
return 20

def getTimestampFormat(self):
if self.getInterval() < 60:
return "%H%M%S"
Expand Down Expand Up @@ -62,6 +69,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"]
Expand Down Expand Up @@ -102,6 +119,8 @@ def getSource(mode: str):
return Fst4ProfileSource()
elif mode == "fst4w":
return Fst4wProfileSource()
elif mode == "jt4":
return JT4ProfileSource()
elif mode == "q65":
return Q65ProfileSource()

Expand Down Expand Up @@ -197,6 +216,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):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this method being used anywhere. Any plans for this? If not, I'd recommend removing it.

Copy link
Author

@sq6emm sq6emm Mar 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed by audio/chopper.py

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't. Please remove.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right... so... I saw the other comment, I believe it went a bit astray. I'm gonna stay on this thread in an attempt to keep things separate.

I also think I see what the issue is here, it's a simple misunderstanding. In pull requests, you usually put comments on the offending line, or the line that best matches the topic to be discussed. In this case, I put the initial comment on the line that has the getSubMode() method signature, trying to tell you that it isn't used.

getInterval() is indeed used, it is part of the AudioChopperProfile API, but I wasn't referring to that one 😉

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected;)

return self.submode

def decoder_commandline(self, file):
return ["jt9", "-4", "-b", str(self.submode), "-d", str(self.decoding_depth()), "-F", str(self.jt4_frequency_tolerance()), 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
Expand Down