From cbcba5807f4fe5d5e9cfef9a35efd69fd336b738 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 23 Sep 2021 18:43:41 +0200 Subject: [PATCH] restore PSK decoding --- csdr/chain/demodulator.py | 40 +++++++++++++++++--------- csdr/chain/digimodes.py | 35 +++++++++++++++++++---- csdr/chain/selector.py | 40 +++++++++++++++++++++++--- owrx/dsp.py | 60 ++++++++++++++++++++++++++++----------- owrx/service/__init__.py | 11 ++++--- owrx/service/chain.py | 10 +++---- 6 files changed, 145 insertions(+), 51 deletions(-) diff --git a/csdr/chain/demodulator.py b/csdr/chain/demodulator.py index b65a8bc..64399b6 100644 --- a/csdr/chain/demodulator.py +++ b/csdr/chain/demodulator.py @@ -1,21 +1,8 @@ from csdr.chain import Chain -from abc import ABC, abstractmethod +from abc import ABC, ABCMeta, abstractmethod from pycsdr.modules import Writer -class BaseDemodulatorChain(Chain): - def supportsSquelch(self) -> bool: - return True - - def setSampleRate(self, sampleRate: int) -> None: - pass - - -class SecondaryDemodulator(Chain): - def supportsSquelch(self) -> bool: - return True - - class FixedAudioRateChain(ABC): @abstractmethod def getFixedAudioRate(self) -> int: @@ -49,3 +36,28 @@ class SlotFilterChain(ABC): @abstractmethod def setSlotFilter(self, filter: int) -> None: pass + + +class SecondarySelectorChain(ABC): + def getBandwidth(self) -> float: + pass + + +class BaseDemodulatorChain(Chain): + def supportsSquelch(self) -> bool: + return True + + def setSampleRate(self, sampleRate: int) -> None: + pass + + +class SecondaryDemodulator(Chain): + def supportsSquelch(self) -> bool: + return True + + def setSampleRate(self, sampleRate: int) -> None: + pass + + +class ServiceDemodulator(SecondaryDemodulator, FixedAudioRateChain, metaclass=ABCMeta): + pass diff --git a/csdr/chain/digimodes.py b/csdr/chain/digimodes.py index 1cadb3e..ccabce0 100644 --- a/csdr/chain/digimodes.py +++ b/csdr/chain/digimodes.py @@ -1,15 +1,15 @@ -from csdr.chain.demodulator import SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver +from csdr.chain.demodulator import ServiceDemodulator, SecondaryDemodulator, DialFrequencyReceiver, SecondarySelectorChain from owrx.audio.chopper import AudioChopper from owrx.aprs.kiss import KissDeframer from owrx.aprs import Ax25Parser, AprsParser -from pycsdr.modules import Convert, FmDemod +from pycsdr.modules import Convert, FmDemod, Agc, TimingRecovery, DBPskDecoder, VaricodeDecoder from pycsdr.types import Format from owrx.aprs.module import DirewolfModule from digiham.modules import FskDemodulator, PocsagDecoder from owrx.pocsag import PocsagParser -class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver): +class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver): # TODO parser typing def __init__(self, mode: str, parser): self.chopper = AudioChopper(mode, parser) @@ -23,7 +23,7 @@ class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFre self.chopper.setDialFrequency(frequency) -class PacketDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver): +class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver): def __init__(self, service: bool = False): self.parser = AprsParser() workers = [ @@ -46,7 +46,7 @@ class PacketDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequency self.parser.setDialFrequency(frequency) -class PocsagDemodulator(SecondaryDemodulator, FixedAudioRateChain): +class PocsagDemodulator(ServiceDemodulator): def __init__(self): workers = [ FmDemod(), @@ -61,3 +61,28 @@ class PocsagDemodulator(SecondaryDemodulator, FixedAudioRateChain): def getFixedAudioRate(self) -> int: return 48000 + + +class PskDemodulator(SecondaryDemodulator, SecondarySelectorChain): + def __init__(self, baudRate: float): + self.baudRate = baudRate + # this is an assumption, we will adjust in setSampleRate + self.sampleRate = 12000 + secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3 + workers = [ + Agc(Format.COMPLEX_FLOAT), + TimingRecovery(secondary_samples_per_bits, 0.5, 2, useQ=True), + DBPskDecoder(), + VaricodeDecoder(), + ] + super().__init__(workers) + + def getBandwidth(self): + return self.baudRate + + def setSampleRate(self, sampleRate: int) -> None: + if sampleRate == self.sampleRate: + return + self.sampleRate = sampleRate + secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3 + self.replace(1, TimingRecovery(secondary_samples_per_bits, 0.5, 2, useQ=True)) diff --git a/csdr/chain/selector.py b/csdr/chain/selector.py index ac651d5..d87342a 100644 --- a/csdr/chain/selector.py +++ b/csdr/chain/selector.py @@ -62,10 +62,12 @@ class Decimator(Chain): class Selector(Chain): - def __init__(self, inputRate: int, outputRate: int, shiftRate: float, withSquelch: bool = True): + def __init__(self, inputRate: int, outputRate: int, withSquelch: bool = True): + self.inputRate = inputRate self.outputRate = outputRate + self.frequencyOffset = 0 - self.shift = Shift(shiftRate) + self.shift = Shift(0.0) self.decimation = Decimator(inputRate, outputRate) @@ -88,8 +90,15 @@ class Selector(Chain): bp_transition = 320.0 / self.outputRate return Bandpass(transition=bp_transition, use_fft=True) - def setShiftRate(self, rate: float) -> None: - self.shift.setRate(rate) + def setFrequencyOffset(self, offset: int) -> None: + if offset == self.frequencyOffset: + return + self.frequencyOffset = offset + self._updateShift() + + def _updateShift(self): + shift = -self.frequencyOffset / self.inputRate + self.shift.setRate(shift) def _convertToLinear(self, db: float) -> float: return float(math.pow(10, db / 10)) @@ -125,4 +134,27 @@ class Selector(Chain): self.replace(2, self.bandpass) def setInputRate(self, inputRate: int) -> None: + if inputRate == self.inputRate: + return + self.inputRate = inputRate self.decimation.setInputRate(inputRate) + self._updateShift() + + +class SecondarySelector(Chain): + def __init__(self, sampleRate: int, bandwidth: float): + self.sampleRate = sampleRate + self.frequencyOffset = 0 + self.shift = Shift(0.0) + cutoffRate = bandwidth / sampleRate + self.bandpass = Bandpass(-cutoffRate, cutoffRate, cutoffRate, use_fft=True) + workers = [self.shift, self.bandpass] + super().__init__(workers) + + def setFrequencyOffset(self, offset: int) -> None: + if offset == self.frequencyOffset: + return + self.frequencyOffset = offset + if self.frequencyOffset is None: + return + self.shift.setRate(-offset / self.sampleRate) diff --git a/owrx/dsp.py b/owrx/dsp.py index 6cb1ca2..a4e98cc 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -3,8 +3,8 @@ from owrx.property import PropertyStack, PropertyLayer, PropertyValidator from owrx.property.validators import OrValidator, RegexValidator, BoolValidator from owrx.modes import Modes from csdr.chain import Chain -from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain -from csdr.chain.selector import Selector +from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain +from csdr.chain.selector import Selector, SecondarySelector from csdr.chain.clientaudio import ClientAudioChain from csdr.chain.fft import FftChain from pycsdr.modules import Buffer, Writer @@ -25,7 +25,7 @@ class ClientDemodulatorChain(Chain): self.sampleRate = sampleRate self.outputRate = outputRate self.hdOutputRate = hdOutputRate - self.selector = Selector(sampleRate, outputRate, 0.0) + self.selector = Selector(sampleRate, outputRate) self.selector.setBandpass(-4000, 4000) self.selectorBuffer = Buffer(Format.COMPLEX_FLOAT) self.audioBuffer = None @@ -41,6 +41,8 @@ class ClientDemodulatorChain(Chain): self.secondaryFftWriter = None self.secondaryWriter = None self.squelchLevel = -150 + self.secondarySelector = None + self.secondaryFrequencyOffset = None super().__init__([self.selector, self.demodulator, self.clientAudioChain]) def stop(self): @@ -131,8 +133,20 @@ class ClientDemodulatorChain(Chain): self._updateDialFrequency() self._syncSquelch() + if isinstance(self.secondaryDemodulator, SecondarySelectorChain): + self.secondarySelector = SecondarySelector(rate, self.secondaryDemodulator.getBandwidth()) + self.secondarySelector.setReader(self.selectorBuffer.getReader()) + self.secondarySelector.setFrequencyOffset(self.secondaryFrequencyOffset) + else: + self.secondarySelector = None + if self.secondaryDemodulator is not None: - if self.secondaryDemodulator.getInputFormat() is Format.COMPLEX_FLOAT: + self.secondaryDemodulator.setSampleRate(rate) + if self.secondarySelector is not None: + buffer = Buffer(Format.COMPLEX_FLOAT) + self.secondarySelector.setWriter(buffer) + self.secondaryDemodulator.setReader(buffer.getReader()) + elif self.secondaryDemodulator.getInputFormat() is Format.COMPLEX_FLOAT: self.secondaryDemodulator.setReader(self.selectorBuffer.getReader()) else: self.secondaryDemodulator.setReader(self.audioBuffer.getReader()) @@ -170,10 +184,7 @@ class ClientDemodulatorChain(Chain): if offset == self.frequencyOffset: return self.frequencyOffset = offset - - shift = -offset / self.sampleRate - self.selector.setShiftRate(shift) - + self.selector.setFrequencyOffset(offset) self._updateDialFrequency() def setCenterFrequency(self, frequency: int) -> None: @@ -211,6 +222,8 @@ class ClientDemodulatorChain(Chain): if not isinstance(self.demodulator, FixedIfSampleRateChain): self.selector.setOutputRate(outputRate) self.demodulator.setSampleRate(outputRate) + if self.secondaryDemodulator is not None: + self.secondaryDemodulator.setSampleRate(outputRate) if not isinstance(self.demodulator, FixedAudioRateChain): self.clientAudioChain.setClientRate(outputRate) @@ -268,6 +281,15 @@ class ClientDemodulatorChain(Chain): # TODO pass + def setSecondaryFrequencyOffset(self, freq: int) -> None: + if self.secondaryFrequencyOffset == freq: + return + self.secondaryFrequencyOffset = freq + + if self.secondarySelector is None: + return + self.secondarySelector.setFrequencyOffset(self.secondaryFrequencyOffset) + class ModulationValidator(OrValidator): """ @@ -397,8 +419,6 @@ class DspManager(SdrSourceEventClient): self.props.wireProperty("dmr_filter", self.chain.setSlotFilter), # TODO # self.props.wireProperty("wfm_deemphasis_tau", self.dsp.set_wfm_deemphasis_tau), - # TODO - # self.props.wireProperty("digital_voice_codecserver", self.dsp.set_codecserver), ] # TODO @@ -414,8 +434,7 @@ class DspManager(SdrSourceEventClient): self.subscriptions += [ self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator), self.props.wireProperty("digimodes_fft_size", self.chain.setSecondaryFftSize), - # TODO - # self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq), + self.props.wireProperty("secondary_offset_freq", self.chain.setSecondaryFrequencyOffset), ] self.startOnAvailable = False @@ -424,7 +443,7 @@ class DspManager(SdrSourceEventClient): super().__init__() - def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]): + def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]) -> Optional[BaseDemodulatorChain]: if isinstance(demod, BaseDemodulatorChain): return demod # TODO: move this to Modes @@ -486,7 +505,7 @@ class DspManager(SdrSourceEventClient): } ) - def _getSecondaryDemodulator(self, mod): + def _getSecondaryDemodulator(self, mod) -> Optional[SecondaryDemodulator]: if isinstance(mod, SecondaryDemodulator): return mod # TODO add remaining modes @@ -504,7 +523,12 @@ class DspManager(SdrSourceEventClient): elif mod == "pocsag": from csdr.chain.digimodes import PocsagDemodulator return PocsagDemodulator() - return None + elif mod == "bpsk31": + from csdr.chain.digimodes import PskDemodulator + return PskDemodulator(31.25) + elif mod == "bpsk63": + from csdr.chain.digimodes import PskDemodulator + return PskDemodulator(62.5) def setSecondaryDemodulator(self, mod): demodulator = self._getSecondaryDemodulator(mod) @@ -556,12 +580,16 @@ class DspManager(SdrSourceEventClient): def _unpickle(self, callback): def unpickler(data): - io = BytesIO(data.tobytes()) + b = data.tobytes() + io = BytesIO(b) try: while True: callback(pickle.load(io)) except EOFError: pass + # TODO: this is not ideal. is there a way to know beforehand if the data will be pickled? + except pickle.UnpicklingError: + callback(b.decode("ascii")) return unpickler diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py index e22c34e..e0ad540 100644 --- a/owrx/service/__init__.py +++ b/owrx/service/__init__.py @@ -8,8 +8,8 @@ from owrx.property import PropertyLayer, PropertyDeleted from owrx.service.schedule import ServiceScheduler from owrx.service.chain import ServiceDemodulatorChain from owrx.modes import Modes, DigitalMode -from typing import Union -from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, DialFrequencyReceiver +from typing import Union, Optional +from csdr.chain.demodulator import BaseDemodulatorChain, ServiceDemodulator, DialFrequencyReceiver from pycsdr.modules import Buffer import logging @@ -250,12 +250,11 @@ class ServiceHandler(SdrSourceEventClient): secondaryDemod = self._getSecondaryDemodulator(modeObject.modulation) center_freq = source.getProps()["center_freq"] sampleRate = source.getProps()["samp_rate"] - shift = (center_freq - frequency) / sampleRate bandpass = modeObject.get_bandpass() if isinstance(secondaryDemod, DialFrequencyReceiver): secondaryDemod.setDialFrequency(frequency) - chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, shift) + chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, frequency - center_freq) chain.setBandPass(bandpass.low_cut, bandpass.high_cut) chain.setReader(source.getBuffer().getReader()) @@ -277,8 +276,8 @@ class ServiceHandler(SdrSourceEventClient): return Ssb() # TODO move this elsewhere - def _getSecondaryDemodulator(self, mod): - if isinstance(mod, SecondaryDemodulator): + def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]: + if isinstance(mod, ServiceDemodulatorChain): return mod # TODO add remaining modes if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: diff --git a/owrx/service/chain.py b/owrx/service/chain.py index 549066a..f71cf2b 100644 --- a/owrx/service/chain.py +++ b/owrx/service/chain.py @@ -1,15 +1,13 @@ from csdr.chain import Chain from csdr.chain.selector import Selector -from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, FixedAudioRateChain +from csdr.chain.demodulator import BaseDemodulatorChain, ServiceDemodulator from pycsdr.types import Format class ServiceDemodulatorChain(Chain): - def __init__(self, demod: BaseDemodulatorChain, secondaryDemod: SecondaryDemodulator, sampleRate: int, shiftRate: float): - # TODO magic number... check if this edge case even exsists and change the api if possible - rate = secondaryDemod.getFixedAudioRate() if isinstance(secondaryDemod, FixedAudioRateChain) else 1200 - - self.selector = Selector(sampleRate, rate, shiftRate, withSquelch=False) + def __init__(self, demod: BaseDemodulatorChain, secondaryDemod: ServiceDemodulator, sampleRate: int, frequencyOffset: int): + self.selector = Selector(sampleRate, secondaryDemod.getFixedAudioRate(), withSquelch=False) + self.selector.setFrequencyOffset(frequencyOffset) workers = [self.selector]