Exemplo n.º 1
0
class FakeSensor(object):
    """Fake sensor."""
    def __init__(self,
                 name,
                 sensor_type,
                 description,
                 units='',
                 params=None,
                 clock=time):
        self.name = name
        sensor_type = Sensor.parse_type(sensor_type)
        params = str(params).split(' ') if params else None
        self._sensor = Sensor(sensor_type, name, description, units, params)
        self.__doc__ = self.description = description
        self._clock = clock
        self._listeners = set()
        self._last_update = SensorUpdate(0.0, 0.0, 'unknown', None)
        self._strategy = None
        self._next_period = None
        self.set_strategy('none')

    @property
    def value(self):
        return self._last_update.value

    @property
    def status(self):
        return self._last_update.status

    @property
    def strategy(self):
        return SampleStrategy.SAMPLING_LOOKUP[self._strategy.get_sampling()]

    def get_value(self):
        # XXX Check whether this also triggers a sensor update a la strategy
        return self._sensor.value()

    def _set_value(self, value, status=Sensor.NOMINAL):
        self._sensor.set_value(value, status, self._clock.time())

    def _update_value(self, timestamp, status_str, value_str):
        update_seconds = self._clock.time()
        value = self._sensor.parse_value(value_str)
        self._last_update = SensorUpdate(update_seconds, timestamp, status_str,
                                         value)
        for listener in set(self._listeners):
            listener(update_seconds, timestamp, status_str, value_str)

    def set_strategy(self, strategy, params=None):
        """Set sensor strategy."""
        def inform_callback(sensor_name, timestamp_str, status_str, value_str):
            """Inform callback for sensor strategy."""
            self._update_value(float(timestamp_str), status_str, value_str)
            print sensor_name, timestamp_str, status_str, value_str

        if self._strategy:
            self._strategy.detach()
        params = normalize_strategy_parameters(params)
        self._strategy = SampleStrategy.get_strategy(strategy, inform_callback,
                                                     self._sensor, *params)
        self._strategy.attach()
        self._next_period = self._strategy.periodic(self._clock.time())

    def update(self, timestamp):
        while self._next_period and timestamp >= self._next_period:
            self._next_period = self._strategy.periodic(self._next_period)

    def register_listener(self, listener, min_wait=-1.0):
        """Add a callback function that is called when sensor value is updated.

        Parameters
        ----------
        listener : function
            Callback signature: listener(update_seconds, value_seconds, status, value)
        min_wait : float, optional
            Minimum waiting period before listener can be called again, used
            to limit the callback rate (zero or negative for no rate limit)
            *This is ignored* as the same effect can be achieved with an
            event-rate strategy on the sensor.

        """
        self._listeners.add(listener)

    def unregister_listener(self, listener):
        """Remove a listener callback added with register_listener().

        Parameters
        ----------
        listener : function
            Reference to the callback function that should be removed

        """
        self._listeners.discard(listener)
