From d6515ed991b28de8f173302329ec9733fc460e70 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 22 Dec 2019 19:09:02 +0100 Subject: [PATCH] Added support for continuous checks on distance sensor and DistanceSensorEvent --- platypush/message/event/__init__.py | 40 +++--- platypush/message/event/distance.py | 12 ++ platypush/plugins/gpio/__init__.py | 9 +- .../plugins/gpio/sensor/distance/__init__.py | 115 ++++++++++++++---- 4 files changed, 130 insertions(+), 46 deletions(-) create mode 100644 platypush/message/event/distance.py diff --git a/platypush/message/event/__init__.py b/platypush/message/event/__init__.py index 184490e2..f2499b35 100644 --- a/platypush/message/event/__init__.py +++ b/platypush/message/event/__init__.py @@ -11,6 +11,7 @@ from platypush.config import Config from platypush.message import Message from platypush.utils import get_event_class_by_type + class Event(Message): """ Event message class """ @@ -39,6 +40,9 @@ class Event(Message): self.args = kwargs self.disable_logging = disable_logging + for arg, value in self.args.items(): + self.__setattr__(arg, value) + @classmethod def build(cls, msg): """ Builds an event message from a JSON UTF-8 string/bytearray, a @@ -55,16 +59,14 @@ class Event(Message): args['timestamp'] = msg['_timestamp'] if '_timestamp' in msg else time.time() return event_class(**args) - @staticmethod def _generate_id(): """ Generate a unique event ID """ id = '' - for i in range(0,16): + for i in range(0, 16): id += '%.2x' % random.randint(0, 255) return id - def matches_condition(self, condition): """ If the event matches an event condition, it will return an EventMatchResult @@ -75,7 +77,8 @@ class Event(Message): result = EventMatchResult(is_match=False, parsed_args=self.args) match_scores = [] - if not isinstance(self, condition.type): return result + if not isinstance(self, condition.type): + return result for (attr, value) in condition.args.items(): if attr not in self.args: @@ -100,7 +103,6 @@ class Event(Message): return result - def _matches_argument(self, argname, condition_value): """ Returns an EventMatchResult if the event argument [argname] matches @@ -147,10 +149,9 @@ class Event(Message): else: result.parsed_args[argname] += ' ' + event_token - if (len(condition_tokens) == 1 and len(event_tokens) == 1) \ - or (len(event_tokens) > 1 and len(condition_tokens) > 1 \ - and event_tokens[1] == condition_tokens[1]): + or (len(event_tokens) > 1 and len(condition_tokens) > 1 + and event_tokens[1] == condition_tokens[1]): # Stop appending tokens to this argument, as the next # condition will be satisfied as well condition_tokens.pop(0) @@ -164,7 +165,6 @@ class Event(Message): result.is_match = len(condition_tokens) == 0 return result - def __str__(self): """ Overrides the str() operator and converts @@ -175,13 +175,13 @@ class Event(Message): flatten(args) return json.dumps({ - 'type' : 'event', - 'target' : self.target, - 'origin' : self.origin if hasattr(self, 'origin') else None, - 'id' : self.id if hasattr(self, 'id') else None, - '_timestamp' : self.timestamp, - 'args' : { - 'type' : self.type, + 'type': 'event', + 'target': self.target, + 'origin': self.origin if hasattr(self, 'origin') else None, + 'id': self.id if hasattr(self, 'id') else None, + '_timestamp': self.timestamp, + 'args': { + 'type': self.type, **args }, }) @@ -226,18 +226,16 @@ class StopEvent(Event): def flatten(args): if isinstance(args, dict): - for (key,value) in args.items(): + for (key, value) in args.items(): if isinstance(value, date): args[key] = value.isoformat() elif isinstance(value, dict) or isinstance(value, list): flatten(args[key]) elif isinstance(args, list): - for i in range(0,len(args)): + for i in range(0, len(args)): if isinstance(args[i], date): - args[i] = value.isoformat() + args[i] = args[i].isoformat() elif isinstance(args[i], dict) or isinstance(args[i], list): flatten(args[i]) - # vim:sw=4:ts=4:et: - diff --git a/platypush/message/event/distance.py b/platypush/message/event/distance.py new file mode 100644 index 00000000..b72beafa --- /dev/null +++ b/platypush/message/event/distance.py @@ -0,0 +1,12 @@ +from platypush.message.event import Event + + +class DistanceSensorEvent(Event): + """ + Event triggered when a new value is processed by a distance sensor. + """ + def __init__(self, distance: float, unit: str = 'mm', *args, **kwargs): + super().__init__(*args, distance=distance, unit=unit, **kwargs) + + +# vim:sw=4:ts=4:et: diff --git a/platypush/plugins/gpio/__init__.py b/platypush/plugins/gpio/__init__.py index 2bff798c..177041a9 100644 --- a/platypush/plugins/gpio/__init__.py +++ b/platypush/plugins/gpio/__init__.py @@ -176,9 +176,12 @@ class GpioPlugin(Plugin): Cleanup the state of the GPIO and resets PIN values. """ import RPi.GPIO as GPIO - GPIO.cleanup() - self._initialized_pins = {} - self._initialized = False + + with self._init_lock: + if self._initialized: + GPIO.cleanup() + self._initialized_pins = {} + self._initialized = False # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/gpio/sensor/distance/__init__.py b/platypush/plugins/gpio/sensor/distance/__init__.py index fad3f476..1e4fba0d 100644 --- a/platypush/plugins/gpio/sensor/distance/__init__.py +++ b/platypush/plugins/gpio/sensor/distance/__init__.py @@ -1,5 +1,10 @@ +import threading import time +from typing import Optional + +from platypush.context import get_bus +from platypush.message.event.distance import DistanceSensorEvent from platypush.plugins import action from platypush.plugins.gpio import GpioPlugin from platypush.plugins.gpio.sensor import GpioSensorPlugin @@ -13,6 +18,11 @@ class GpioSensorDistancePlugin(GpioPlugin, GpioSensorPlugin): Requires: * ``RPi.GPIO`` (``pip install RPi.GPIO``) + + Triggers: + + * :class:`platypush.message.event.distance.DistanceSensorEvent` when a new distance measurement is available + """ def __init__(self, trigger_pin: int, echo_pin: int, @@ -31,35 +41,42 @@ class GpioSensorDistancePlugin(GpioPlugin, GpioSensorPlugin): for the sensor to be ready (default: 2 seconds). """ - GpioPlugin.__init__(self, *args, **kwargs) + GpioPlugin.__init__(self, pins={'trigger': trigger_pin, 'echo': echo_pin, }, *args, **kwargs) + self.trigger_pin = trigger_pin self.echo_pin = echo_pin self.timeout = timeout self.warmup_time = warmup_time - self._initialized = False - self._init_gpio() - - def _init_gpio(self): - if self._initialized: - return + self._measurement_thread: Optional[threading.Thread] = None + self._measurement_thread_lock = threading.RLock() + self._measurement_thread_can_run = False + self._init_board() + def _init_board(self): import RPi.GPIO as GPIO - GPIO.setmode(self.mode) - GPIO.setup(self.trigger_pin, GPIO.OUT) - GPIO.setup(self.echo_pin, GPIO.IN) - GPIO.output(self.trigger_pin, GPIO.LOW) - self.logger.info('Waiting {} seconds for the sensor to be ready'.format(self.warmup_time)) - time.sleep(self.warmup_time) - self.logger.info('Sensor ready') - self._initialized = True + with self._init_lock: + if self._initialized: + return + + GpioPlugin._init_board(self) + self._initialized = False + + GPIO.setup(self.trigger_pin, GPIO.OUT) + GPIO.setup(self.echo_pin, GPIO.IN) + GPIO.output(self.trigger_pin, GPIO.LOW) + + self.logger.info('Waiting {} seconds for the sensor to be ready'.format(self.warmup_time)) + time.sleep(self.warmup_time) + self.logger.info('Sensor ready') + self._initialized = True def _get_data(self): import RPi.GPIO as GPIO pulse_start = pulse_on = time.time() - self._init_gpio() + self._init_board() GPIO.output(self.trigger_pin, GPIO.HIGH) time.sleep(0.00001) # 1 us pulse to trigger echo measurement GPIO.output(self.trigger_pin, GPIO.LOW) @@ -94,7 +111,10 @@ class GpioSensorDistancePlugin(GpioPlugin, GpioSensorPlugin): """ try: - return self._get_data() + distance = self._get_data() + bus = get_bus() + bus.post(DistanceSensorEvent(distance=distance, unit='mm')) + return distance except TimeoutError as e: self.logger.warning(str(e)) return @@ -104,16 +124,67 @@ class GpioSensorDistancePlugin(GpioPlugin, GpioSensorPlugin): @action def close(self): - import RPi.GPIO as GPIO - if self._initialized: - GPIO.cleanup() - self._initialized = False + return self.cleanup() def __enter__(self): - self._init_gpio() + self._init_board() def __exit__(self): self.close() + def _get_measurement_thread(self, duration: float): + def _thread(): + with self: + start_time = time.time() + + try: + while self._measurement_thread_can_run and ( + not duration or time.time() - start_time <= duration): + self.get_measurement() + finally: + self._measurement_thread = None + + return _thread + + def _is_measurement_thread_running(self): + with self._measurement_thread_lock: + return self._measurement_thread is not None + + @action + def start_measurement(self, duration: Optional[float] = None): + """ + Start the measurement thread. It will trigger :class:`platypush.message.event.distance.DistanceSensorEvent` + events when new measurements are available. + + :param duration: If set, then the thread will run for the specified amount of seconds (default: None) + """ + with self._measurement_thread_lock: + if self._is_measurement_thread_running(): + self.logger.warning('A measurement thread is already running') + return + + thread_func = self._get_measurement_thread(duration=duration) + self._measurement_thread = threading.Thread(target=thread_func) + self._measurement_thread_can_run = True + self._measurement_thread.start() + + @action + def stop_measurement(self): + """ + Stop the running measurement thread. + """ + with self._measurement_thread_lock: + if not self._is_measurement_thread_running(): + self.logger.warning('No measurement thread is running') + return + + self._measurement_thread_can_run = False + self.logger.info('Waiting for the measurement thread to end') + + if self._measurement_thread: + self._measurement_thread.join(timeout=self.timeout) + + self.logger.info('Measurement thread terminated') + # vim:sw=4:ts=4:et: