move wsjt/js8 decisions out of csdr

This commit is contained in:
Jakob Ketterl 2021-04-09 18:16:25 +02:00
parent 22ec80c8ea
commit bbad34cec3
10 changed files with 135 additions and 95 deletions

View File

@ -28,19 +28,10 @@ import threading
import math import math
from functools import partial from functools import partial
from csdr.output import Output
from owrx.kiss import KissClient, DirewolfConfig, DirewolfConfigSubscriber from owrx.kiss import KissClient, DirewolfConfig, DirewolfConfigSubscriber
from owrx.wsjt import ( from owrx.audio.handler import AudioHandler
Ft8Profile,
WsprProfile,
Jt9Profile,
Jt65Profile,
Ft4Profile,
Fst4Profile,
Fst4wProfile,
Q65Profile,
)
from owrx.js8 import Js8Profiles
from owrx.audio import AudioChopper
from csdr.pipe import Pipe from csdr.pipe import Pipe
@ -49,40 +40,8 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class output(object): class Dsp(DirewolfConfigSubscriber):
def send_output(self, t, read_fn): def __init__(self, output: Output):
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):
self.samp_rate = 250000 self.samp_rate = 250000
self.output_rate = 11025 self.output_rate = 11025
self.hd_output_rate = 44100 self.hd_output_rate = 44100
@ -414,33 +373,10 @@ class dsp(DirewolfConfigSubscriber):
) )
self.secondary_processes_running = True self.secondary_processes_running = True
if self.isWsjtMode(): if self.isWsjtMode() or self.isJs8():
smd = self.get_secondary_demodulator() handler = AudioHandler(self, self.get_secondary_demodulator())
chopper_profiles = None handler.send_output("audio", self.secondary_process_demod.stdout.read)
if smd == "ft8": self.output.send_output("wsjt_demod", handler.read)
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)
elif self.isPacket(): elif self.isPacket():
# we best get the ax25 packets from the kiss socket # we best get the ax25 packets from the kiss socket
kiss = KissClient(self.direwolf_config.getPort()) kiss = KissClient(self.direwolf_config.getPort())

36
csdr/output.py Normal file
View File

@ -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

View File

@ -10,7 +10,6 @@ from multiprocessing.connection import Pipe, wait
from datetime import datetime, timedelta from datetime import datetime, timedelta
from queue import Queue, Full, Empty from queue import Queue, Full, Empty
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -169,9 +168,8 @@ class AudioChopperProfile(ABC):
class AudioWriter(object): class AudioWriter(object):
def __init__(self, dsp, source, profile: AudioChopperProfile): def __init__(self, active_dsp: "csdr.csdr.Dsp", profile: AudioChopperProfile):
self.dsp = dsp self.dsp = active_dsp
self.source = source
self.profile = profile self.profile = profile
self.tmp_dir = CoreConfig().get_temporary_directory() self.tmp_dir = CoreConfig().get_temporary_directory()
self.wavefile = None self.wavefile = None
@ -289,9 +287,9 @@ class AudioWriter(object):
class AudioChopper(threading.Thread, metaclass=ABCMeta): class AudioChopper(threading.Thread, metaclass=ABCMeta):
def __init__(self, dsp, source, *profiles: AudioChopperProfile): def __init__(self, active_dsp: "csdr.csdr.Dsp", readfn: callable, *profiles: AudioChopperProfile):
self.source = source self.readfn = readfn
self.writers = [AudioWriter(dsp, source, p) for p in profiles] self.writers = [AudioWriter(active_dsp, p) for p in profiles]
self.doRun = True self.doRun = True
super().__init__() super().__init__()
@ -302,7 +300,7 @@ class AudioChopper(threading.Thread, metaclass=ABCMeta):
while self.doRun: while self.doRun:
data = None data = None
try: try:
data = self.source.read(256) data = self.readfn(256)
except ValueError: except ValueError:
pass pass
if data is None or (isinstance(data, bytes) and len(data) == 0): if data is None or (isinstance(data, bytes) and len(data) == 0):

22
owrx/audio/handler.py Normal file
View File

@ -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)

View File