Exemplo n.º 2
0
class KATCPSensor(with_metaclass(abc.ABCMeta, object)):
    """Wrapper around a specific KATCP sensor on a given KATCP device.

    Each available KATCP sensor for a particular device has an associated
    :class:`KATCPSensor` object in the object hierarchy. This wrapper is mainly
    for interactive convenience. It provides the KATCP request help string as a
    docstring and registers listeners. Subclasses need to call the base class
    version of __init__().
    """
    def __init__(self, sensor_description, sensor_manager):
        """Subclasses must arrange to call this in their __init__().

        Parameters
        ----------
        sensor_description : dict
           Description of the KATCP sensor, with keys same as the parameters of
           :class:`katcp.Sensor`
        sensor_manager : :class:`KATCPSensorsManager` instance
           Manages sensor strategies, allows sensor polling, and provides time
        """
        self._manager = sensor_manager
        self.clear_listeners()
        self._reading = KATCPSensorReading(0, 0, Sensor.UNKNOWN, None)
        # We'll be abusing a katcp.Sensor object slightly to make use of its
        # parsing and formatting functionality
        self._sensor = Sensor(**sensor_description)
        self._name = self._sensor.name
        # Overide the katpc.Sensor's set method with ours
        self._sensor.set = self.set
        # Steal the the katcp.Sensor's set_formatted method. Since we overrode
        # its set() method with ours, calling set_formatted will result in this
        # KATCPSensor object's value being set.
        self.set_formatted = self._sensor.set_formatted

    @property
    def parent_name(self):
        """Name of the parent of this KATCPSensor"""
        return self._manager.resource_name

    @property
    def name(self):
        """Name of this KATCPSensor"""
        return self._name

    @property
    def normalised_name(self):
        """Normalised name of this KATCPSensor that can be used as a python identifier"""
        return escape_name(self._name)

    @property
    def reading(self):
        """Most recently received sensor reading as KATCPSensorReading instance"""
        return self._reading

    @property
    def value(self):
        return self._reading.value

    @property
    def status(self):
        return self._reading.status

    @property
    def sampling_strategy(self):
        """Current sampling strategy"""
        return self._manager.get_sampling_strategy(self.name)

    @property
    def description(self):
        return self._sensor.description

    @property
    def units(self):
        return self._sensor.units

    @property
    def type(self):
        return self._sensor.type

    def parse_value(self, s_value):
        """Parse a value from a string.

        Parameters
        ----------
        s_value : str
            A string value to attempt to convert to a value for
            the sensor.

        Returns
        -------
        value : object
            A value of a type appropriate to the sensor.

        """
        return self._sensor.parse_value(s_value)

    def set_strategy(self, strategy, params=None):
        """Set current sampling strategy for sensor.
        Add this footprint for backwards compatibility.

        Parameters
        ----------

        strategy : seq of str or str
            As tuple contains (<strat_name>, [<strat_parm1>, ...]) where the strategy
            names and parameters are as defined by the KATCP spec. As str contains the
            same elements in space-separated form.
        params : seq of str or str
            (<strat_name>, [<strat_parm1>, ...])

        Returns
        -------
        done : tornado Future that resolves when done or raises KATCPSensorError

        """
        if not params:
            param_args = []
        elif isinstance(params, basestring):
            param_args = [str(p) for p in params.split(' ')]
        else:
            if not isinstance(params, collections.Iterable):
                params = (params, )
            param_args = [str(p) for p in params]
        samp_strategy = " ".join([strategy] + param_args)
        return self._manager.set_sampling_strategy(self.name, samp_strategy)

    def set_sampling_strategy(self, strategy):
        """Set current sampling strategy for sensor

        Parameters
        ----------

        strategy : seq of str or str
            As tuple contains (<strat_name>, [<strat_parm1>, ...]) where the strategy
            names and parameters are as defined by the KATCP spec. As str contains the
            same elements in space-separated form.

        Returns
        -------
        done : tornado Future that resolves when done or raises KATCPSensorError

        """
        return self._manager.set_sampling_strategy(self.name, strategy)

    def drop_sampling_strategy(self):
        """Drop memorised sampling strategy for sensor, if any

        Calling this method ensures that the sensor manager does not attempt
        to reapply a sampling strategy.  It will not raise an error if no strategy
        has been set.  Use :meth:`set_sampling_strategy` to memorise a strategy again.
        """
        self._manager.drop_sampling_strategy(self.name)

    def register_listener(self, listener, reading=False):
        """Add a callback function that is called when sensor value is updated.

        The callback footprint is received_timestamp, timestamp, status, value.

        Parameters
        ----------
        listener : function
            Callback signature: if reading listener(katcp_sensor, reading) where
            `katcp_sensor` is this KATCPSensor instance `reading` is an instance of
            :class:`KATCPSensorReading`.

            Callback signature: default, if not reading listener(received_timestamp,
            timestamp, status, value)
        """
        listener_id = hashable_identity(listener)
        self._listeners[listener_id] = (listener, reading)
        logger.debug('Register listener for {}'.format(self.name))

    def unregister_listener(self, listener):
        """Remove a listener callback added with register_listener().

        Parameters
        ----------
        listener : function
            Reference to the callback function that should be removed

        """
        listener_id = hashable_identity(listener)
        self._listeners.pop(listener_id, None)

    def is_listener(self, listener):
        listener_id = hashable_identity(listener)
        return listener_id in self._listeners

    def clear_listeners(self):
        """Clear any registered listeners to updates from this sensor."""
        self._listeners = {}

    def call_listeners(self, reading):
        logger.debug('Calling listeners {}'.format(self.name))
        for listener, use_reading in list(self._listeners.values()):
            try:
                if use_reading:
                    listener(self, reading)
                else:
                    listener(reading.received_timestamp, reading.timestamp,
                             reading.status, reading.value)
            except Exception:
                logger.exception(
                    'Unhandled exception calling KATCPSensor callback {0!r}'.
                    format(listener))

    def set(self, timestamp, status, value):
        """Set sensor with a given received value, matches :meth:`katcp.Sensor.set`"""
        received_timestamp = self._manager.time()
        reading = KATCPSensorReading(received_timestamp, timestamp, status,
                                     value)
        self._reading = reading
        self.call_listeners(reading)

    def set_value(self, value, status=Sensor.NOMINAL, timestamp=None):
        """Set sensor value with optinal specification of status and timestamp"""
        if timestamp is None:
            timestamp = self._manager.time()
        self.set(timestamp, status, value)

    def set_formatted(self, raw_timestamp, raw_status, raw_value, major):
        """Set sensor using KATCP string formatted inputs

        Mirrors :meth:`katcp.Sensor.set_formatted`.

        This implementation is empty. Will, during instantiation, be overridden by the
        set_formatted() method of a katcp.Sensor object.
        """

    @tornado.gen.coroutine
    def get_reading(self):
        """Get a fresh sensor reading from the KATCP resource

        Returns
        -------
        reply : tornado Future resolving with  :class:`KATCPSensorReading` object

        Notes
        -----

        As a side-effect this will update the reading stored in this object, and result in
        registered listeners being called.
        """
        yield self._manager.poll_sensor(self._name)
        # By now the sensor manager should have set the reading
        raise Return(self._reading)

    @tornado.gen.coroutine
    def get_value(self):
        """Get a fresh sensor value from the KATCP resource

        Returns
        -------
        reply : tornado Future resolving with  :class:`KATCPSensorReading` object

        Notes
        -----

        As a side-effect this will update the reading stored in this object, and result in
        registered listeners being called.
        """
        yield self._manager.poll_sensor(self._name)
        # By now the sensor manager should have set the reading
        raise Return(self._reading.value)

    @tornado.gen.coroutine
    def get_status(self):
        """Get a fresh sensor status from the KATCP resource

        Returns
        -------
        reply : tornado Future resolving with  :class:`KATCPSensorReading` object

        Notes
        -----

        As a side-effect this will update the reading stored in this object, and result in
        registered listeners being called.
        """
        yield self._manager.poll_sensor(self._name)
        # By now the sensor manager should have set the reading
        raise Return(self._reading.status)

    def wait(self, condition_or_value, timeout=None):
        """Wait for the sensor to satisfy a condition.

        Parameters
        ----------
        condition_or_value : obj or callable, or seq of objs or callables
            If obj, sensor.value is compared with obj. If callable,
            condition_or_value(reading) is called, and must return True if its
            condition is satisfied. Since the reading is passed in, the value,
            status, timestamp or received_timestamp attributes can all be used
            in the check.
            TODO: Sequences of conditions (use SensorTransitionWaiter thingum?)
        timeout : float or None
            The timeout in seconds (None means wait forever)

        Returns
        -------
        This command returns a tornado Future that resolves with True when the
        sensor value satisfies the condition. It will never resolve with False;
        if a timeout is given a TimeoutError happens instead.

        Raises
        ------
        :class:`KATCPSensorError`
            If the sensor does not have a strategy set
        :class:`tornado.gen.TimeoutError`
            If the sensor condition still fails after a stated timeout period

        """
        if (isinstance(condition_or_value, collections.Sequence)
                and not isinstance(condition_or_value, basestring)):
            raise NotImplementedError(
                'Currently only single conditions are supported')
        condition_test = (condition_or_value if callable(condition_or_value)
                          else lambda s: s.value == condition_or_value)

        ioloop = tornado.ioloop.IOLoop.current()
        f = Future()
        if self.sampling_strategy == ('none', ):
            raise KATCPSensorError(
                'Cannot wait on a sensor that does not have a strategy set')

        def handle_update(sensor, reading):
            # This handler is called whenever a sensor update is received
            try:
                assert sensor is self
                if condition_test(reading):
                    self.unregister_listener(handle_update)
                    # Try and be idempotent if called multiple times after the
                    # condition is matched. This should not happen unless the
                    # sensor object is being updated in a thread outside of the
                    # ioloop.
                    if not f.done():
                        ioloop.add_callback(f.set_result, True)
            except Exception:
                f.set_exc_info(sys.exc_info())
                self.unregister_listener(handle_update)

        self.register_listener(handle_update, reading=True)
        # Handle case where sensor is already at the desired value
        ioloop.add_callback(handle_update, self, self._reading)

        if timeout:
            to = ioloop.time() + timeout
            timeout_f = with_timeout(to, f)
            # Make sure we stop listening if the wait times out to prevent a
            # buildup of listeners
            timeout_f.add_done_callback(
                lambda f: self.unregister_listener(handle_update))
            return timeout_f
        else:
            return f
