def __init__(self, id, props): self.id = id self.commandMapper = None self.props = PropertyStack() # layer 0 reserved for profile properties self.props.addLayer(1, props) self.props.addLayer(2, Config.get()) self.sdrProps = self.props.filter(*self.getEventNames()) self.profile_id = None self.activateProfile() self.wireEvents() if "port" in props and props["port"] is not None: self.port = props["port"] else: self.port = getAvailablePort() self.monitor = None self.clients = [] self.spectrumClients = [] self.spectrumThread = None self.process = None self.modificationLock = threading.Lock() self.failed = False self.state = SdrSource.STATE_STOPPED self.busyState = SdrSource.BUSYSTATE_IDLE if self.isAlwaysOn(): self.start()
def testEventOnLayerRemoval(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" mock = Mock() stack.wireProperty("testkey", mock.method) mock.method.assert_called_once_with("high value") mock.reset_mock() stack.removeLayer(high_layer) mock.method.assert_called_once_with("low 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(PropertyDeleted)
def __init__(self, id, props): self.id = id self.commandMapper = None self.props = PropertyStack() # layer 0 reserved for profile properties self.profileCarousel = SdrProfileCarousel(props) # prevent profile names from overriding the device name self.props.addLayer( 0, PropertyFilter(self.profileCarousel, ByLambda(lambda x: x != "name"))) # props from our device config self.props.addLayer(1, props) # the sdr_id is constant, so we put it in a separate layer # this is used to detect device changes, that are then sent to the client self.props.addLayer(2, PropertyLayer(sdr_id=id).readonly()) # finally, accept global config properties from the top-level config self.props.addLayer(3, Config.get()) self.sdrProps = self.props.filter(*self.getEventNames()) self.wireEvents() self.port = getAvailablePort() self.monitor = None self.clients = [] self.spectrumClients = [] self.spectrumThread = None self.spectrumLock = threading.Lock() self.process = None self.modificationLock = threading.Lock() self.state = SdrSourceState.STOPPED self.enabled = "enabled" not in props or props["enabled"] props.filter("enabled").wire(self._handleEnableChanged) self.failed = False self.busyState = SdrBusyState.IDLE self.validateProfiles() if self.isAlwaysOn() and self.isEnabled(): self.start()
def testReplaceLayerNoEventWhenValueUnchanged(self): fixed = PropertyLayer() fixed["testkey"] = "fixed value" first_layer = PropertyLayer() first_layer["testkey"] = "same value" second_layer = PropertyLayer() second_layer["testkey"] = "same value" stack = PropertyStack() stack.addLayer(1, fixed) stack.addLayer(0, first_layer) mock = Mock() stack.wire(mock.method) mock.method.assert_not_called() stack.replaceLayer(0, second_layer) mock.method.assert_not_called()
def testChangeEventWhenKeyDeleted(self): ps = PropertyStack() low_pm = PropertyLayer(testkey="lowvalue") high_pm = PropertyLayer(testkey="highvalue") ps.addLayer(0, high_pm) ps.addLayer(1, low_pm) mock = Mock() ps.wire(mock.method) del high_pm["testkey"] mock.method.assert_called_once_with({"testkey": "lowvalue"})
def testDeletionWithSecondLayer(self): ps = PropertyStack() low_pm = PropertyLayer(testkey="testvalue") high_pm = PropertyLayer() ps.addLayer(0, high_pm) ps.addLayer(1, low_pm) mock = Mock() ps.wire(mock.method) del low_pm["testkey"] mock.method.assert_called_once_with({"testkey": PropertyDeleted})
def handleSdrAvailable(self): # send initial config self.getDsp().setProperties(self.connectionProperties) stack = PropertyStack() stack.addLayer(0, self.sdr.getProps()) stack.addLayer(1, Config.get()) configProps = stack.filter(*OpenWebRxReceiverClient.config_keys) def sendConfig(key, value): config = configProps.__dict__() # TODO mathematical properties? hmmmm config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] # TODO this is a hack to support multiple sdrs config["sdr_id"] = self.sdr.getId() self.write_config(config) cf = configProps["center_freq"] srh = configProps["samp_rate"] / 2 frequencyRange = (cf - srh, cf + srh) self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange)) bookmarks = [b.__dict__() for b in Bookmarks.getSharedInstance().getBookmarks(frequencyRange)] self.write_bookmarks(bookmarks) self.configSub = configProps.wire(sendConfig) sendConfig(None, None) self.__sendProfiles() self.sdr.addSpectrumClient(self)
def testLayerRemoval(self): om = PropertyStack() low_pm = PropertyLayer() high_pm = PropertyLayer() low_pm["testkey"] = "low value" high_pm["testkey"] = "high value" om.addLayer(1, low_pm) om.addLayer(0, high_pm) self.assertEqual(om["testkey"], "high value") om.removeLayer(high_pm) self.assertEqual(om["testkey"], "low value")
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 testUnwiresEventsOnRemoval(self): layer = PropertyLayer() layer["testkey"] = "before" stack = PropertyStack() stack.addLayer(0, layer) mock = Mock() stack.wire(mock.method) stack.removeLayer(layer) mock.method.assert_called_once_with("testkey", None) mock.reset_mock() layer["testkey"] = "after" mock.method.assert_not_called()
def testPropertyChangeEventPriority(self): low_layer = PropertyLayer() high_layer = PropertyLayer() low_layer["testkey"] = "initial low value" high_layer["testkey"] = "initial high value" stack = PropertyStack() stack.addLayer(1, low_layer) stack.addLayer(0, high_layer) mock = Mock() stack.wire(mock.method) low_layer["testkey"] = "modified low value" mock.method.assert_not_called() high_layer["testkey"] = "modified high value" mock.method.assert_called_once_with("testkey", "modified high value")
def testPropertyChange(self): layer = PropertyLayer() stack = PropertyStack() stack.addLayer(0, layer) mock = Mock() stack.wire(mock.method) layer["testkey"] = "testvalue" mock.method.assert_called_once_with("testkey", "testvalue")
def testPriorityFallback(self): om = PropertyStack() low_pm = PropertyLayer() high_pm = PropertyLayer() low_pm["testkey"] = "low value" om.addLayer(1, low_pm) om.addLayer(0, high_pm) self.assertEqual(om["testkey"], "low value")
def testDeletionEvent(self): ps = PropertyStack() pm = PropertyLayer(testkey="testvalue") ps.addLayer(0, pm) mock = Mock() ps.wire(mock.method) del ps["testkey"] mock.method.assert_called_once_with({"testkey": PropertyDeleted})
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 testWritesToExpectedLayer(self): om = PropertyStack() low_pm = PropertyLayer() high_pm = PropertyLayer() low_pm["testkey"] = "low value" om.addLayer(1, low_pm) om.addLayer(0, high_pm) om["testkey"] = "new value" self.assertEqual(low_pm["testkey"], "new value")
def __init__(self, sdrSource): self.sdrSource = sdrSource super().__init__() stack = PropertyStack() stack.addLayer(0, self.sdrSource.props) stack.addLayer(1, Config.get()) self.props = props = stack.filter( "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through", "temporary_directory", ) self.dsp = dsp = csdr.dsp(self) dsp.nc_port = self.sdrSource.getPort() dsp.set_demodulator("fft") def set_fft_averages(key, value): samp_rate = props["samp_rate"] fft_size = props["fft_size"] fft_fps = props["fft_fps"] fft_voverlap_factor = props["fft_voverlap_factor"] dsp.set_fft_averages( int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) if fft_voverlap_factor > 0 else 0 ) self.subscriptions = [ props.wireProperty("samp_rate", dsp.set_samp_rate), props.wireProperty("fft_size", dsp.set_fft_size), props.wireProperty("fft_fps", dsp.set_fft_fps), props.wireProperty("fft_compression", dsp.set_fft_compression), props.wireProperty("temporary_directory", dsp.set_temporary_directory), props.filter("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages), ] set_fft_averages(None, None) dsp.csdr_dynamic_bufsize = props["csdr_dynamic_bufsize"] dsp.csdr_print_bufsizes = props["csdr_print_bufsizes"] dsp.csdr_through = props["csdr_through"] logger.debug("Spectrum thread initialized successfully.")
def setParams(self, params): config = Config.get() # allow direct configuration only if enabled in the config if "configurable_keys" not in config: return keys = config["configurable_keys"] if not keys: return # only the keys in the protected property manager can be overridden from the web stack = PropertyStack() stack.addLayer(0, self.sdr.getProps()) stack.addLayer(1, config) protected = stack.filter(*keys) for key, value in params.items(): try: protected[key] = value except KeyError: pass
def validateProfiles(self): props = PropertyStack() props.addLayer(1, self.props) for id, p in self.props["profiles"].items(): props.replaceLayer(0, p) if "center_freq" not in props: logger.warning('Profile "%s" does not specify a center_freq', id) continue if "samp_rate" not in props: logger.warning('Profile "%s" does not specify a samp_rate', id) continue if "start_freq" in props: start_freq = props["start_freq"] srh = props["samp_rate"] / 2 center_freq = props["center_freq"] if start_freq < center_freq - srh or start_freq > center_freq + srh: logger.warning('start_freq for profile "%s" is out of range', id)
def __init__(self, sdrSource): self.sdrSource = sdrSource super().__init__() stack = PropertyStack() stack.addLayer(0, self.sdrSource.props) stack.addLayer(1, Config.get()) self.props = stack.filter( "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", ) self.dsp = None self.reader = None self.subscriptions = [] logger.debug("Spectrum thread initialized successfully.")
def addLayer(self, profile_id, profile): profile_stack = PropertyStack() profile_stack.addLayer(0, PropertyLayer(profile_id=profile_id).readonly()) profile_stack.addLayer(1, profile) super().addLayer(profile_id, profile_stack)
class SdrSource(ABC): STATE_STOPPED = 0 STATE_STARTING = 1 STATE_RUNNING = 2 STATE_STOPPING = 3 STATE_TUNING = 4 STATE_FAILED = 5 BUSYSTATE_IDLE = 0 BUSYSTATE_BUSY = 1 CLIENT_INACTIVE = 0 CLIENT_BACKGROUND = 1 CLIENT_USER = 2 def __init__(self, id, props): self.id = id self.commandMapper = None self.props = PropertyStack() # layer 0 reserved for profile properties self.props.addLayer(1, props) self.props.addLayer(2, Config.get()) self.sdrProps = self.props.filter(*self.getEventNames()) self.profile_id = None self.activateProfile() self.wireEvents() if "port" in props and props["port"] is not None: self.port = props["port"] else: self.port = getAvailablePort() self.monitor = None self.clients = [] self.spectrumClients = [] self.spectrumThread = None self.process = None self.modificationLock = threading.Lock() self.failed = False self.state = SdrSource.STATE_STOPPED self.busyState = SdrSource.BUSYSTATE_IDLE if self.isAlwaysOn(): self.start() def isAlwaysOn(self): return "always-on" in self.props and self.props["always-on"] def getEventNames(self): return [ "samp_rate", "center_freq", "ppm", "rf_gain", "lfo_offset", ] + list(self.getCommandMapper().keys()) def getCommandMapper(self): if self.commandMapper is None: self.commandMapper = CommandMapper() return self.commandMapper @abstractmethod def onPropertyChange(self, name, value): pass def wireEvents(self): self.sdrProps.wire(self.onPropertyChange) def getCommand(self): return [self.getCommandMapper().map(self.getCommandValues())] def activateProfile(self, profile_id=None): profiles = self.props["profiles"] if profile_id is None: profile_id = list(profiles.keys())[0] if profile_id not in profiles: logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.id) return if profile_id == self.profile_id: return logger.debug("activating profile {0}".format(profile_id)) self.props["profile_id"] = profile_id profile = profiles[profile_id] self.profile_id = profile_id layer = PropertyLayer() for (key, value) in profile.items(): # skip the name, that would overwrite the source name. if key == "name": continue layer[key] = value self.props.replaceLayer(0, layer) def getId(self): return self.id def getProfileId(self): return self.profile_id def getProfiles(self): return self.props["profiles"] def getName(self): return self.props["name"] def getProps(self): return self.props def getPort(self): return self.port def getCommandValues(self): dict = self.sdrProps.__dict__() if "lfo_offset" in dict and dict["lfo_offset"] is not None: dict["tuner_freq"] = dict["center_freq"] + dict["lfo_offset"] else: dict["tuner_freq"] = dict["center_freq"] return dict def start(self): with self.modificationLock: if self.monitor: return try: self.preStart() except Exception: logger.exception("Exception during preStart()") cmd = self.getCommand() cmd = [c for c in cmd if c is not None] # don't use shell mode for commands without piping if len(cmd) > 1: # multiple commands with pipes cmd = "|".join(cmd) self.process = subprocess.Popen(cmd, shell=True, start_new_session=True) else: # single command cmd = cmd[0] # start_new_session can go as soon as there's no piped commands left # the os.killpg call must be replaced with something more reasonable at the same time self.process = subprocess.Popen(shlex.split(cmd), start_new_session=True) logger.info("Started sdr source: " + cmd) available = False def wait_for_process_to_end(): rc = self.process.wait() logger.debug("shut down with RC={0}".format(rc)) self.monitor = None self.monitor = threading.Thread(target=wait_for_process_to_end) self.monitor.start() retries = 1000 while retries > 0: retries -= 1 if self.monitor is None: break testsock = socket.socket() try: testsock.connect(("127.0.0.1", self.getPort())) testsock.close() available = True break except: time.sleep(0.1) if not available: self.failed = True try: self.postStart() except Exception: logger.exception("Exception during postStart()") self.failed = True self.setState(SdrSource.STATE_FAILED if self.failed else SdrSource.STATE_RUNNING) def preStart(self): """ override this method in subclasses if there's anything to be done before starting up the actual SDR """ pass def postStart(self): """ override this method in subclasses if there's things to do after the actual SDR has started up """ pass def isAvailable(self): return self.monitor is not None def isFailed(self): return self.failed def stop(self): self.setState(SdrSource.STATE_STOPPING) with self.modificationLock: if self.process is not None: try: os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) except ProcessLookupError: # been killed by something else, ignore pass if self.monitor: self.monitor.join() self.setState(SdrSource.STATE_STOPPED) def hasClients(self, *args): clients = [c for c in self.clients if c.getClientClass() in args] return len(clients) > 0 def addClient(self, c): self.clients.append(c) hasUsers = self.hasClients(SdrSource.CLIENT_USER) hasBackgroundTasks = self.hasClients(SdrSource.CLIENT_BACKGROUND) if hasUsers or hasBackgroundTasks: self.start() self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE) def removeClient(self, c): try: self.clients.remove(c) except ValueError: pass # no need to check for users if we are always-on if self.isAlwaysOn(): return hasUsers = self.hasClients(SdrSource.CLIENT_USER) hasBackgroundTasks = self.hasClients(SdrSource.CLIENT_BACKGROUND) self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE) if not hasUsers and not hasBackgroundTasks: self.stop() def addSpectrumClient(self, c): self.spectrumClients.append(c) if self.spectrumThread is None: # local import due to circular depencency from owrx.fft import SpectrumThread self.spectrumThread = SpectrumThread(self) self.spectrumThread.start() def removeSpectrumClient(self, c): try: self.spectrumClients.remove(c) except ValueError: pass if not self.spectrumClients and self.spectrumThread is not None: self.spectrumThread.stop() self.spectrumThread = None def writeSpectrumData(self, data): for c in self.spectrumClients: c.write_spectrum_data(data) def getState(self): return self.state def setState(self, state): if state == self.state: return self.state = state for c in self.clients: c.onStateChange(state) def setBusyState(self, state): if state == self.busyState: return self.busyState = state for c in self.clients: c.onBusyStateChange(state)
def setupStack(self): stack = PropertyStack() # stack layer 0 reserved for sdr properties # stack.addLayer(0, self.sdr.getProps()) stack.addLayer(1, Config.get()) configProps = stack.filter(*OpenWebRxReceiverClient.sdr_config_keys) def sendConfig(changes=None): if changes is None: config = configProps.__dict__() else: # transform deletions into Nones config = { k: v if v is not PropertyDeleted else None for k, v in changes.items() } if ((changes is None or "start_freq" in changes or "center_freq" in changes) and "start_freq" in configProps and "center_freq" in configProps): config["start_offset_freq"] = configProps[ "start_freq"] - configProps["center_freq"] if (changes is None or "profile_id" in changes) and self.sdr is not None: config["sdr_id"] = self.sdr.getId() self.write_config(config) def sendBookmarks(*args): cf = configProps["center_freq"] srh = configProps["samp_rate"] / 2 dial_frequencies = [] bookmarks = [] if "center_freq" in configProps and "samp_rate" in configProps: frequencyRange = (cf - srh, cf + srh) dial_frequencies = Bandplan.getSharedInstance( ).collectDialFrequencies(frequencyRange) bookmarks = [ b.__dict__() for b in Bookmarks.getSharedInstance().getBookmarks(frequencyRange) ] self.write_dial_frequencies(dial_frequencies) self.write_bookmarks(bookmarks) def updateBookmarkSubscription(*args): if self.bookmarkSub is not None: self.bookmarkSub.cancel() if "center_freq" in configProps and "samp_rate" in configProps: cf = configProps["center_freq"] srh = configProps["samp_rate"] / 2 frequencyRange = (cf - srh, cf + srh) self.bookmarkSub = Bookmarks.getSharedInstance().subscribe( frequencyRange, sendBookmarks) sendBookmarks() self.configSubs.append(configProps.wire(sendConfig)) self.configSubs.append( stack.filter("center_freq", "samp_rate").wire(updateBookmarkSubscription)) # send initial config sendConfig() return stack
def testLayer(self): om = PropertyStack() pm = PropertyLayer() pm["testkey"] = "testvalue" om.addLayer(1, pm) self.assertEqual(om["testkey"], "testvalue")
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__()
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 SdrSource(ABC): def __init__(self, id, props): self.id = id self.commandMapper = None self.props = PropertyStack() # layer 0 reserved for profile properties self.profileCarousel = SdrProfileCarousel(props) # prevent profile names from overriding the device name self.props.addLayer( 0, PropertyFilter(self.profileCarousel, ByLambda(lambda x: x != "name"))) # props from our device config self.props.addLayer(1, props) # the sdr_id is constant, so we put it in a separate layer # this is used to detect device changes, that are then sent to the client self.props.addLayer(2, PropertyLayer(sdr_id=id).readonly()) # finally, accept global config properties from the top-level config self.props.addLayer(3, Config.get()) self.sdrProps = self.props.filter(*self.getEventNames()) self.wireEvents() self.port = getAvailablePort() self.monitor = None self.clients = [] self.spectrumClients = [] self.spectrumThread = None self.spectrumLock = threading.Lock() self.process = None self.modificationLock = threading.Lock() self.state = SdrSourceState.STOPPED self.enabled = "enabled" not in props or props["enabled"] props.filter("enabled").wire(self._handleEnableChanged) self.failed = False self.busyState = SdrBusyState.IDLE self.validateProfiles() if self.isAlwaysOn() and self.isEnabled(): self.start() def isEnabled(self): return self.enabled def _handleEnableChanged(self, changes): if "enabled" in changes and changes["enabled"] is not PropertyDeleted: self.enabled = changes["enabled"] else: self.enabled = True if not self.enabled: self.stop() for c in self.clients.copy(): if self.isEnabled(): c.onEnable() else: c.onDisable() def isFailed(self): return self.failed def fail(self): self.failed = True for c in self.clients.copy(): c.onFail() def validateProfiles(self): props = PropertyStack() props.addLayer(1, self.props) for id, p in self.props["profiles"].items(): props.replaceLayer(0, p) if "center_freq" not in props: logger.warning('Profile "%s" does not specify a center_freq', id) continue if "samp_rate" not in props: logger.warning('Profile "%s" does not specify a samp_rate', id) continue if "start_freq" in props: start_freq = props["start_freq"] srh = props["samp_rate"] / 2 center_freq = props["center_freq"] if start_freq < center_freq - srh or start_freq > center_freq + srh: logger.warning( 'start_freq for profile "%s" is out of range', id) def isAlwaysOn(self): return "always-on" in self.props and self.props["always-on"] def getEventNames(self): return [ "samp_rate", "center_freq", "ppm", "rf_gain", "lfo_offset", ] + list(self.getCommandMapper().keys()) def getCommandMapper(self): if self.commandMapper is None: self.commandMapper = CommandMapper() return self.commandMapper @abstractmethod def onPropertyChange(self, changes): pass def wireEvents(self): self.sdrProps.wire(self.onPropertyChange) def getCommand(self): return [self.getCommandMapper().map(self.getCommandValues())] def activateProfile(self, profile_id): logger.debug("activating profile {0} for {1}".format( profile_id, self.getId())) try: self.profileCarousel.switch(profile_id) except KeyError: logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.getId()) def getId(self): return self.id def getProfileId(self): return self.props["profile_id"] def getProfiles(self): return self.props["profiles"] def getName(self): return self.props["name"] def getProps(self): return self.props def getPort(self): return self.port def getCommandValues(self): dict = self.sdrProps.__dict__() if "lfo_offset" in dict and dict["lfo_offset"] is not None: dict["tuner_freq"] = dict["center_freq"] + dict["lfo_offset"] else: dict["tuner_freq"] = dict["center_freq"] return dict def start(self): with self.modificationLock: if self.monitor: return if self.isFailed(): return try: self.preStart() except Exception: logger.exception("Exception during preStart()") cmd = self.getCommand() cmd = [c for c in cmd if c is not None] # don't use shell mode for commands without piping if len(cmd) > 1: # multiple commands with pipes cmd = "|".join(cmd) self.process = subprocess.Popen(cmd, shell=True, start_new_session=True) else: # single command cmd = cmd[0] # start_new_session can go as soon as there's no piped commands left # the os.killpg call must be replaced with something more reasonable at the same time self.process = subprocess.Popen(shlex.split(cmd), start_new_session=True) logger.info("Started sdr source: " + cmd) available = False failed = False def wait_for_process_to_end(): nonlocal failed rc = self.process.wait() logger.debug("shut down with RC={0}".format(rc)) self.process = None self.monitor = None if self.getState() is SdrSourceState.RUNNING: self.fail() else: failed = True self.setState(SdrSourceState.STOPPED) self.monitor = threading.Thread(target=wait_for_process_to_end, name="source_monitor") self.monitor.start() retries = 1000 while retries > 0 and not failed: retries -= 1 if self.monitor is None: break testsock = socket.socket() try: testsock.connect(("127.0.0.1", self.getPort())) testsock.close() available = True break except: time.sleep(0.1) if not available: failed = True try: self.postStart() except Exception: logger.exception("Exception during postStart()") failed = True if failed: self.fail() else: self.setState(SdrSourceState.RUNNING) def preStart(self): """ override this method in subclasses if there's anything to be done before starting up the actual SDR """ pass def postStart(self): """ override this method in subclasses if there's things to do after the actual SDR has started up """ pass def isAvailable(self): return self.monitor is not None def stop(self): with self.modificationLock: if self.process is not None: self.setState(SdrSourceState.STOPPING) try: os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) except ProcessLookupError: # been killed by something else, ignore pass if self.monitor: self.monitor.join() def shutdown(self): self.stop() for c in self.clients.copy(): c.onShutdown() def getClients(self, *args): if not args: return self.clients return [c for c in self.clients if c.getClientClass() in args] def hasClients(self, *args): return len(self.getClients(*args)) > 0 def addClient(self, c: SdrSourceEventClient): if c in self.clients: return self.clients.append(c) c.onStateChange(self.getState()) hasUsers = self.hasClients(SdrClientClass.USER) hasBackgroundTasks = self.hasClients(SdrClientClass.BACKGROUND) if hasUsers or hasBackgroundTasks: self.start() self.setBusyState( SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE) def removeClient(self, c: SdrSourceEventClient): if c not in self.clients: return self.clients.remove(c) self.checkStatus() def checkStatus(self): hasUsers = self.hasClients(SdrClientClass.USER) self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE) # no need to check for users if we are always-on if self.isAlwaysOn(): return hasBackgroundTasks = self.hasClients(SdrClientClass.BACKGROUND) if not hasUsers and not hasBackgroundTasks: self.stop() def addSpectrumClient(self, c): if c in self.spectrumClients: return # local import due to circular depencency from owrx.fft import SpectrumThread self.spectrumClients.append(c) with self.spectrumLock: if self.spectrumThread is None: self.spectrumThread = SpectrumThread(self) self.spectrumThread.start() def removeSpectrumClient(self, c): try: self.spectrumClients.remove(c) except ValueError: pass with self.spectrumLock: if not self.spectrumClients and self.spectrumThread is not None: self.spectrumThread.stop() self.spectrumThread = None def writeSpectrumData(self, data): for c in self.spectrumClients: c.write_spectrum_data(data) def getState(self) -> SdrSourceState: return self.state def setState(self, state: SdrSourceState): if state == self.state: return self.state = state for c in self.clients.copy(): c.onStateChange(state) def setBusyState(self, state: SdrBusyState): if state == self.busyState: return self.busyState = state for c in self.clients.copy(): c.onBusyStateChange(state)
def setSdr(self, id=None): while True: next = None if id is not None: next = SdrService.getSource(id) if next is None: next = SdrService.getFirstSource() if next is None: # exit condition: no sdrs available logger.warning("no more SDR devices available") self.handleNoSdrsAvailable() return # exit condition: no change if next == self.sdr: return self.stopDsp() if self.configSub is not None: self.configSub.cancel() self.configSub = None self.sdr = next self.getDsp() # found a working sdr, exit the loop if self.sdr.getState() != SdrSource.STATE_FAILED: break logger.warning('SDR device "%s" has failed, selecing new device', self.sdr.getName()) self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName())) # send initial config self.getDsp().setProperties(self.connectionProperties) stack = PropertyStack() stack.addLayer(0, self.sdr.getProps()) stack.addLayer(1, Config.get()) configProps = stack.filter(*OpenWebRxReceiverClient.config_keys) def sendConfig(key, value): config = configProps.__dict__() # TODO mathematical properties? hmmmm config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] # TODO this is a hack to support multiple sdrs config["sdr_id"] = self.sdr.getId() self.write_config(config) cf = configProps["center_freq"] srh = configProps["samp_rate"] / 2 frequencyRange = (cf - srh, cf + srh) self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange)) bookmarks = [b.__dict__() for b in Bookmarks.getSharedInstance().getBookmarks(frequencyRange)] self.write_bookmarks(bookmarks) self.configSub = configProps.wire(sendConfig) sendConfig(None, None) self.__sendProfiles() self.sdr.addSpectrumClient(self)