def testEventOnLayerWithNewProperty(self):
     low_layer = PropertyLayer()
     low_layer["existingkey"] = "existing value"
     stack = PropertyStack()
     stack.addLayer(1, low_layer)
     mock = Mock()
     stack.wireProperty("newkey", mock.method)
     high_layer = PropertyLayer()
     high_layer["newkey"] = "new value"
     stack.addLayer(0, high_layer)
     mock.method.assert_called_once_with("new value")
 def testNoEventOnExistingValue(self):
     low_layer = PropertyLayer()
     low_layer["testkey"] = "same value"
     stack = PropertyStack()
     stack.addLayer(1, low_layer)
     mock = Mock()
     stack.wireProperty("testkey", mock.method)
     mock.reset_mock()
     high_layer = PropertyLayer()
     high_layer["testkey"] = "same value"
     stack.addLayer(0, high_layer)
     mock.method.assert_not_called()
 def testPropertyEventOnLayerAdd(self):
     low_layer = PropertyLayer()
     low_layer["testkey"] = "low value"
     stack = PropertyStack()
     stack.addLayer(1, low_layer)
     mock = Mock()
     stack.wireProperty("testkey", mock.method)
     mock.reset_mock()
     high_layer = PropertyLayer()
     high_layer["testkey"] = "high value"
     stack.addLayer(0, high_layer)
     mock.method.assert_called_once_with("high value")
    def testReplaceLayer(self):
        first_layer = PropertyLayer()
        first_layer["testkey"] = "old value"
        second_layer = PropertyLayer()
        second_layer["testkey"] = "new value"

        stack = PropertyStack()
        stack.addLayer(0, first_layer)

        mock = Mock()
        stack.wireProperty("testkey", mock.method)
        mock.method.assert_called_once_with("old value")
        mock.reset_mock()

        stack.replaceLayer(0, second_layer)
        mock.method.assert_called_once_with("new value")
    def testNoneOnKeyRemoval(self):
        low_layer = PropertyLayer()
        high_layer = PropertyLayer()
        stack = PropertyStack()
        stack.addLayer(1, low_layer)
        stack.addLayer(0, high_layer)
        low_layer["testkey"] = "low value"
        high_layer["testkey"] = "high value"
        high_layer["unique key"] = "unique value"

        mock = Mock()
        stack.wireProperty("unique key", mock.method)
        mock.method.assert_called_once_with("unique value")
        mock.reset_mock()
        stack.removeLayer(high_layer)
        mock.method.assert_called_once_with(None)
