From 284646ee6cfb3a4b3aed998c6d5274f90d086942 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 6 Jul 2019 18:21:43 +0200 Subject: [PATCH] first stab at ft8 decoding: chop up audio, call jt9 binary to decode --- csdr.py | 21 +++++++++--- htdocs/index.html | 1 + htdocs/openwebrx.js | 4 +++ owrx/wsjt.py | 79 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 owrx/wsjt.py 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()