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