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