Example #6
0
class DspManager(csdr.output, SdrSourceEventClient):
    def __init__(self, handler, sdrSource):
        self.handler = handler
        self.sdrSource = sdrSource
        self.parsers = {
            "meta": MetaParser(self.handler),
            "wsjt_demod": WsjtParser(self.handler),
            "packet_demod": AprsParser(self.handler),
            "pocsag_demod": PocsagParser(self.handler),
            "js8_demod": Js8Parser(self.handler),
        }

        self.props = PropertyStack()

        # local demodulator properties not forwarded to the sdr
        # ensure strict validation since these can be set from the client
        # and are used to build executable commands
        validators = {
            "output_rate": "int",
            "hd_output_rate": "int",
            "squelch_level": "num",
            "secondary_mod": ModulationValidator(),
            "low_cut": "num",
            "high_cut": "num",
            "offset_freq": "int",
            "mod": ModulationValidator(),
            "secondary_offset_freq": "int",
            "dmr_filter": "int",
        }
        self.localProps = PropertyValidator(
            PropertyLayer().filter(*validators.keys()), validators)

        self.props.addLayer(0, self.localProps)
        # properties that we inherit from the sdr
        self.props.addLayer(
            1,
            self.sdrSource.getProps().filter(
                "audio_compression",
                "fft_compression",
                "digimodes_fft_size",
                "csdr_dynamic_bufsize",
                "csdr_print_bufsizes",
                "csdr_through",
                "digimodes_enable",
                "samp_rate",
                "digital_voice_unvoiced_quality",
                "temporary_directory",
                "center_freq",
                "start_mod",
                "start_freq",
                "wfm_deemphasis_tau",
            ))

        self.dsp = csdr.dsp(self)
        self.dsp.nc_port = self.sdrSource.getPort()

        def set_low_cut(cut):
            bpf = self.dsp.get_bpf()
            bpf[0] = cut
            self.dsp.set_bpf(*bpf)

        def set_high_cut(cut):
            bpf = self.dsp.get_bpf()
            bpf[1] = cut
            self.dsp.set_bpf(*bpf)

        def set_dial_freq(key, value):
            if self.props["center_freq"] is None or self.props[
                    "offset_freq"] is None:
                return
            freq = self.props["center_freq"] + self.props["offset_freq"]
            for parser in self.parsers.values():
                parser.setDialFrequency(freq)

        if "start_mod" in self.props:
            self.dsp.set_demodulator(self.props["start_mod"])
            mode = Modes.findByModulation(self.props["start_mod"])

            if mode and mode.bandpass:
                self.dsp.set_bpf(mode.bandpass.low_cut, mode.bandpass.high_cut)
            else:
                self.dsp.set_bpf(-4000, 4000)

        if "start_freq" in self.props and "center_freq" in self.props:
            self.dsp.set_offset_freq(self.props["start_freq"] -
                                     self.props["center_freq"])
        else:
            self.dsp.set_offset_freq(0)

        self.subscriptions = [
            self.props.wireProperty("audio_compression",
                                    self.dsp.set_audio_compression),
            self.props.wireProperty("fft_compression",
                                    self.dsp.set_fft_compression),
            self.props.wireProperty("digimodes_fft_size",
                                    self.dsp.set_secondary_fft_size),
            self.props.wireProperty("samp_rate", self.dsp.set_samp_rate),
            self.props.wireProperty("output_rate", self.dsp.set_output_rate),
            self.props.wireProperty("hd_output_rate",
                                    self.dsp.set_hd_output_rate),
            self.props.wireProperty("offset_freq", self.dsp.set_offset_freq),
            self.props.wireProperty("center_freq", self.dsp.set_center_freq),
            self.props.wireProperty("squelch_level",
                                    self.dsp.set_squelch_level),
            self.props.wireProperty("low_cut", set_low_cut),
            self.props.wireProperty("high_cut", set_high_cut),
            self.props.wireProperty("mod", self.dsp.set_demodulator),
            self.props.wireProperty("digital_voice_unvoiced_quality",
                                    self.dsp.set_unvoiced_quality),
            self.props.wireProperty("dmr_filter", self.dsp.set_dmr_filter),
            self.props.wireProperty("temporary_directory",
                                    self.dsp.set_temporary_directory),
            self.props.wireProperty("wfm_deemphasis_tau",
                                    self.dsp.set_wfm_deemphasis_tau),
            self.props.filter("center_freq",
                              "offset_freq").wire(set_dial_freq),
        ]

        self.dsp.csdr_dynamic_bufsize = self.props["csdr_dynamic_bufsize"]
        self.dsp.csdr_print_bufsizes = self.props["csdr_print_bufsizes"]
        self.dsp.csdr_through = self.props["csdr_through"]

        if self.props["digimodes_enable"]:

            def set_secondary_mod(mod):
                if mod == False:
                    mod = None
                self.dsp.set_secondary_demodulator(mod)
                if mod is not None:
                    self.handler.write_secondary_dsp_config({
                        "secondary_fft_size":
                        self.props["digimodes_fft_size"],
                        "if_samp_rate":
                        self.dsp.if_samp_rate(),
                        "secondary_bw":
                        self.dsp.secondary_bw(),
                    })

            self.subscriptions += [
                self.props.wireProperty("secondary_mod", set_secondary_mod),
                self.props.wireProperty("secondary_offset_freq",
                                        self.dsp.set_secondary_offset_freq),
            ]

        self.startOnAvailable = False

        self.sdrSource.addClient(self)

        super().__init__()

    def start(self):
        if self.sdrSource.isAvailable():
            self.dsp.start()
        else:
            self.startOnAvailable = True

    def receive_output(self, t, read_fn):
        logger.debug("adding new output of type %s", t)
        writers = {
            "audio": self.handler.write_dsp_data,
            "hd_audio": self.handler.write_hd_audio,
            "smeter": self.handler.write_s_meter_level,
            "secondary_fft": self.handler.write_secondary_fft,
            "secondary_demod": self.handler.write_secondary_demod,
        }
        for demod, parser in self.parsers.items():
            writers[demod] = parser.parse

        write = writers[t]

        threading.Thread(target=self.pump(read_fn, write),
                         name="dsp_pump_{}".format(t)).start()

    def stop(self):
        self.dsp.stop()
        self.startOnAvailable = False
        self.sdrSource.removeClient(self)
        for sub in self.subscriptions:
            sub.cancel()
        self.subscriptions = []

    def setProperties(self, props):
        for k, v in props.items():
            self.setProperty(k, v)

    def setProperty(self, prop, value):
        self.localProps[prop] = value

    def getClientClass(self):
        return SdrSource.CLIENT_USER

    def onStateChange(self, state):
        if state == SdrSource.STATE_RUNNING:
            logger.debug(
                "received STATE_RUNNING, attempting DspSource restart")
            if self.startOnAvailable:
                self.dsp.start()
                self.startOnAvailable = False
        elif state == SdrSource.STATE_STOPPING:
            logger.debug("received STATE_STOPPING, shutting down DspSource")
            self.dsp.stop()
        elif state == SdrSource.STATE_FAILED:
            logger.debug("received STATE_FAILED, shutting down DspSource")
            self.dsp.stop()

    def onBusyStateChange(self, state):
        pass