@ -8,7 +8,8 @@ from owrx.property import PropertyStack, PropertyLayer, PropertyValidator
from owrx.property.validators import OrValidator, RegexValidator, BoolValidator from owrx.property.validators import OrValidator, RegexValidator, BoolValidator
from owrx.modes import Modes from owrx.modes import Modes
from owrx.config.core import CoreConfig from owrx.config.core import CoreConfig
from csdr import csdr from csdr.output import Output
from csdr import Dsp
import threading import threading
import re import re
@ -26,7 +27,7 @@ class ModulationValidator(OrValidator):
super().__init__(BoolValidator(), RegexValidator(re.compile("^[a-z0-9]+$"))) super().__init__(BoolValidator(), RegexValidator(re.compile("^[a-z0-9]+$")))
class DspManager(csdr.output, SdrSourceEventClient): class DspManager(Output, SdrSourceEventClient):
def __init__(self, handler, sdrSource): def __init__(self, handler, sdrSource):
self.handler = handler self.handler = handler
self.sdrSource = sdrSource 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() self.dsp.nc_port = self.sdrSource.getPort()
def set_low_cut(cut): def set_low_cut(cut):

View File

@ -1,6 +1,7 @@
from owrx.config.core import CoreConfig from owrx.config.core import CoreConfig
from owrx.config import Config from owrx.config import Config
from csdr import csdr import csdr
from csdr.output import Output
import threading import threading
from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
from owrx.property import PropertyStack from owrx.property import PropertyStack
@ -10,7 +11,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SpectrumThread(csdr.output, SdrSourceEventClient): class SpectrumThread(Output, SdrSourceEventClient):
def __init__(self, sdrSource): def __init__(self, sdrSource):
self.sdrSource = sdrSource self.sdrSource = sdrSource
super().__init__() super().__init__()
@ -26,7 +27,7 @@ class SpectrumThread(csdr.output, SdrSourceEventClient):
"fft_compression", "fft_compression",
) )
self.dsp = dsp = csdr.dsp(self) self.dsp = dsp = csdr.Dsp(self)
dsp.nc_port = self.sdrSource.getPort() dsp.nc_port = self.sdrSource.getPort()
dsp.set_demodulator("fft") dsp.set_demodulator("fft")

View File

@ -1,5 +1,6 @@
from owrx.feature import FeatureDetector from owrx.feature import FeatureDetector
from functools import reduce from functools import reduce
from abc import ABCMeta, abstractmethod
class Bandpass(object): class Bandpass(object):
@ -51,13 +52,39 @@ class DigitalMode(Mode):
return Modes.findByModulation(self.underlying[0]).get_modulation() 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): def __init__(self, modulation, name, bandpass=None, requirements=None):
if bandpass is None: if bandpass is None:
bandpass = Bandpass(0, 3000) 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: if requirements is None:
requirements = ["wsjt-x"] 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): class Modes(object):
@ -89,9 +116,7 @@ class Modes(object):
WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]), WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]),
WsjtMode("fst4w", "FST4W", bandpass=Bandpass(1350, 1650), 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"]), WsjtMode("q65", "Q65", requirements=["wsjt-x-2-4"]),
DigitalMode( Js8Mode("js8", "JS8Call"),
"js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True
),
DigitalMode( DigitalMode(
"packet", "packet",
"Packet", "Packet",

View File

@ -2,7 +2,8 @@ import threading
from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
from owrx.sdr import SdrService from owrx.sdr import SdrService
from owrx.bands import Bandplan 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.wsjt import WsjtParser
from owrx.aprs import AprsParser from owrx.aprs import AprsParser
from owrx.js8 import Js8Parser from owrx.js8 import Js8Parser
@ -20,7 +21,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ServiceOutput(output, metaclass=ABCMeta): class ServiceOutput(Output, metaclass=ABCMeta):
def __init__(self, frequency): def __init__(self, frequency):
self.frequency = frequency self.frequency = frequency
@ -286,7 +287,7 @@ class ServiceHandler(SdrSourceEventClient):
output = Js8ServiceOutput(frequency) output = Js8ServiceOutput(frequency)
else: else:
output = WsjtServiceOutput(frequency) output = WsjtServiceOutput(frequency)
d = dsp(output) d = Dsp(output)
d.nc_port = source.getPort() d.nc_port = source.getPort()
center_freq = source.getProps()["center_freq"] center_freq = source.getProps()["center_freq"]
d.set_offset_freq(frequency - center_freq) d.set_offset_freq(frequency - center_freq)

View File

@ -39,6 +39,25 @@ class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta):
def getMode(self): def getMode(self):
pass 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): class Ft8Profile(WsjtProfile):
def getInterval(self): def getInterval(self):

View File

@ -23,6 +23,7 @@ setup(
"owrx.form", "owrx.form",
"owrx.config", "owrx.config",
"owrx.reporting", "owrx.reporting",
"owrx.audio",
"csdr", "csdr",
"htdocs", "htdocs",
"owrxadmin", "owrxadmin",