Exemplo n.º 3
0
class KATCPSensor(object):
    """Wrapper around a specific KATCP sensor on a given KATCP device.

    Each available KATCP sensor for a particular device has an associated
    :class:`KATCPSensor` object in the object hierarchy. This wrapper is mainly
    for interactive convenience. It provides the KATCP request help string as a
    docstring and registers listeners. Subclasses need to call the base class
    version of __init__().
    """
    __metaclass__ = abc.ABCMeta

    def __init__(self, sensor_description, sensor_manager):
        """Subclasses must arrange to call this in their __init__().

        Parameters
        ----------
        sensor_description : dict
           Description of the KATCP sensor, with keys same as the parameters of
           :class:`katcp.Sensor`
        sensor_manager : :class:`KATCPSensorsManager` instance
           Manages sensor strategies, allows sensor polling, and provides time
        """
        self._manager = sensor_manager
        self.clear_listeners()
        self._reading = KATCPSensorReading(0, 0, Sensor.UNKNOWN, None)
        # We'll be abusing a katcp.Sensor object slightly to make use of its
        # parsing and formatting functionality
        self._sensor = Sensor(**sensor_description)
        self._name = self._sensor.name
        # Overide the katpc.Sensor's set method with ours
        self._sensor.set = self.set
        # Steal the the katcp.Sensor's set_formatted method. Since we overrode
        # its set() method with ours, calling set_formatted will result in this
        # KATCPSensor object's value being set.
        self.set_formatted = self._sensor.set_formatted

    @property
    def parent_name(self):
        """Name of the parent of this KATCPSensor"""
        return self._manager.resource_name

    @property
    def name(self):
        """Name of this KATCPSensor"""
        return self._name

    @property
    def normalised_name(self):
        """Normalised name of this KATCPSensor that can be used as a python identifier"""
        return escape_name(self._name)

    @property
    def reading(self):
        """Most recently received sensor reading as KATCPSensorReading instance"""
        return self._reading

    @property
    def value(self):
        return self._reading.value

    @property
    def status(self):
        return self._reading.status

    @property
    def sampling_strategy(self):
        """Current sampling strategy"""
        return self._manager.get_sampling_strategy(self.name)

    @property
    def description(self):
        return self._sensor.description

    @property
    def units(self):
        return self._sensor.units

    @property
    def type(self):
        return self._sensor.type

    def parse_value(self, s_value):
        """Parse a value from a string.

        Parameters
        ----------
        s_value : str
            A string value to attempt to convert to a value for
            the sensor.

        Returns
        -------
        value : object
            A value of a type appropriate to the sensor.

        """
        return self._sensor.parse_value(s_value)

    def set_strategy(self, strategy, params=None):
        """Set current sampling strategy for sensor.
        Add this footprint for backwards compatibility.

        Parameters
        ----------

        strategy : seq of str or str
            As tuple contains (<strat_name>, [<strat_parm1>, ...]) where the strategy
            names and parameters are as defined by the KATCP spec. As str contains the
            same elements in space-separated form.
        params : seq of str or str
            (<strat_name>, [<strat_parm1>, ...])

        Returns
        -------
        done : tornado Future that resolves when done or raises KATCPSensorError

        """
        if not params:
            param_args = []
        elif isinstance(params, basestring):
            param_args = [str(p) for p in params.split(' ')]
        else:
            if not isinstance(params, collections.Iterable):
                params = (params,)
            param_args = [str(p) for p in params]
        samp_strategy = " ".join([strategy] + param_args)
        return self._manager.set_sampling_strategy(self.name, samp_strategy)

    def set_sampling_strategy(self, strategy):
        """Set current sampling strategy for sensor

        Parameters
        ----------

        strategy : seq of str or str
            As tuple contains (<strat_name>, [<strat_parm1>, ...]) where the strategy
            names and parameters are as defined by the KATCP spec. As str contains the
            same elements in space-separated form.

        Returns
        -------
        done : tornado Future that resolves when done or raises KATCPSensorError

        """
        return self._manager.set_sampling_strategy(self.name, strategy)

    def register_listener(self, listener, reading=False):
        """Add a callback function that is called when sensor value is updated.
        The callback footprint is received_timestamp, timestamp, status, value.

        Parameters
        ----------
        listener : function
            Callback signature: if reading
            listener(katcp_sensor, reading) where
                `katcp_sensor` is this KATCPSensor instance
                `reading` is an instance of :class:`KATCPSensorReading`
            Callback signature: default, if not reading
                listener(received_timestamp, timestamp, status, value)
        """
        listener_id = hashable_identity(listener)
        self._listeners[listener_id] = (listener, reading)
        logger.debug(
                    'Register listener for {}'
                    .format(self.name))

    def unregister_listener(self, listener):
        """Remove a listener callback added with register_listener().

        Parameters
        ----------
        listener : function
            Reference to the callback function that should be removed

        """
        listener_id = hashable_identity(listener)
        self._listeners.pop(listener_id, None)

    def is_listener(self, listener):
        listener_id = hashable_identity(listener)
        return listener_id in self._listeners

    def clear_listeners(self):
        """Clear any registered listeners to updates from this sensor."""
        self._listeners = {}

    def call_listeners(self, reading):
        logger.debug(
                    'Calling listeners {}'
                    .format(self.name))
        for listener, use_reading in self._listeners.values():
            try:
                if use_reading:
                    listener(self, reading)
                else:
                    listener(reading.received_timestamp, reading.timestamp,
                             reading.status, reading.value)
            except Exception:
                logger.exception(
                    'Unhandled exception calling KATCPSensor callback {0!r}'
                    .format(listener))

    def set(self, timestamp, status, value):
        """Set sensor with a given received value, matches :meth:`katcp.Sensor.set`"""
        received_timestamp = self._manager.time()
        reading = KATCPSensorReading(received_timestamp, timestamp, status, value)
        self._reading = reading
        self.call_listeners(reading)

    def set_value(self, value, status=Sensor.NOMINAL, timestamp=None):
        """Set sensor value with optinal specification of status and timestamp"""
        if timestamp is None:
            timestamp = self._manager.time()
        self.set(timestamp, status, value)

    def set_formatted(self, raw_timestamp, raw_status, raw_value, major):
        """Set sensor using KATCP string formatted inputs

        Mirrors :meth:`katcp.Sensor.set_formatted`.

        This implementation is empty. Will, during instantiation, be overridden by the
        set_formatted() method of a katcp.Sensor object.
        """

    @tornado.gen.coroutine
    def get_reading(self):
        """Get a fresh sensor reading from the KATCP resource

        Returns
        -------
        reply : tornado Future resolving with  :class:`KATCPSensorReading` object

        Note
        ----

        As a side-effect this will update the reading stored in this object, and result in
        registered listeners being called.
        """
        yield self._manager.poll_sensor(self._name)
        # By now the sensor manager should have set the reading
        raise Return(self._reading)

    @tornado.gen.coroutine
    def get_value(self):
        """Get a fresh sensor value from the KATCP resource

        Returns
        -------
        reply : tornado Future resolving with  :class:`KATCPSensorReading` object

        Note
        ----

        As a side-effect this will update the reading stored in this object, and result in
        registered listeners being called.
        """
        yield self._manager.poll_sensor(self._name)
        # By now the sensor manager should have set the reading
        raise Return(self._reading.value)

    @tornado.gen.coroutine
    def get_status(self):
        """Get a fresh sensor status from the KATCP resource

        Returns
        -------
        reply : tornado Future resolving with  :class:`KATCPSensorReading` object

        Note
        ----

        As a side-effect this will update the reading stored in this object, and result in
        registered listeners being called.
        """
        yield self._manager.poll_sensor(self._name)
        # By now the sensor manager should have set the reading
        raise Return(self._reading.status)

    def wait(self, condition_or_value, timeout=None):
        """Wait for the sensor to satisfy a condition.

        Parameters
        ----------
        condition_or_value : obj or callable, or seq of objs or callables
            If obj, sensor.value is compared with obj. If callable,
            condition_or_value(reading) is called, and must return True if its
            condition is satisfied. Since the reading is passed in, the value,
            status, timestamp or received_timestamp attributes can all be used
            in the check.
            TODO: Sequences of conditions (use SensorTransitionWaiter thingum?)
        timeout : float or None
            The timeout in seconds (None means wait forever)

        Returns
        -------
        This command returns a tornado Future that resolves with True when the
        sensor value satisfies the condition. It will never resolve with False;
        if a timeout is given a TimeoutError happens instead.

        Raises
        ------
        :class:`KATCPSensorError`
            If the sensor does not have a strategy set
        :class:`tornado.gen.TimeoutError`
            If the sensor condition still fails after a stated timeout period

        """
        if (isinstance(condition_or_value, collections.Sequence) and not
                isinstance(condition_or_value, basestring)):
            raise NotImplementedError(
                'Currently only single conditions are supported')
        condition_test = (condition_or_value if callable(condition_or_value)
                          else lambda s: s.value == condition_or_value)

        ioloop = tornado.ioloop.IOLoop.current()
        f = Future()
        if self.sampling_strategy == ('none', ):
            raise KATCPSensorError(
                'Cannot wait on a sensor that does not have a strategy set')

        def handle_update(sensor, reading):
            # This handler is called whenever a sensor update is received
            try:
                assert sensor is self
                if condition_test(reading):
                    self.unregister_listener(handle_update)
                    # Try and be idempotent if called multiple times after the
                    # condition is matched. This should not happen unless the
                    # sensor object is being updated in a thread outside of the
                    # ioloop.
                    if not f.done():
                        ioloop.add_callback(f.set_result, True)
            except Exception:
                f.set_exc_info(sys.exc_info())
                self.unregister_listener(handle_update)

        self.register_listener(handle_update, reading=True)
        # Handle case where sensor is already at the desired value
        ioloop.add_callback(handle_update, self, self._reading)

        if timeout:
            to = ioloop.time() + timeout
            timeout_f = with_timeout(to, f)
            # Make sure we stop listening if the wait times out to prevent a
            # buildup of listeners
            timeout_f.add_done_callback(
                lambda f: self.unregister_listener(handle_update))
            return timeout_f
        else:
            return f