diff --git a/csdr.py b/csdr.py
index 31ee80a..066d2de 100755
--- a/csdr.py
+++ b/csdr.py
@@ -21,11 +21,11 @@ OpenWebRX csdr plugin: do the signal processing with csdr
"""
import subprocess
-import time
import os
import signal
import threading
from functools import partial
+from owrx.wsjt import Ft8Chopper
import logging
logger = logging.getLogger(__name__)
@@ -186,6 +186,12 @@ class dsp(object):
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"
+ elif which == "ft8":
+ chain = secondary_chain_base + "csdr realpart_cf | "
+ if self.last_decimation != 1.0 :
+ chain += "csdr fractional_decimator_ff {last_decimation} | "
+ chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
+ return chain
def set_secondary_demodulator(self, what):
if self.get_secondary_demodulator() == what:
@@ -238,7 +244,8 @@ class dsp(object):
secondary_samples_per_bits=self.secondary_samples_per_bits(),
secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
- if_samp_rate=self.if_samp_rate()
+ if_samp_rate=self.if_samp_rate(),
+ last_decimation=self.last_decimation
)
logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft)
@@ -253,7 +260,11 @@ class dsp(object):
self.secondary_processes_running = True
self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())))
- self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
+ if self.get_secondary_demodulator() == "ft8":
+ chopper = Ft8Chopper(self.secondary_process_demod.stdout)
+ chopper.start()
+ else:
+ self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
#open control pipes for csdr and send initialization data
if self.secondary_shift_pipe != None: #TODO digimodes
@@ -262,7 +273,7 @@ class dsp(object):
def set_secondary_offset_freq(self, value):
self.secondary_offset_freq=value
- if self.secondary_processes_running:
+ if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"):
self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate()))
self.secondary_shift_pipe_file.flush()
@@ -332,6 +343,8 @@ class dsp(object):
def get_audio_rate(self):
if self.isDigitalVoice():
return 48000
+ elif self.secondary_demodulator == "ft8":
+ return 12000
return self.get_output_rate()
def isDigitalVoice(self, demodulator = None):
diff --git a/htdocs/index.html b/htdocs/index.html
index 07b219e..5373f49 100644
--- a/htdocs/index.html
+++ b/htdocs/index.html
@@ -109,6 +109,7 @@
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js
index 399a435..280b380 100644
--- a/htdocs/openwebrx.js
+++ b/htdocs/openwebrx.js
@@ -2633,6 +2633,7 @@ function demodulator_digital_replace(subtype)
{
case "bpsk31":
case "rtty":
+ case "ft8":
secondary_demod_start(subtype);
demodulator_analog_replace('usb', true);
demodulator_buttons_update();
@@ -2809,6 +2810,9 @@ function secondary_demod_listbox_changed()
case "rtty":
demodulator_digital_replace('rtty');
break;
+ case "ft8":
+ demodulator_digital_replace('ft8');
+ break;
}
}
diff --git a/owrx/wsjt.py b/owrx/wsjt.py
new file mode 100644
index 0000000..c9fc319
--- /dev/null
+++ b/owrx/wsjt.py
@@ -0,0 +1,79 @@
+import threading
+import wave
+from datetime import datetime, timedelta
+import time
+import sched
+import subprocess
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Ft8Chopper(threading.Thread):
+ def __init__(self, source):
+ self.source = source
+ (self.wavefilename, self.wavefile) = self.getWaveFile()
+ self.scheduler = sched.scheduler(time.time, time.sleep)
+ self.queue = []
+ self.doRun = True
+ super().__init__()
+
+ def getWaveFile(self):
+ filename = "/tmp/openwebrx-ft8chopper-{0}.wav".format(datetime.now().strftime("%Y%m%d-%H%M%S"))
+ wavefile = wave.open(filename, "wb")
+ wavefile.setnchannels(1)
+ wavefile.setsampwidth(2)
+ wavefile.setframerate(12000)
+ return (filename, wavefile)
+
+ def getNextDecodingTime(self):
+ t = datetime.now()
+ seconds = (int(t.second / 15) + 1) * 15
+ if seconds >= 60:
+ t = t + timedelta(minutes = 1)
+ seconds = 0
+ t = t.replace(second = seconds, microsecond = 0)
+ logger.debug("scheduling: {0}".format(t))
+ return t.timestamp()
+
+ def startScheduler(self):
+ self._scheduleNextSwitch()
+ threading.Thread(target = self.scheduler.run).start()
+
+ def emptyScheduler(self):
+ for event in self.scheduler.queue:
+ self.scheduler.cancel(event)
+
+ def _scheduleNextSwitch(self):
+ self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles)
+
+ def switchFiles(self):
+ file = self.wavefile
+ filename = self.wavefilename
+ (self.wavefilename, self.wavefile) = self.getWaveFile()
+
+ file.close()
+ self.queue.append(filename)
+ self._scheduleNextSwitch()
+
+ def decode(self):
+ if self.queue:
+ file = self.queue.pop()
+ logger.debug("processing file {0}".format(file))
+ #TODO expose decoding quality parameters through config
+ self.decoder = subprocess.Popen(["jt9", "--ft8", "-d", "3", file])
+
+ def run(self) -> None:
+ logger.debug("FT8 chopper starting up")
+ self.startScheduler()
+ while self.doRun:
+ data = self.source.read(256)
+ if data is None or (isinstance(data, bytes) and len(data) == 0):
+ logger.warning("zero read on ft8 chopper")
+ self.doRun = False
+ else:
+ self.wavefile.writeframes(data)
+
+ self.decode()
+ logger.debug("FT8 chopper shutting down")
+ self.emptyScheduler()