Example #7
0
class DspManager(SdrSourceEventClient,
                 ClientDemodulatorSecondaryDspEventClient):
    def __init__(self, handler, sdrSource):
        self.handler = handler
        self.sdrSource = sdrSource

        self.props = PropertyStack()

        # current audio mode. should be "audio" or "hd_audio" depending on what demodulatur is in use.
        self.audioOutput = None

        # local demodulator properties not forwarded to the sdr
        # ensure strict validation since these can be set from the client
        # and are used to build executable commands
        validators = {
            "output_rate": "int",
            "hd_output_rate": "int",
            "squelch_level": "num",
            "secondary_mod": ModulationValidator(),
            "low_cut": "num",
            "high_cut": "num",
            "offset_freq": "int",
            "mod": ModulationValidator(),
            "secondary_offset_freq": "int",
            "dmr_filter": "int",
        }
        self.localProps = PropertyValidator(
            PropertyLayer().filter(*validators.keys()), validators)

        self.props.addLayer(0, self.localProps)
        # properties that we inherit from the sdr
        self.props.addLayer(
            1,
            self.sdrSource.getProps().filter(
                "audio_compression",
                "fft_compression",
                "digimodes_fft_size",
                "samp_rate",
                "center_freq",
                "start_mod",
                "start_freq",
                "wfm_deemphasis_tau",
                "digital_voice_codecserver",
            ),
        )

        # defaults for values that may not be set
        self.props.addLayer(
            2,
            PropertyLayer(
                output_rate=12000,
                hd_output_rate=48000,
                digital_voice_codecserver="",
            ).readonly())

        self.chain = ClientDemodulatorChain(self._getDemodulator("nfm"),
                                            self.props["samp_rate"],
                                            self.props["output_rate"],
                                            self.props["hd_output_rate"],
                                            self.props["audio_compression"],
                                            self)

        self.readers = {}

        if "start_mod" in self.props:
            mode = Modes.findByModulation(self.props["start_mod"])
            if mode:
                self.setDemodulator(mode.get_modulation())
                if isinstance(mode, DigitalMode):
                    self.setSecondaryDemodulator(mode.modulation)
                if mode.bandpass:
                    bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut]
                    self.chain.setBandpass(*bpf)
            else:
                # TODO modes should be mandatory
                self.setDemodulator(self.props["start_mod"])

        if "start_freq" in self.props and "center_freq" in self.props:
            self.chain.setFrequencyOffset(self.props["start_freq"] -
                                          self.props["center_freq"])
        else:
            self.chain.setFrequencyOffset(0)

        self.subscriptions = [
            self.props.wireProperty("audio_compression",
                                    self.setAudioCompression),
            self.props.wireProperty("fft_compression",
                                    self.chain.setSecondaryFftCompression),
            self.props.wireProperty("fft_voverlap_factor",
                                    self.chain.setSecondaryFftOverlapFactor),
            self.props.wireProperty("fft_fps", self.chain.setSecondaryFftFps),
            self.props.wireProperty("digimodes_fft_size",
                                    self.setSecondaryFftSize),
            self.props.wireProperty("samp_rate", self.chain.setSampleRate),
            self.props.wireProperty("output_rate", self.chain.setOutputRate),
            self.props.wireProperty("hd_output_rate",
                                    self.chain.setHdOutputRate),
            self.props.wireProperty("offset_freq",
                                    self.chain.setFrequencyOffset),
            self.props.wireProperty("center_freq",
                                    self.chain.setCenterFrequency),
            self.props.wireProperty("squelch_level",
                                    self.chain.setSquelchLevel),
            self.props.wireProperty("low_cut", self.chain.setLowCut),
            self.props.wireProperty("high_cut", self.chain.setHighCut),
            self.props.wireProperty("mod", self.setDemodulator),
            self.props.wireProperty("dmr_filter", self.chain.setSlotFilter),
            self.props.wireProperty("wfm_deemphasis_tau",
                                    self.chain.setWfmDeemphasisTau),
            self.props.wireProperty("secondary_mod",
                                    self.setSecondaryDemodulator),
            self.props.wireProperty("secondary_offset_freq",
                                    self.chain.setSecondaryFrequencyOffset),
        ]

        # wire power level output
        buffer = Buffer(Format.FLOAT)
        self.chain.setPowerWriter(buffer)
        self.wireOutput("smeter", buffer)

        # wire meta output
        buffer = Buffer(Format.CHAR)
        self.chain.setMetaWriter(buffer)
        self.wireOutput("meta", buffer)

        # wire secondary FFT
        buffer = Buffer(self.chain.getSecondaryFftOutputFormat())
        self.chain.setSecondaryFftWriter(buffer)
        self.wireOutput("secondary_fft", buffer)

        # wire secondary demodulator
        buffer = Buffer(Format.CHAR)
        self.chain.setSecondaryWriter(buffer)
        self.wireOutput("secondary_demod", buffer)

        self.startOnAvailable = False

        self.sdrSource.addClient(self)

    def setSecondaryFftSize(self, size):
        self.chain.setSecondaryFftSize(size)
        self.handler.write_secondary_dsp_config({"secondary_fft_size": size})

    def _getDemodulator(
        self,
        demod: Union[str,
                     BaseDemodulatorChain]) -> Optional[BaseDemodulatorChain]:
        if isinstance(demod, BaseDemodulatorChain):
            return demod
        # TODO: move this to Modes
        if demod == "nfm":
            from csdr.chain.analog import NFm
            return NFm(self.props["output_rate"])
        elif demod == "wfm":
            from csdr.chain.analog import WFm
            return WFm(self.props["hd_output_rate"],
                       self.props["wfm_deemphasis_tau"])
        elif demod == "am":
            from csdr.chain.analog import Am
            return Am()
        elif demod in ["usb", "lsb", "cw"]:
            from csdr.chain.analog import Ssb
            return Ssb()
        elif demod == "dmr":
            from csdr.chain.digiham import Dmr
            return Dmr(self.props["digital_voice_codecserver"])
        elif demod == "dstar":
            from csdr.chain.digiham import Dstar
            return Dstar(self.props["digital_voice_codecserver"])
        elif demod == "ysf":
            from csdr.chain.digiham import Ysf
            return Ysf(self.props["digital_voice_codecserver"])
        elif demod == "nxdn":
            from csdr.chain.digiham import Nxdn
            return Nxdn(self.props["digital_voice_codecserver"])
        elif demod == "m17":
            from csdr.chain.m17 import M17
            return M17()
        elif demod == "drm":
            from csdr.chain.drm import Drm
            return Drm()
        elif demod == "freedv":
            from csdr.chain.freedv import FreeDV
            return FreeDV()

    def setDemodulator(self, mod):
        self.chain.stopDemodulator()
        try:
            demodulator = self._getDemodulator(mod)
            if demodulator is None:
                raise ValueError("unsupported demodulator: {}".format(mod))
            self.chain.setDemodulator(demodulator)

            output = "hd_audio" if isinstance(demodulator,
                                              HdAudio) else "audio"

            if output != self.audioOutput:
                self.audioOutput = output
                # re-wire the audio to the correct client API
                buffer = Buffer(self.chain.getOutputFormat())
                self.chain.setWriter(buffer)
                self.wireOutput(self.audioOutput, buffer)
        except DemodulatorError as de:
            self.handler.write_demodulator_error(str(de))

    def _getSecondaryDemodulator(self, mod) -> Optional[SecondaryDemodulator]:
        if isinstance(mod, SecondaryDemodulator):
            return mod
        if mod in [
                "ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"
        ]:
            from csdr.chain.digimodes import AudioChopperDemodulator
            from owrx.wsjt import WsjtParser
            return AudioChopperDemodulator(mod, WsjtParser())
        elif mod == "js8":
            from csdr.chain.digimodes import AudioChopperDemodulator
            from owrx.js8 import Js8Parser
            return AudioChopperDemodulator(mod, Js8Parser())
        elif mod == "packet":
            from csdr.chain.digimodes import PacketDemodulator
            return PacketDemodulator()
        elif mod == "pocsag":
            from csdr.chain.digimodes import PocsagDemodulator
            return PocsagDemodulator()
        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)
        if not demodulator:
            self.chain.setSecondaryDemodulator(None)
        else:
            self.chain.setSecondaryDemodulator(demodulator)

    def setAudioCompression(self, comp):
        try:
            self.chain.setAudioCompression(comp)
        except ValueError:
            # wrong output format... need to re-wire
            buffer = Buffer(self.chain.getOutputFormat())
            self.chain.setWriter(buffer)
            self.wireOutput(self.audioOutput, buffer)

    def start(self):
        if self.sdrSource.isAvailable():
            self.chain.setReader(self.sdrSource.getBuffer().getReader())
        else:
            self.startOnAvailable = True

    def unwireOutput(self, t: str):
        if t in self.readers:
            self.readers[t].stop()
            del self.readers[t]

    def wireOutput(self, t: str, buffer: Buffer):
        logger.debug("wiring new output of type %s", t)
        writers = {
            "audio": self.handler.write_dsp_data,
            "hd_audio": self.handler.write_hd_audio,
            "smeter": self.handler.write_s_meter_level,
            "secondary_fft": self.handler.write_secondary_fft,
            "secondary_demod":
            self._unpickle(self.handler.write_secondary_demod),
            "meta": self._unpickle(self.handler.write_metadata),
        }

        write = writers[t]

        self.unwireOutput(t)

        reader = buffer.getReader()
        self.readers[t] = reader
        threading.Thread(target=self.chain.pump(reader.read, write),
                         name="dsp_pump_{}".format(t)).start()

    def _unpickle(self, callback):
        def unpickler(data):
            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

    def stop(self):
        if self.chain:
            self.chain.stop()
            self.chain = None
        for reader in self.readers.values():
            reader.stop()
        self.readers = {}

        self.startOnAvailable = False
        self.sdrSource.removeClient(self)
        for sub in self.subscriptions:
            sub.cancel()
        self.subscriptions = []

    def setProperties(self, props):
        for k, v in props.items():
            self.setProperty(k, v)

    def setProperty(self, prop, value):
        self.localProps[prop] = value

    def getClientClass(self) -> SdrClientClass:
        return SdrClientClass.USER

    def onStateChange(self, state: SdrSourceState):
        if state is SdrSourceState.RUNNING:
            logger.debug(
                "received STATE_RUNNING, attempting DspSource restart")
            if self.startOnAvailable:
                self.chain.setReader(self.sdrSource.getBuffer().getReader())
                self.startOnAvailable = False
        elif state is SdrSourceState.STOPPING:
            logger.debug("received STATE_STOPPING, shutting down DspSource")
            self.stop()

    def onFail(self):
        logger.debug("received onFail(), shutting down DspSource")
        self.stop()

    def onShutdown(self):
        self.stop()

    def onSecondaryDspBandwidthChange(self, bw):
        self.handler.write_secondary_dsp_config({"secondary_bw": bw})

    def onSecondaryDspRateChange(self, rate):
        self.handler.write_secondary_dsp_config({"if_samp_rate": rate})