mirror of
https://github.com/KevinMidboe/python-gpiozero.git
synced 2025-10-29 17:50:37 +00:00
Add events to all input devices
Fairly major tidy up of the hierarchy as well. There's now a trivial base class: InputDevice which simply permits reading of state. WaitableInputDevice descends from this and introduces waitable events and callbacks, and provides a hook for calling them but needs further machinery to activate that hook. DigitalInputDevice (crap name?) descends from WaitableInputDevice and uses the standard RPi.GPIO callback mechanisms to handle events. This is intended for use with trivial on/off devices with predictably small bounce times. Next is SmoothedInputDevice (crap name?) which also descends from WaitableInputDevice. This includes a background threaded queue which constantly monitors the state of the device and provides a running mean of its state. This is compared to a threshold for determining active / inactive state. This is intended for use with on/off devices that "jitter" a lot and for which a running average is therefore appropriate or for devices which provide an effectively analog readout (like charging capacitor timings). MonitorSensor and LightSensor now descend from SmoothedInputDevice, and Button descends from DigitalInputDevice. All "concrete" classes provide event aliases appropriate to their function (e.g. when_dark, when_pressed, etc.)
This commit is contained in:
@@ -72,7 +72,6 @@ Alternatively:
|
||||
|
||||
```python
|
||||
from gpiozero import LED
|
||||
from time import sleep
|
||||
|
||||
red = LED(2)
|
||||
red.blink(1, 1)
|
||||
@@ -118,7 +117,7 @@ from gpiozero import Button
|
||||
|
||||
button = Button(4)
|
||||
|
||||
button.wait_for_input()
|
||||
button.wait_for_press()
|
||||
print("Button was pressed")
|
||||
```
|
||||
|
||||
@@ -127,44 +126,54 @@ Run a function every time the button is pressed:
|
||||
```python
|
||||
from gpiozero import Button
|
||||
|
||||
def hello(pin):
|
||||
print("Button was pressed")
|
||||
def warning():
|
||||
print("Don't push the button!")
|
||||
|
||||
button = Button(4)
|
||||
|
||||
button.add_callback(hello)
|
||||
button.when_pressed = warning
|
||||
```
|
||||
|
||||
### Motion Sensor
|
||||
|
||||
Detect motion:
|
||||
Detect motion and light an LED when it's detected:
|
||||
|
||||
```python
|
||||
from gpiozero import MotionSensor
|
||||
from gpiozero import MotionSensor, LED
|
||||
|
||||
pir = MotionSensor(5)
|
||||
led = LED(16)
|
||||
|
||||
while True:
|
||||
if pir.motion_detected:
|
||||
print("Motion detected")
|
||||
pir.when_motion = led.on
|
||||
pir.when_no_motion = led.off
|
||||
```
|
||||
|
||||
### Light Sensor
|
||||
|
||||
Retrieve light sensor value:
|
||||
Wait for light and dark:
|
||||
|
||||
```python
|
||||
from time import sleep
|
||||
from gpiozero import LightSensor
|
||||
|
||||
sensor = LightSensor(18)
|
||||
|
||||
while True:
|
||||
sensor.wait_for_light()
|
||||
print("It's light! :)")
|
||||
sensor.wait_for_dark()
|
||||
print("It's dark :(")
|
||||
```
|
||||
|
||||
Run a function when the light changes:
|
||||
|
||||
```python
|
||||
from gpiozero import LightSensor, LED
|
||||
|
||||
sensor = LightSensor(18)
|
||||
led = LED(16)
|
||||
|
||||
sensor.when_dark = led.on
|
||||
sensor.when_light = led.off
|
||||
|
||||
while True:
|
||||
sleep(1)
|
||||
```
|
||||
|
||||
### Temperature Sensor
|
||||
@@ -196,3 +205,4 @@ sleep(5)
|
||||
left_motor.off()
|
||||
right_motor.off()
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import weakref
|
||||
from threading import Thread, Event
|
||||
from collections import deque
|
||||
|
||||
from RPi import GPIO
|
||||
|
||||
@@ -12,8 +14,14 @@ class GPIODevice(object):
|
||||
if pin is None:
|
||||
raise GPIODeviceError('No GPIO pin number given')
|
||||
self._pin = pin
|
||||
self._active_state = 1
|
||||
self._inactive_state = 0
|
||||
self._active_state = GPIO.HIGH
|
||||
self._inactive_state = GPIO.LOW
|
||||
|
||||
def _read(self):
|
||||
return GPIO.input(self.pin) == self._active_state
|
||||
|
||||
def _fire_events(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def pin(self):
|
||||
@@ -21,7 +29,7 @@ class GPIODevice(object):
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return GPIO.input(self.pin) == self._active_state
|
||||
return self._read()
|
||||
|
||||
|
||||
_GPIO_THREADS = set()
|
||||
@@ -47,3 +55,44 @@ class GPIOThread(Thread):
|
||||
self.join()
|
||||
_GPIO_THREADS.discard(self)
|
||||
|
||||
|
||||
class GPIOQueue(GPIOThread):
|
||||
def __init__(self, parent, queue_len=5, sample_wait=0.0, partial=False):
|
||||
assert isinstance(parent, GPIODevice)
|
||||
super(GPIOQueue, self).__init__(target=self.fill)
|
||||
if queue_len < 1:
|
||||
raise InputDeviceError('queue_len must be at least one')
|
||||
self.queue = deque(maxlen=queue_len)
|
||||
self.partial = partial
|
||||
self.sample_wait = sample_wait
|
||||
self.full = Event()
|
||||
self.parent = weakref.proxy(parent)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
if not self.partial:
|
||||
self.full.wait()
|
||||
try:
|
||||
return sum(self.queue) / len(self.queue)
|
||||
except ZeroDivisionError:
|
||||
# No data == inactive value
|
||||
return 0.0
|
||||
|
||||
def fill(self):
|
||||
try:
|
||||
while (
|
||||
not self.stopping.wait(self.sample_wait) and
|
||||
len(self.queue) < self.queue.maxlen
|
||||
):
|
||||
self.queue.append(self.parent._read())
|
||||
if self.partial:
|
||||
self.parent._fire_events()
|
||||
self.full.set()
|
||||
while not self.stopping.wait(self.sample_wait):
|
||||
self.queue.append(self.parent._read())
|
||||
self.parent._fire_events()
|
||||
except ReferenceError:
|
||||
# Parent is dead; time to die!
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -2,12 +2,17 @@ from __future__ import division
|
||||
|
||||
from time import sleep, time
|
||||
from threading import Event
|
||||
from collections import deque
|
||||
|
||||
from RPi import GPIO
|
||||
from w1thermsensor import W1ThermSensor
|
||||
|
||||
from .devices import GPIODeviceError, GPIODevice, GPIOThread
|
||||
from .devices import GPIODeviceError, GPIODevice, GPIOQueue
|
||||
|
||||
|
||||
def _alias(key):
|
||||
return property(
|
||||
lambda self: getattr(self, key),
|
||||
lambda self, val: setattr(self, key, val))
|
||||
|
||||
|
||||
class InputDeviceError(GPIODeviceError):
|
||||
@@ -15,13 +20,14 @@ class InputDeviceError(GPIODeviceError):
|
||||
|
||||
|
||||
class InputDevice(GPIODevice):
|
||||
def __init__(self, pin=None, pull_up=True):
|
||||
def __init__(self, pin=None, pull_up=False):
|
||||
super(InputDevice, self).__init__(pin)
|
||||
self._pull_up = pull_up
|
||||
self._edge = (GPIO.RISING, GPIO.FALLING)[pull_up]
|
||||
self._active_edge = (GPIO.RISING, GPIO.FALLING)[pull_up]
|
||||
self._inactive_edge = (GPIO.FALLING, GPIO.RISING)[pull_up]
|
||||
if pull_up:
|
||||
self._active_state = 0
|
||||
self._inactive_state = 1
|
||||
self._active_state = GPIO.LOW
|
||||
self._inactive_state = GPIO.HIGH
|
||||
GPIO.setup(pin, GPIO.IN, (GPIO.PUD_DOWN, GPIO.PUD_UP)[pull_up])
|
||||
|
||||
@property
|
||||
@@ -29,165 +35,159 @@ class InputDevice(GPIODevice):
|
||||
return self._pull_up
|
||||
|
||||
|
||||
class Button(InputDevice):
|
||||
pass
|
||||
class WaitableInputDevice(InputDevice):
|
||||
def __init__(self, pin=None, pull_up=False):
|
||||
super(WaitableInputDevice, self).__init__(pin, pull_up)
|
||||
self._active_event = Event()
|
||||
self._inactive_event = Event()
|
||||
self._when_activated = None
|
||||
self._when_deactivated = None
|
||||
self._last_state = None
|
||||
|
||||
def wait_for_active(self, timeout=None):
|
||||
return self._active_event.wait(timeout)
|
||||
|
||||
def wait_for_inactive(self, timeout=None):
|
||||
return self._inactive_event.wait(timeout)
|
||||
|
||||
def _get_when_activated(self):
|
||||
return self._when_activated
|
||||
def _set_when_activated(self, value):
|
||||
if not callable(value) and value is not None:
|
||||
raise InputDeviceError('value must be None or a function')
|
||||
self._when_activated = value
|
||||
when_activated = property(_get_when_activated, _set_when_activated)
|
||||
|
||||
def _get_when_deactivated(self):
|
||||
return self._when_deactivated
|
||||
def _set_when_deactivated(self, value):
|
||||
if not callable(value) and value is not None:
|
||||
raise InputDeviceError('value must be None or a function')
|
||||
self._when_deactivated = value
|
||||
when_deactivated = property(_get_when_deactivated, _set_when_deactivated)
|
||||
|
||||
def _fire_events(self):
|
||||
old_state = self._last_state
|
||||
new_state = self._last_state = self.is_active
|
||||
if old_state is None:
|
||||
# Initial "indeterminate" state; set events but don't fire
|
||||
# callbacks as there's not necessarily an edge
|
||||
if new_state:
|
||||
self._active_event.set()
|
||||
else:
|
||||
self._inactive_event.set()
|
||||
else:
|
||||
if not old_state and new_state:
|
||||
self._inactive_event.clear()
|
||||
self._active_event.set()
|
||||
if self.when_activated:
|
||||
self.when_activated()
|
||||
elif old_state and not new_state:
|
||||
self._active_event.clear()
|
||||
self._inactive_event.set()
|
||||
if self.when_deactivated:
|
||||
self.when_deactivated()
|
||||
|
||||
|
||||
class MotionSensor(InputDevice):
|
||||
class DigitalInputDevice(WaitableInputDevice):
|
||||
def __init__(self, pin=None, pull_up=False, bouncetime=None):
|
||||
super(DigitalInputDevice, self).__init__(pin, pull_up)
|
||||
# Yes, that's really the default bouncetime in RPi.GPIO...
|
||||
GPIO.add_event_detect(
|
||||
self.pin, GPIO.BOTH, callback=self._fire_events,
|
||||
bouncetime=-666 if bouncetime is None else bouncetime)
|
||||
# Call _fire_events once to set initial state of events
|
||||
super(DigitalInputDevice, self)._fire_events()
|
||||
|
||||
def __del__(self):
|
||||
GPIO.remove_event_detect(self.pin)
|
||||
|
||||
def _fire_events(self, channel):
|
||||
super(DigitalInputDevice, self)._fire_events()
|
||||
|
||||
|
||||
class SmoothedInputDevice(WaitableInputDevice):
|
||||
def __init__(
|
||||
self, pin=None, pull_up=False, threshold=0.5,
|
||||
queue_len=5, sample_wait=0.0, partial=False):
|
||||
super(SmoothedInputDevice, self).__init__(pin, pull_up)
|
||||
self._queue = GPIOQueue(self, queue_len, sample_wait, partial)
|
||||
self.threshold = float(threshold)
|
||||
|
||||
@property
|
||||
def queue_len(self):
|
||||
return self._queue.queue.maxlen
|
||||
|
||||
@property
|
||||
def partial(self):
|
||||
return self._queue.partial
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._queue.value
|
||||
|
||||
def _get_threshold(self):
|
||||
return self._threshold
|
||||
def _set_threshold(self, value):
|
||||
if not (0.0 < value < 1.0):
|
||||
raise InputDeviceError('threshold must be between zero and one exclusive')
|
||||
self._threshold = float(value)
|
||||
threshold = property(_get_threshold, _set_threshold)
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.value > self.threshold
|
||||
|
||||
|
||||
class Button(DigitalInputDevice):
|
||||
def __init__(self, pin=None, pull_up=True, bouncetime=None):
|
||||
super(Button, self).__init__(pin, pull_up, bouncetime)
|
||||
|
||||
when_pressed = _alias('when_activated')
|
||||
when_released = _alias('when_deactivated')
|
||||
|
||||
wait_for_press = _alias('wait_for_active')
|
||||
wait_for_release = _alias('wait_for_inactive')
|
||||
|
||||
|
||||
class MotionSensor(SmoothedInputDevice):
|
||||
def __init__(
|
||||
self, pin=None, queue_len=5, sample_rate=10, threshold=0.5,
|
||||
partial=False):
|
||||
super(MotionSensor, self).__init__(pin, pull_up=False)
|
||||
if queue_len < 1:
|
||||
raise InputDeviceError('queue_len must be at least one')
|
||||
self.sample_rate = sample_rate
|
||||
self.threshold = threshold
|
||||
self.partial = partial
|
||||
self._queue = deque(maxlen=queue_len)
|
||||
self._queue_full = Event()
|
||||
self._queue_thread = GPIOThread(target=self._fill_queue)
|
||||
self._queue_thread.start()
|
||||
super(MotionSensor, self).__init__(
|
||||
pin, pull_up=False, threshold=threshold,
|
||||
queue_len=queue_len, sample_wait=1 / sample_rate, partial=partial)
|
||||
self._queue.start()
|
||||
|
||||
@property
|
||||
def queue_len(self):
|
||||
return self._queue.maxlen
|
||||
motion_detected = _alias('is_active')
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
if not self.partial:
|
||||
self._queue_full.wait()
|
||||
try:
|
||||
return sum(self._queue) / len(self._queue)
|
||||
except ZeroDivisionError:
|
||||
# No data == no motion
|
||||
return 0.0
|
||||
when_motion = _alias('when_activated')
|
||||
when_no_motion = _alias('when_deactivated')
|
||||
|
||||
@property
|
||||
def motion_detected(self):
|
||||
return self.value > self.threshold
|
||||
|
||||
def _get_sample_rate(self):
|
||||
return self._sample_rate
|
||||
def _set_sample_rate(self, value):
|
||||
if value <= 0:
|
||||
raise InputDeviceError('sample_rate must be greater than zero')
|
||||
self._sample_rate = value
|
||||
sample_rate = property(_get_sample_rate, _set_sample_rate)
|
||||
|
||||
def _get_threshold(self):
|
||||
return self._threshold
|
||||
def _set_threshold(self, value):
|
||||
if value < 0:
|
||||
raise InputDeviceError('threshold must be zero or more')
|
||||
self._threshold = value
|
||||
threshold = property(_get_threshold, _set_threshold)
|
||||
|
||||
def _fill_queue(self):
|
||||
while (
|
||||
not self._queue_thread.stopping.wait(1 / self.sample_rate) and
|
||||
len(self._queue) < self._queue.maxlen
|
||||
):
|
||||
self._queue.append(self.is_active)
|
||||
self._queue_full.set()
|
||||
while not self._queue_thread.stopping.wait(1 / self.sample_rate):
|
||||
self._queue.append(self.is_active)
|
||||
wait_for_motion = _alias('wait_for_active')
|
||||
wait_for_no_motion = _alias('wait_for_inactive')
|
||||
|
||||
|
||||
class LightSensor(InputDevice):
|
||||
class LightSensor(SmoothedInputDevice):
|
||||
def __init__(
|
||||
self, pin=None, queue_len=5, darkness_time=0.01,
|
||||
self, pin=None, queue_len=5, charge_time_limit=0.01,
|
||||
threshold=0.1, partial=False):
|
||||
super(LightSensor, self).__init__(pin, pull_up=False)
|
||||
if queue_len < 1:
|
||||
raise InputDeviceError('queue_len must be at least one')
|
||||
self.darkness_time = darkness_time
|
||||
self.threshold = threshold
|
||||
self.partial = partial
|
||||
super(LightSensor, self).__init__(
|
||||
pin, pull_up=False, threshold=threshold,
|
||||
queue_len=queue_len, sample_wait=0.0, partial=partial)
|
||||
self._charge_time_limit = charge_time_limit
|
||||
self._charged = Event()
|
||||
GPIO.add_event_detect(self.pin, GPIO.RISING, lambda channel: self._charged.set())
|
||||
self._queue = deque(maxlen=queue_len)
|
||||
self._queue_full = Event()
|
||||
self._queue_thread = GPIOThread(target=self._fill_queue)
|
||||
self._last_state = None
|
||||
self._when_light = None
|
||||
self._when_dark = None
|
||||
self._when_light_event = Event()
|
||||
self._when_dark_event = Event()
|
||||
self._queue_thread.start()
|
||||
self._queue.start()
|
||||
|
||||
def __del__(self):
|
||||
GPIO.remove_event_detect(self.pin)
|
||||
|
||||
@property
|
||||
def queue_len(self):
|
||||
return self._queue.maxlen
|
||||
def charge_time_limit(self):
|
||||
return self._charge_time_limit
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
if not self.partial:
|
||||
self._queue_full.wait()
|
||||
try:
|
||||
return 1.0 - (sum(self._queue) / len(self._queue)) / self.darkness_time
|
||||
except ZeroDivisionError:
|
||||
# No data == no light
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def light_detected(self):
|
||||
return self.value > self.threshold
|
||||
|
||||
def _get_when_light(self):
|
||||
return self._when_light
|
||||
def _set_when_light(self, value):
|
||||
if not callable(value) and value is not None:
|
||||
raise InputDeviceError('when_light must be None or a function')
|
||||
self._when_light = value
|
||||
when_light = property(_get_when_light, _set_when_light)
|
||||
|
||||
def _get_when_dark(self):
|
||||
return self._when_dark
|
||||
def _set_when_dark(self, value):
|
||||
if not callable(value) and value is not None:
|
||||
raise InputDeviceError('when_dark must be None or a function')
|
||||
self._when_dark = value
|
||||
|
||||
def wait_for_light(self, timeout=None):
|
||||
self._when_light_event.wait(timeout)
|
||||
|
||||
def wait_for_dark(self, timeout=None):
|
||||
self._when_dark_event.wait(timeout)
|
||||
|
||||
def _get_darkness_time(self):
|
||||
return self._darkness_time
|
||||
def _set_darkness_time(self, value):
|
||||
if value <= 0.0:
|
||||
raise InputDeviceError('darkness_time must be greater than zero')
|
||||
self._darkness_time = value
|
||||
# XXX Empty the queue and restart the thread
|
||||
darkness_time = property(_get_darkness_time, _set_darkness_time)
|
||||
|
||||
def _get_threshold(self):
|
||||
return self._threshold
|
||||
def _set_threshold(self, value):
|
||||
if value < 0:
|
||||
raise InputDeviceError('threshold must be zero or more')
|
||||
self._threshold = value
|
||||
threshold = property(_get_threshold, _set_threshold)
|
||||
|
||||
def _fill_queue(self):
|
||||
try:
|
||||
while (
|
||||
not self._queue_thread.stopping.is_set() and
|
||||
len(self._queue) < self._queue.maxlen
|
||||
):
|
||||
self._queue.append(self._time_charging())
|
||||
if self.partial:
|
||||
self._fire_events()
|
||||
self._queue_full.set()
|
||||
while not self._queue_thread.stopping.is_set():
|
||||
self._queue.append(self._time_charging())
|
||||
self._fire_events()
|
||||
finally:
|
||||
GPIO.remove_event_detect(self.pin)
|
||||
|
||||
def _time_charging(self):
|
||||
def _read(self):
|
||||
# Drain charge from the capacitor
|
||||
GPIO.setup(self.pin, GPIO.OUT)
|
||||
GPIO.output(self.pin, GPIO.LOW)
|
||||
@@ -196,26 +196,20 @@ class LightSensor(InputDevice):
|
||||
start = time()
|
||||
self._charged.clear()
|
||||
GPIO.setup(self.pin, GPIO.IN)
|
||||
self._charged.wait(self.darkness_time)
|
||||
return min(self.darkness_time, time() - start)
|
||||
self._charged.wait(self.charge_time_limit)
|
||||
return 1.0 - min(self.charge_time_limit, time() - start) / self.charge_time_limit
|
||||
|
||||
light_detected = _alias('is_active')
|
||||
|
||||
when_light = _alias('when_activated')
|
||||
when_dark = _alias('when_deactivated')
|
||||
|
||||
wait_for_light = _alias('wait_for_active')
|
||||
wait_for_dark = _alias('wait_for_inactive')
|
||||
|
||||
def _fire_events(self):
|
||||
last_state = self._last_state
|
||||
self._last_state = state = self.light_detected
|
||||
if not last_state and state:
|
||||
self._when_dark_event.clear()
|
||||
self._when_light_event.set()
|
||||
if self.when_light:
|
||||
self.when_light()
|
||||
elif last_state and not state:
|
||||
self._when_light_event.clear()
|
||||
self._when_dark_event.set()
|
||||
if self.when_dark:
|
||||
self.when_dark()
|
||||
|
||||
class TemperatureSensor(W1ThermSensor):
|
||||
@property
|
||||
def value(self):
|
||||
return self.get_temperature()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user