Added support for continuous checks on distance sensor and DistanceSensorEvent

This commit is contained in:
Fabio Manganiello 2019-12-22 19:09:02 +01:00
parent 9c4f917b53
commit d6515ed991
4 changed files with 130 additions and 46 deletions

View File

@ -11,6 +11,7 @@ from platypush.config import Config
from platypush.message import Message from platypush.message import Message
from platypush.utils import get_event_class_by_type from platypush.utils import get_event_class_by_type
class Event(Message): class Event(Message):
""" Event message class """ """ Event message class """
@ -39,6 +40,9 @@ class Event(Message):
self.args = kwargs self.args = kwargs
self.disable_logging = disable_logging self.disable_logging = disable_logging
for arg, value in self.args.items():
self.__setattr__(arg, value)
@classmethod @classmethod
def build(cls, msg): def build(cls, msg):
""" Builds an event message from a JSON UTF-8 string/bytearray, a """ 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() args['timestamp'] = msg['_timestamp'] if '_timestamp' in msg else time.time()
return event_class(**args) return event_class(**args)
@staticmethod @staticmethod
def _generate_id(): def _generate_id():
""" Generate a unique event ID """ """ Generate a unique event ID """
id = '' id = ''
for i in range(0,16): for i in range(0, 16):
id += '%.2x' % random.randint(0, 255) id += '%.2x' % random.randint(0, 255)
return id return id
def matches_condition(self, condition): def matches_condition(self, condition):
""" """
If the event matches an event condition, it will return an EventMatchResult 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) result = EventMatchResult(is_match=False, parsed_args=self.args)
match_scores = [] 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(): for (attr, value) in condition.args.items():
if attr not in self.args: if attr not in self.args:
@ -100,7 +103,6 @@ class Event(Message):
return result return result
def _matches_argument(self, argname, condition_value): def _matches_argument(self, argname, condition_value):
""" """
Returns an EventMatchResult if the event argument [argname] matches Returns an EventMatchResult if the event argument [argname] matches
@ -147,10 +149,9 @@ class Event(Message):
else: else:
result.parsed_args[argname] += ' ' + event_token result.parsed_args[argname] += ' ' + event_token
if (len(condition_tokens) == 1 and len(event_tokens) == 1) \ if (len(condition_tokens) == 1 and len(event_tokens) == 1) \
or (len(event_tokens) > 1 and len(condition_tokens) > 1 \ or (len(event_tokens) > 1 and len(condition_tokens) > 1
and event_tokens[1] == condition_tokens[1]): and event_tokens[1] == condition_tokens[1]):
# Stop appending tokens to this argument, as the next # Stop appending tokens to this argument, as the next
# condition will be satisfied as well # condition will be satisfied as well
condition_tokens.pop(0) condition_tokens.pop(0)
@ -164,7 +165,6 @@ class Event(Message):
result.is_match = len(condition_tokens) == 0 result.is_match = len(condition_tokens) == 0
return result return result
def __str__(self): def __str__(self):
""" """
Overrides the str() operator and converts Overrides the str() operator and converts
@ -175,13 +175,13 @@ class Event(Message):
flatten(args) flatten(args)
return json.dumps({ return json.dumps({
'type' : 'event', 'type': 'event',
'target' : self.target, 'target': self.target,
'origin' : self.origin if hasattr(self, 'origin') else None, 'origin': self.origin if hasattr(self, 'origin') else None,
'id' : self.id if hasattr(self, 'id') else None, 'id': self.id if hasattr(self, 'id') else None,
'_timestamp' : self.timestamp, '_timestamp': self.timestamp,
'args' : { 'args': {
'type' : self.type, 'type': self.type,
**args **args
}, },
}) })
@ -226,18 +226,16 @@ class StopEvent(Event):
def flatten(args): def flatten(args):
if isinstance(args, dict): if isinstance(args, dict):
for (key,value) in args.items(): for (key, value) in args.items():
if isinstance(value, date): if isinstance(value, date):
args[key] = value.isoformat() args[key] = value.isoformat()
elif isinstance(value, dict) or isinstance(value, list): elif isinstance(value, dict) or isinstance(value, list):
flatten(args[key]) flatten(args[key])
elif isinstance(args, list): elif isinstance(args, list):
for i in range(0,len(args)): for i in range(0, len(args)):
if isinstance(args[i], date): if isinstance(args[i], date):
args[i] = value.isoformat() args[i] = args[i].isoformat()
elif isinstance(args[i], dict) or isinstance(args[i], list): elif isinstance(args[i], dict) or isinstance(args[i], list):
flatten(args[i]) flatten(args[i])
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -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:

View File

@ -176,9 +176,12 @@ class GpioPlugin(Plugin):
Cleanup the state of the GPIO and resets PIN values. Cleanup the state of the GPIO and resets PIN values.
""" """
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
GPIO.cleanup()
self._initialized_pins = {} with self._init_lock:
self._initialized = False if self._initialized:
GPIO.cleanup()
self._initialized_pins = {}
self._initialized = False
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -1,5 +1,10 @@
import threading
import time 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 import action
from platypush.plugins.gpio import GpioPlugin from platypush.plugins.gpio import GpioPlugin
from platypush.plugins.gpio.sensor import GpioSensorPlugin from platypush.plugins.gpio.sensor import GpioSensorPlugin
@ -13,6 +18,11 @@ class GpioSensorDistancePlugin(GpioPlugin, GpioSensorPlugin):
Requires: Requires:
* ``RPi.GPIO`` (``pip install RPi.GPIO``) * ``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, 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). 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.trigger_pin = trigger_pin
self.echo_pin = echo_pin self.echo_pin = echo_pin
self.timeout = timeout self.timeout = timeout
self.warmup_time = warmup_time self.warmup_time = warmup_time
self._initialized = False self._measurement_thread: Optional[threading.Thread] = None
self._init_gpio() self._measurement_thread_lock = threading.RLock()
self._measurement_thread_can_run = False
def _init_gpio(self): self._init_board()
if self._initialized:
return
def _init_board(self):
import RPi.GPIO as GPIO 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)) with self._init_lock:
time.sleep(self.warmup_time) if self._initialized:
self.logger.info('Sensor ready') return
self._initialized = True
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): def _get_data(self):
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
pulse_start = pulse_on = time.time() pulse_start = pulse_on = time.time()
self._init_gpio() self._init_board()
GPIO.output(self.trigger_pin, GPIO.HIGH) GPIO.output(self.trigger_pin, GPIO.HIGH)
time.sleep(0.00001) # 1 us pulse to trigger echo measurement time.sleep(0.00001) # 1 us pulse to trigger echo measurement
GPIO.output(self.trigger_pin, GPIO.LOW) GPIO.output(self.trigger_pin, GPIO.LOW)
@ -94,7 +111,10 @@ class GpioSensorDistancePlugin(GpioPlugin, GpioSensorPlugin):
""" """
try: 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: except TimeoutError as e:
self.logger.warning(str(e)) self.logger.warning(str(e))
return return
@ -104,16 +124,67 @@ class GpioSensorDistancePlugin(GpioPlugin, GpioSensorPlugin):
@action @action
def close(self): def close(self):
import RPi.GPIO as GPIO return self.cleanup()
if self._initialized:
GPIO.cleanup()
self._initialized = False
def __enter__(self): def __enter__(self):
self._init_gpio() self._init_board()
def __exit__(self): def __exit__(self):
self.close() 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: # vim:sw=4:ts=4:et: