diff --git a/csdr.py b/csdr.py
index d9452c0..05b8973 100755
--- a/csdr.py
+++ b/csdr.py
@@ -25,7 +25,7 @@ import os
import signal
import threading
from functools import partial
-from owrx.wsjt import Ft8Chopper, WsprChopper
+from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper
import logging
logger = logging.getLogger(__name__)
@@ -277,6 +277,10 @@ class dsp(object):
chopper = Ft8Chopper(self.secondary_process_demod.stdout)
elif smd == "wspr":
chopper = WsprChopper(self.secondary_process_demod.stdout)
+ elif smd == "jt65":
+ chopper = Jt65Chopper(self.secondary_process_demod.stdout)
+ elif smd == "jt9":
+ chopper = Jt9Chopper(self.secondary_process_demod.stdout)
chopper.start()
self.output.add_output("wsjt_demod", chopper.read)
else:
@@ -371,7 +375,7 @@ class dsp(object):
def isWsjtMode(self, demodulator = None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
- return demodulator in ["ft8", "wspr"]
+ return demodulator in ["ft8", "wspr", "jt65", "jt9"]
def set_output_rate(self,output_rate):
self.output_rate=output_rate
diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css
index a251a3a..47cf9cf 100644
--- a/htdocs/css/openwebrx.css
+++ b/htdocs/css/openwebrx.css
@@ -840,20 +840,22 @@ img.openwebrx-mirror-img
}
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container,
-#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container
+#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container,
+#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container,
+#openwebrx-panel-digimodes[data-mode="jt9"] #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,
+#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel
{
display: none;
}
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container,
-#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container
+#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container,
+#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-canvas-container,
+#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container
{
height: 200px;
margin: -10px;
}
-
-#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
-#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel
-{
- display: none;
-}
\ No newline at end of file
diff --git a/htdocs/index.html b/htdocs/index.html
index 8c9aa50..854236f 100644
--- a/htdocs/index.html
+++ b/htdocs/index.html
@@ -82,6 +82,8 @@
+
+
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js
index 92905cf..4d184bf 100644
--- a/htdocs/openwebrx.js
+++ b/htdocs/openwebrx.js
@@ -1385,7 +1385,7 @@ function update_wsjt_panel(msg) {
var t = new Date(msg['timestamp']);
var pad = function(i) { return ('' + i).padStart(2, "0"); }
var linkedmsg = msg['msg'];
- if (msg['mode'] == 'FT8') {
+ if (['FT8', 'JT65', 'JT9'].indexOf(msg['mode']) >= 0) {
var 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] + '';
@@ -2709,6 +2709,8 @@ function demodulator_digital_replace(subtype)
case "rtty":
case "ft8":
case "wspr":
+ case "jt65":
+ case "jt9":
secondary_demod_start(subtype);
demodulator_analog_replace('usb', true);
demodulator_buttons_update();
@@ -2716,7 +2718,7 @@ function demodulator_digital_replace(subtype)
}
$('#openwebrx-panel-digimodes').attr('data-mode', subtype);
toggle_panel("openwebrx-panel-digimodes", true);
- toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr'].indexOf(subtype) >= 0);
+ toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9'].indexOf(subtype) >= 0);
}
function secondary_demod_create_canvas()
@@ -2888,6 +2890,8 @@ function secondary_demod_listbox_changed()
case "rtty":
case "ft8":
case "wspr":
+ case "jt65":
+ case "jt9":
demodulator_digital_replace(sdm);
break;
}
diff --git a/owrx/wsjt.py b/owrx/wsjt.py
index 925b3b0..75188ac 100644
--- a/owrx/wsjt.py
+++ b/owrx/wsjt.py
@@ -144,9 +144,29 @@ class WsprChopper(WsjtChopper):
return ["wsprd", "-d", file]
+class Jt65Chopper(WsjtChopper):
+ def __init__(self, source):
+ self.interval = 60
+ super().__init__(source)
+
+ def decoder_commandline(self, file):
+ #TODO expose decoding quality parameters through config
+ return ["jt9", "--jt65", "-d", "3", file]
+
+
+class Jt9Chopper(WsjtChopper):
+ def __init__(self, source):
+ self.interval = 60
+ super().__init__(source)
+
+ def decoder_commandline(self, file):
+ #TODO expose decoding quality parameters through config
+ return ["jt9", "--jt9", "-d", "3", file]
+
+
class WsjtParser(object):
locator_pattern = re.compile(".*\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$")
- jt9_pattern = re.compile("^[0-9]{6} .*")
+ jt9_pattern = re.compile("^([0-9]{6}|\\*{4}) .*")
wspr_pattern = re.compile("^[0-9]{4} .*")
wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")
@@ -154,7 +174,9 @@ class WsjtParser(object):
self.handler = handler
modes = {
- "~": "FT8"
+ "~": "FT8",
+ "#": "JT65",
+ "@": "JT9"
}
def parse(self, data):
@@ -179,15 +201,22 @@ class WsjtParser(object):
def parse_from_jt9(self, msg):
# ft8 sample
# '222100 -15 -0.0 508 ~ CQ EA7MJ IM66'
+ # jt65 sample
+ # '**** -10 0.4 1556 # CQ RN6AM KN95'
out = {}
- ts = datetime.strptime(msg[0:6], "%H%M%S")
- out["timestamp"] = int(datetime.combine(date.today(), ts.time(), datetime.now().tzinfo).timestamp() * 1000)
- out["db"] = float(msg[7:10])
- out["dt"] = float(msg[11:15])
- out["freq"] = int(msg[16:20])
- modeChar = msg[21:22]
+ if msg.startswith("****"):
+ out["timestamp"] = int(datetime.now().timestamp() * 1000)
+ msg = msg[5:]
+ else:
+ ts = datetime.strptime(msg[0:6], "%H%M%S")
+ out["timestamp"] = int(datetime.combine(date.today(), ts.time(), datetime.now().tzinfo).timestamp() * 1000)
+ msg = msg[7:]
+ out["db"] = float(msg[0:3])
+ out["dt"] = float(msg[4:8])
+ out["freq"] = int(msg[9:13])
+ modeChar = msg[14:15]
out["mode"] = mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown"
- wsjt_msg = msg[24:60].strip()
+ wsjt_msg = msg[17:53].strip()
self.parseLocator(wsjt_msg, mode)
out["msg"] = wsjt_msg
return out