From bbad34cec3055fc0ee577b4b969929aaf7d80d47 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 9 Apr 2021 18:16:25 +0200 Subject: [PATCH] move wsjt/js8 decisions out of csdr --- csdr/{csdr.py => __init__.py} | 82 +++------------------------- csdr/output.py | 36 ++++++++++++ owrx/{audio.py => audio/__init__.py} | 14 ++--- owrx/audio/handler.py | 22 ++++++++ owrx/dsp.py | 7 ++- owrx/fft.py | 7 ++- owrx/modes.py | 35 ++++++++++-- owrx/service/__init__.py | 7 ++- owrx/wsjt.py | 19 +++++++ setup.py | 1 + 10 files changed, 135 insertions(+), 95 deletions(-) rename csdr/{csdr.py => __init__.py} (92%) create mode 100644 csdr/output.py rename owrx/{audio.py => audio/__init__.py} (96%) create mode 100644 owrx/audio/handler.py diff --git a/csdr/csdr.py b/csdr/__init__.py similarity index 92% rename from csdr/csdr.py rename to csdr/__init__.py index 49725e7..dbc08d9 100644 --- a/csdr/csdr.py +++ b/csdr/__init__.py @@ -28,19 +28,10 @@ import threading import math from functools import partial +from csdr.output import Output + from owrx.kiss import KissClient, DirewolfConfig, DirewolfConfigSubscriber -from owrx.wsjt import ( - Ft8Profile, - WsprProfile, - Jt9Profile, - Jt65Profile, - Ft4Profile, - Fst4Profile, - Fst4wProfile, - Q65Profile, -) -from owrx.js8 import Js8Profiles -from owrx.audio import AudioChopper +from owrx.audio.handler import AudioHandler from csdr.pipe import Pipe @@ -49,40 +40,8 @@ import logging logger = logging.getLogger(__name__) -class output(object): - def send_output(self, t, read_fn): - if not self.supports_type(t): - # TODO rewrite the output mechanism in a way that avoids producing unnecessary data - logger.warning("dumping output of type %s since it is not supported.", t) - threading.Thread(target=self.pump(read_fn, lambda x: None), name="csdr_pump_thread").start() - return - self.receive_output(t, read_fn) - - def receive_output(self, t, read_fn): - pass - - def pump(self, read, write): - def copy(): - run = True - while run: - data = None - try: - data = read() - except ValueError: - pass - if data is None or (isinstance(data, bytes) and len(data) == 0): - run = False - else: - write(data) - - return copy - - def supports_type(self, t): - return True - - -class dsp(DirewolfConfigSubscriber): - def __init__(self, output): +class Dsp(DirewolfConfigSubscriber): + def __init__(self, output: Output): self.samp_rate = 250000 self.output_rate = 11025 self.hd_output_rate = 44100 @@ -414,33 +373,10 @@ class dsp(DirewolfConfigSubscriber): ) self.secondary_processes_running = True - if self.isWsjtMode(): - smd = self.get_secondary_demodulator() - chopper_profiles = None - if smd == "ft8": - chopper_profiles = [Ft8Profile()] - elif smd == "wspr": - chopper_profiles = [WsprProfile()] - elif smd == "jt65": - chopper_profiles = [Jt65Profile()] - elif smd == "jt9": - chopper_profiles = [Jt9Profile()] - elif smd == "ft4": - chopper_profiles = [Ft4Profile()] - elif smd == "fst4": - chopper_profiles = Fst4Profile.getEnabledProfiles() - elif smd == "fst4w": - chopper_profiles = Fst4wProfile.getEnabledProfiles() - elif smd == "q65": - chopper_profiles = Q65Profile.getEnabledProfiles() - if chopper_profiles is not None and len(chopper_profiles): - chopper = AudioChopper(self, self.secondary_process_demod.stdout, *chopper_profiles) - chopper.start() - self.output.send_output("wsjt_demod", chopper.read) - elif self.isJs8(): - chopper = AudioChopper(self, self.secondary_process_demod.stdout, *Js8Profiles.getEnabledProfiles()) - chopper.start() - self.output.send_output("js8_demod", chopper.read) + if self.isWsjtMode() or self.isJs8(): + handler = AudioHandler(self, self.get_secondary_demodulator()) + handler.send_output("audio", self.secondary_process_demod.stdout.read) + self.output.send_output("wsjt_demod", handler.read) elif self.isPacket(): # we best get the ax25 packets from the kiss socket kiss = KissClient(self.direwolf_config.getPort()) diff --git a/csdr/output.py b/csdr/output.py new file mode 100644 index 0000000..5fef242 --- /dev/null +++ b/csdr/output.py @@ -0,0 +1,36 @@ +import threading +import logging + +logger = logging.getLogger(__name__) + + +class Output(object): + def send_output(self, t, read_fn): + if not self.supports_type(t): + # TODO rewrite the output mechanism in a way that avoids producing unnecessary data + logger.warning("dumping output of type %s since it is not supported.", t) + threading.Thread(target=self.pump(read_fn, lambda x: None), name="csdr_pump_thread").start() + return + self.receive_output(t, read_fn) + + def receive_output(self, t, read_fn): + pass + + def pump(self, read, write): + def copy(): + run = True + while run: + data = None + try: + data = read() + except ValueError: + pass + if data is None or (isinstance(data, bytes) and len(data) == 0): + run = False + else: + write(data) + + return copy + + def supports_type(self, t): + return True diff --git a/owrx/audio.py b/owrx/audio/__init__.py similarity index 96% rename from owrx/audio.py rename to owrx/audio/__init__.py index 93956ea..39d7bfb 100644 --- a/owrx/audio.py +++ b/owrx/audio/__init__.py @@ -10,7 +10,6 @@ from multiprocessing.connection import Pipe, wait from datetime import datetime, timedelta from queue import Queue, Full, Empty - import logging logger = logging.getLogger(__name__) @@ -169,9 +168,8 @@ class AudioChopperProfile(ABC): class AudioWriter(object): - def __init__(self, dsp, source, profile: AudioChopperProfile): - self.dsp = dsp - self.source = source + def __init__(self, active_dsp: "csdr.csdr.Dsp", profile: AudioChopperProfile): + self.dsp = active_dsp self.profile = profile self.tmp_dir = CoreConfig().get_temporary_directory() self.wavefile = None @@ -289,9 +287,9 @@ class AudioWriter(object): class AudioChopper(threading.Thread, metaclass=ABCMeta): - def __init__(self, dsp, source, *profiles: AudioChopperProfile): - self.source = source - self.writers = [AudioWriter(dsp, source, p) for p in profiles] + def __init__(self, active_dsp: "csdr.csdr.Dsp", readfn: callable, *profiles: AudioChopperProfile): + self.readfn = readfn + self.writers = [AudioWriter(active_dsp, p) for p in profiles] self.doRun = True super().__init__() @@ -302,7 +300,7 @@ class AudioChopper(threading.Thread, metaclass=ABCMeta): while self.doRun: data = None try: - data = self.source.read(256) + data = self.readfn(256) except ValueError: pass if data is None or (isinstance(data, bytes) and len(data) == 0): diff --git a/owrx/audio/handler.py b/owrx/audio/handler.py new file mode 100644 index 0000000..51c5dcc --- /dev/null +++ b/owrx/audio/handler.py @@ -0,0 +1,22 @@ +from owrx.modes import Modes, AudioChopperMode +from csdr.output import Output +from owrx.audio import AudioChopper + + +class AudioHandler(Output): + def __init__(self, active_dsp: "csdr.csdr.Dsp", mode: str): + self.dsp = active_dsp + self.mode = Modes.findByModulation(mode) + if mode is None or not isinstance(self.mode, AudioChopperMode): + raise ValueError("Mode {} is not an audio chopper mode".format(mode)) + self.chopper = None + + def supports_type(self, t): + return t == "audio" + + def receive_output(self, t, read_fn): + self.chopper = AudioChopper(self.dsp, read_fn, *self.mode.getProfiles()) + self.chopper.start() + + def read(self, *args, **kwargs): + return self.chopper.read(*args, **kwargs) diff --git a/owrx/dsp.py b/owrx/dsp.py index ec96b70..0caa70d 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -8,7 +8,8 @@ from owrx.property import PropertyStack, PropertyLayer, PropertyValidator from owrx.property.validators import OrValidator, RegexValidator, BoolValidator from owrx.modes import Modes from owrx.config.core import CoreConfig -from csdr import csdr +from csdr.output import Output +from csdr import Dsp import threading import re @@ -26,7 +27,7 @@ class ModulationValidator(OrValidator): super().__init__(BoolValidator(), RegexValidator(re.compile("^[a-z0-9]+$"))) -class DspManager(csdr.output, SdrSourceEventClient): +class DspManager(Output, SdrSourceEventClient): def __init__(self, handler, sdrSource): self.handler = handler self.sdrSource = sdrSource @@ -75,7 +76,7 @@ class DspManager(csdr.output, SdrSourceEventClient): ), ) - self.dsp = csdr.dsp(self) + self.dsp = Dsp(self) self.dsp.nc_port = self.sdrSource.getPort() def set_low_cut(cut): diff --git a/owrx/fft.py b/owrx/fft.py index 2c8ba15..0900b17 100644 --- a/owrx/fft.py +++ b/owrx/fft.py @@ -1,6 +1,7 @@ from owrx.config.core import CoreConfig from owrx.config import Config -from csdr import csdr +import csdr +from csdr.output import Output import threading from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass from owrx.property import PropertyStack @@ -10,7 +11,7 @@ import logging logger = logging.getLogger(__name__) -class SpectrumThread(csdr.output, SdrSourceEventClient): +class SpectrumThread(Output, SdrSourceEventClient): def __init__(self, sdrSource): self.sdrSource = sdrSource super().__init__() @@ -26,7 +27,7 @@ class SpectrumThread(csdr.output, SdrSourceEventClient): "fft_compression", ) - self.dsp = dsp = csdr.dsp(self) + self.dsp = dsp = csdr.Dsp(self) dsp.nc_port = self.sdrSource.getPort() dsp.set_demodulator("fft") diff --git a/owrx/modes.py b/owrx/modes.py index 869bf49..f34cbc4 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -1,5 +1,6 @@ from owrx.feature import FeatureDetector from functools import reduce +from abc import ABCMeta, abstractmethod class Bandpass(object): @@ -51,13 +52,39 @@ class DigitalMode(Mode): return Modes.findByModulation(self.underlying[0]).get_modulation() -class WsjtMode(DigitalMode): +class AudioChopperMode(DigitalMode, metaclass=ABCMeta): def __init__(self, modulation, name, bandpass=None, requirements=None): if bandpass is None: bandpass = Bandpass(0, 3000) + super().__init__(modulation, name, ["usb"], bandpass=bandpass, requirements=requirements, service=True) + + @abstractmethod + def getProfiles(self): + pass + + +class WsjtMode(AudioChopperMode): + def __init__(self, modulation, name, bandpass=None, requirements=None): if requirements is None: requirements = ["wsjt-x"] - super().__init__(modulation, name, ["usb"], bandpass=bandpass, requirements=requirements, service=True) + super().__init__(modulation, name, bandpass=bandpass, requirements=requirements) + + def getProfiles(self): + # inline import due to circular dependencies + from owrx.wsjt import WsjtProfile + return WsjtProfile.getProfiles(self.modulation) + + +class Js8Mode(AudioChopperMode): + def __init__(self, modulation, name, bandpass=None, requirements=None): + if requirements is None: + requirements = ["js8call"] + super().__init__(modulation, name, bandpass, requirements) + + def getProfiles(self): + # inline import due to circular dependencies + from owrx.js8 import Js8Profiles + return Js8Profiles.getEnabledProfiles() class Modes(object): @@ -89,9 +116,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( - "js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True - ), + Js8Mode("js8", "JS8Call"), DigitalMode( "packet", "Packet", diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py index 799863b..eac8c25 100644 --- a/owrx/service/__init__.py +++ b/owrx/service/__init__.py @@ -2,7 +2,8 @@ import threading from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass from owrx.sdr import SdrService from owrx.bands import Bandplan -from csdr.csdr import dsp, output +from csdr.output import Output +from csdr import Dsp from owrx.wsjt import WsjtParser from owrx.aprs import AprsParser from owrx.js8 import Js8Parser @@ -20,7 +21,7 @@ import logging logger = logging.getLogger(__name__) -class ServiceOutput(output, metaclass=ABCMeta): +class ServiceOutput(Output, metaclass=ABCMeta): def __init__(self, frequency): self.frequency = frequency @@ -286,7 +287,7 @@ class ServiceHandler(SdrSourceEventClient): output = Js8ServiceOutput(frequency) else: output = WsjtServiceOutput(frequency) - d = dsp(output) + d = Dsp(output) d.nc_port = source.getPort() center_freq = source.getProps()["center_freq"] d.set_offset_freq(frequency - center_freq) diff --git a/owrx/wsjt.py b/owrx/wsjt.py index ee46617..9cafe68 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -39,6 +39,25 @@ class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta): def getMode(self): pass + @staticmethod + def getProfiles(mode: str): + if mode == "ft8": + return [Ft8Profile()] + elif mode == "wspr": + return [WsprProfile()] + elif mode == "jt65": + return [Jt65Profile()] + elif mode == "jt9": + return [Jt9Profile()] + elif mode == "ft4": + return [Ft4Profile()] + elif mode == "fst4": + return Fst4Profile.getEnabledProfiles() + elif mode == "fst4w": + return Fst4wProfile.getEnabledProfiles() + elif mode == "q65": + return Q65Profile.getEnabledProfiles() + class Ft8Profile(WsjtProfile): def getInterval(self): diff --git a/setup.py b/setup.py index b0101f0..8373ffd 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ setup( "owrx.form", "owrx.config", "owrx.reporting", + "owrx.audio", "csdr", "htdocs", "owrxadmin",