Added support for continuous checks on distance sensor and DistanceSensorEvent
This commit is contained in:
parent
9c4f917b53
commit
d6515ed991
4 changed files with 130 additions and 46 deletions
|
@ -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:
|
||||||
|
|
||||||
|
|
12
platypush/message/event/distance.py
Normal file
12
platypush/message/event/distance.py
Normal 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:
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue