Shut down GPIO threads nicely

The motion sensor queue doesn't shut down properly at script end at the
moment and prevents the interpreter shutting down. This is because it's
a non-daemon thread so `__del__` never gets run and so on.

This is a bit of a major PR - I can split it up if you want. Firstly it
makes a common base class called `GPIODevice` for both `InputDevice` and
`OutputDevice`. This just takes care of the read-only pin stuff. Next it
makes a `GPIOThread` class that ensures its a daemon thread, and which
also ensures proper cleanup on shutdown.

Finally, it fixes `MotionSensor` to use the new `GPIOThread` class
(tested this time! Works nicely) and adds the `blink` method to the
`LED` class (which also works nicely this time).
This commit is contained in:
Dave Jones
2015-09-18 11:19:13 +01:00
parent c810730cf5
commit e2ddad6fea
4 changed files with 118 additions and 37 deletions

View File

@@ -4,14 +4,11 @@ import atexit
from RPi import GPIO
def gpiozero_shutdown():
GPIO.cleanup()
atexit.register(gpiozero_shutdown)
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
from .devices import (
_gpio_threads_shutdown,
GPIODeviceError,
GPIODevice,
)
from .input_devices import (
InputDeviceError,
InputDevice,
@@ -26,3 +23,13 @@ from .output_devices import (
Buzzer,
Motor,
)
def gpiozero_shutdown():
_gpio_threads_shutdown()
GPIO.cleanup()
atexit.register(gpiozero_shutdown)
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

43
gpiozero/devices.py Normal file
View File

@@ -0,0 +1,43 @@
from threading import Thread, Event
from RPi import GPIO
class GPIODeviceError(Exception):
pass
class GPIODevice(object):
def __init__(self, pin=None):
if pin is None:
raise GPIODeviceError('No GPIO pin number given')
self._pin = pin
@property
def pin(self):
return self._pin
_GPIO_THREADS = set()
def _gpio_threads_shutdown():
while _GPIO_THREADS:
for t in _GPIO_THREADS.copy():
t.stop()
class GPIOThread(Thread):
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
super(GPIOThread, self).__init__(group, target, name, args, kwargs)
self.stopping = Event()
self.daemon = True
def start(self):
self.stopping.clear()
_GPIO_THREADS.add(self)
super(GPIOThread, self).start()
def stop(self):
self.stopping.set()
self.join()
_GPIO_THREADS.discard(self)

View File

@@ -1,29 +1,28 @@
from __future__ import division
from time import sleep
from threading import Thread, Event
from threading import Event
from collections import deque
from RPi import GPIO
from w1thermsensor import W1ThermSensor
from .devices import GPIODeviceError, GPIODevice, GPIOThread
class InputDevice(object):
class InputDeviceError(GPIODeviceError):
pass
class InputDevice(GPIODevice):
def __init__(self, pin=None):
if pin is None:
raise InputDeviceError('No GPIO pin number given')
self._pin = pin
super(InputDevice, self).__init__(pin)
self._pull = GPIO.PUD_UP
self._edge = GPIO.FALLING
self._active_state = 0
self._inactive_state = 1
GPIO.setup(pin, GPIO.IN, self._pull)
@property
def pin(self):
return self._pin
@property
def is_active(self):
return GPIO.input(self.pin) == self._active_state
@@ -34,7 +33,6 @@ class InputDevice(object):
def add_callback(self, callback=None, bouncetime=1000):
if callback is None:
raise InputDeviceError('No callback function given')
GPIO.add_event_detect(self.pin, self._edge, callback, bouncetime)
def remove_callback(self):
@@ -48,30 +46,30 @@ class Button(InputDevice):
class MotionSensor(InputDevice):
def __init__(self, pin=None, queue_len=20, sample_rate=10, partial=False):
super(MotionSensor, self).__init__(pin)
self._sample_rate = sample_rate
self._partial = partial
self.sample_rate = sample_rate
self.partial = partial
self._queue = deque(maxlen=queue_len)
self._queue_full = Event()
self._terminated = False
self._queue_thread = Thread(target=self._fill_queue)
self._queue_thread = GPIOThread(target=self._fill_queue)
self._queue_thread.start()
def __del__(self):
self._terminated = True
self._queue_thread.join()
@property
def queue_len(self):
return self._queue.maxlen
def _fill_queue(self):
while not self._terminated and len(self._queue) < self._queue.maxlen:
while (
not self._queue_thread.stopping.wait(1 / self.sample_rate) and
len(self._queue) < self._queue.maxlen
):
self._queue.append(self.is_active)
sleep(1 / self._sample_rate)
self._queue_full.set()
while not self._terminated:
while not self._queue_thread.stopping.wait(1 / self.sample_rate):
self._queue.append(self.is_active)
sleep(1 / self._sample_rate)
@property
def motion_detected(self):
if not self._partial:
if not self.partial:
self._queue_full.wait()
return sum(self._queue) > (len(self._queue) / 2)
@@ -124,5 +122,3 @@ class TemperatureSensor(W1ThermSensor):
return self.get_temperature()
class InputDeviceError(Exception):
pass

View File

@@ -1,9 +1,15 @@
from RPi import GPIO
from .devices import GPIODeviceError, GPIODevice, GPIOThread
class OutputDevice(object):
def __init__(self, pin):
self.pin = pin
class OutputDeviceError(GPIODeviceError):
pass
class OutputDevice(GPIODevice):
def __init__(self, pin=None):
super(OutputDevice, self).__init__(pin)
GPIO.setup(pin, GPIO.OUT)
def on(self):
@@ -14,7 +20,36 @@ class OutputDevice(object):
class LED(OutputDevice):
pass
def __init__(self, pin=None):
super(LED, self).__init__(pin)
self._blink_thread = None
def blink(self, on_time, off_time):
self._stop_blink()
self._blink_thread = GPIOThread(target=self._blink_led, args=(on_time, off_time))
self._blink_thread.start()
def _stop_blink(self):
if self._blink_thread:
self._blink_thread.stop()
self._blink_thread = None
def _blink_led(self, on_time, off_time):
while True:
super(LED, self).on()
if self._blink_thread.stopping.wait(on_time):
break
super(LED, self).off()
if self._blink_thread.stopping.wait(off_time):
break
def on(self):
self._stop_blink()
super(LED, self).on()
def off(self):
self._stop_blink()
super(LED, self).off()
class Buzzer(OutputDevice):