Esempio n. 1
0
    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()
Esempio n. 2
0
    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")
Esempio n. 3
0
    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)
Esempio n. 4
0
    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()
Esempio n. 5
0
    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()
Esempio n. 6
0
 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"})
Esempio n. 7
0
 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})
Esempio n. 8
0
    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)
Esempio n. 9
0
 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")
Esempio n. 10
0
 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")
Esempio n. 11
0
 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()
Esempio n. 12
0
    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()
Esempio n. 13
0
 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")
Esempio n. 14
0
 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")
Esempio n. 15
0
 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")
Esempio n. 16
0
 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})
Esempio n. 17
0
    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")
Esempio n. 18
0
 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")
Esempio n. 19
0
    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.")
Esempio n. 20
0
 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
Esempio n. 21
0
 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)
Esempio n. 22
0
    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.")
Esempio n. 23
0
 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)
Esempio n. 24
0
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)
Esempio n. 25
0
    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
Esempio n. 26
0
 def testLayer(self):
     om = PropertyStack()
     pm = PropertyLayer()
     pm["testkey"] = "testvalue"
     om.addLayer(1, pm)
     self.assertEqual(om["testkey"], "testvalue")
Esempio n. 27
0
    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__()
Esempio n. 28
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
Esempio n. 29
0
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)
Esempio n. 30
0
    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)