from owrx.config import Config from owrx.property import PropertyManager, PropertyDeleted, PropertyDelegator, PropertyLayer, PropertyReadOnly from owrx.feature import FeatureDetector, UnknownFeatureException from owrx.source import SdrSource, SdrSourceEventClient from functools import partial import logging logger = logging.getLogger(__name__) class MappedSdrSources(PropertyDelegator): def __init__(self, pm: PropertyManager): self.subscriptions = {} super().__init__(PropertyLayer()) for key, value in pm.items(): self._addSource(key, value) pm.wire(self.handleSdrDeviceChange) def handleSdrDeviceChange(self, changes): for key, value in changes.items(): if value is PropertyDeleted: if key in self: del self[key] else: if key not in self: self._addSource(key, value) def handleDeviceUpdate(self, key, value, *args): if key not in self and self.isDeviceValid(value): self[key] = self.buildNewSource(key, value) elif key in self and not self.isDeviceValid(value): del self[key] def _addSource(self, key, value): self.handleDeviceUpdate(key, value) updateMethod = partial(self.handleDeviceUpdate, key, value) self.subscriptions[key] = [ value.filter("type", "profiles").wire(updateMethod), value["profiles"].wire(updateMethod) ] def isDeviceValid(self, device): return self._sdrTypeAvailable(device) and self._hasProfiles(device) def _hasProfiles(self, device): return "profiles" in device and device["profiles"] and len(device["profiles"]) > 0 def _sdrTypeAvailable(self, value): featureDetector = FeatureDetector() try: if not featureDetector.is_available(value["type"]): logger.error( 'The SDR source type "{0}" is not available. please check the feature report for details.'.format( value["type"] ) ) return False return True except UnknownFeatureException: logger.error( 'The SDR source type "{0}" is invalid. Please check your configuration'.format(value["type"]) ) return False def buildNewSource(self, id, props): sdrType = props["type"] className = "".join(x for x in sdrType.title() if x.isalnum()) + "Source" module = __import__("owrx.source.{0}".format(sdrType), fromlist=[className]) cls = getattr(module, className) return cls(id, props) def _removeSource(self, key, source): source.shutdown() for sub in self.subscriptions[key]: sub.cancel() del self.subscriptions[key] def __setitem__(self, key, value): source = self[key] if key in self else None if source is value: return super().__setitem__(key, value) if source is not None: self._removeSource(key, source) def __delitem__(self, key): source = self[key] if key in self else None super().__delitem__(key) if source is not None: self._removeSource(key, source) class SourceStateHandler(SdrSourceEventClient): def __init__(self, pm, key, source: SdrSource): self.pm = pm self.key = key self.source = source def selfDestruct(self): self.source.removeClient(self) def onFail(self): del self.pm[self.key] def onDisable(self): del self.pm[self.key] def onEnable(self): self.pm[self.key] = self.source def onShutdown(self): del self.pm[self.key] class ActiveSdrSources(PropertyReadOnly): def __init__(self, pm: PropertyManager): self.handlers = {} self._layer = PropertyLayer() super().__init__(self._layer) for key, value in pm.items(): self._addSource(key, value) pm.wire(self.handleSdrDeviceChange) def handleSdrDeviceChange(self, changes): for key, value in changes.items(): if value is PropertyDeleted: self._removeSource(key) else: self._addSource(key, value) def isAvailable(self, source: SdrSource): return source.isEnabled() and not source.isFailed() def _addSource(self, key, source: SdrSource): if self.isAvailable(source): self._layer[key] = source self.handlers[key] = SourceStateHandler(self._layer, key, source) source.addClient(self.handlers[key]) def _removeSource(self, key): self.handlers[key].selfDestruct() del self.handlers[key] if key in self._layer: del self._layer[key] class AvailableProfiles(PropertyReadOnly): def __init__(self, pm: PropertyManager): self.subscriptions = {} self.profileSubscriptions = {} self._layer = PropertyLayer() super().__init__(self._layer) for key, value in pm.items(): self._addSource(key, value) pm.wire(self.handleSdrDeviceChange) def handleSdrDeviceChange(self, changes): for key, value in changes.items(): if value is PropertyDeleted: self._removeSource(key) else: self._addSource(key, value) def handleSdrNameChange(self, s_id, source, name): profiles = source.getProfiles() for p_id in list(self._layer.keys()): source_id, profile_id = p_id.split("|") if source_id == s_id: profile = profiles[profile_id] self._layer[p_id] = "{} {}".format(name, profile["name"]) def handleProfileChange(self, source_id, source: SdrSource, changes): for key, value in changes.items(): if value is PropertyDeleted: self._removeProfile(source_id, key) else: self._addProfile(source_id, source, key, value) def handleProfileNameChange(self, s_id, source: SdrSource, p_id, name): for concat_p_id in list(self._layer.keys()): source_id, profile_id = concat_p_id.split("|") if source_id == s_id and profile_id == p_id: self._layer[concat_p_id] = "{} {}".format(source.getName(), name) def _addSource(self, key, source: SdrSource): for p_id, p in source.getProfiles().items(): self._addProfile(key, source, p_id, p) self.subscriptions[key] = [ source.getProps().wireProperty("name", partial(self.handleSdrNameChange, key, source)), source.getProfiles().wire(partial(self.handleProfileChange, key, source)), ] def _addProfile(self, s_id, source: SdrSource, p_id, profile): self._layer["{}|{}".format(s_id, p_id)] = "{} {}".format(source.getName(), profile["name"]) if s_id not in self.profileSubscriptions: self.profileSubscriptions[s_id] = {} self.profileSubscriptions[s_id][p_id] = profile.wireProperty("name", partial(self.handleProfileNameChange, s_id, source, p_id)) def _removeSource(self, key): for profile_id in list(self._layer.keys()): if profile_id.startswith("{}|".format(key)): del self._layer[profile_id] if key in self.subscriptions: while self.subscriptions[key]: self.subscriptions[key].pop().cancel() del self.subscriptions[key] if key in self.profileSubscriptions: for p_id in self.profileSubscriptions[key].keys(): self.profileSubscriptions[key][p_id].cancel() del self.profileSubscriptions[key] def _removeProfile(self, s_id, p_id): for concat_p_id in list(self._layer.keys()): source_id, profile_id = concat_p_id.split("|") if source_id == s_id and profile_id == p_id: del self._layer[concat_p_id] if s_id in self.profileSubscriptions and p_id in self.profileSubscriptions[s_id]: self.profileSubscriptions[s_id][p_id].cancel() del self.profileSubscriptions[s_id][p_id] class SdrService(object): sources = None activeSources = None availableProfiles = None @staticmethod def getFirstSource(): sources = SdrService.getActiveSources() if not sources: return None # TODO: configure default sdr in config? right now it will pick the first one off the list. return sources[list(sources.keys())[0]] @staticmethod def getSource(id): sources = SdrService.getActiveSources() if not sources: return None if id not in sources: return None return sources[id] @staticmethod def getAllSources(): if SdrService.sources is None: SdrService.sources = MappedSdrSources(Config.get()["sdrs"]) return SdrService.sources @staticmethod def getActiveSources(): if SdrService.activeSources is None: SdrService.activeSources = ActiveSdrSources(SdrService.getAllSources()) return SdrService.activeSources @staticmethod def getAvailableProfiles(): if SdrService.availableProfiles is None: SdrService.availableProfiles = AvailableProfiles(SdrService.getActiveSources()) return SdrService.availableProfiles @staticmethod def stopAllSources(): for source in SdrService.getAllSources().values(): source.stop()