mirror of
https://github.com/KevinMidboe/python-gpiozero.git
synced 2025-10-29 17:50:37 +00:00
Fix #115
Adds when_held event hook to Button (via extension of the EventsMixin class). Also fixes some minor notes and activates codecov coverage tracking.
This commit is contained in:
@@ -9,5 +9,11 @@ python:
|
|||||||
- "pypy3"
|
- "pypy3"
|
||||||
install: "pip install -e .[test]"
|
install: "pip install -e .[test]"
|
||||||
script: make test
|
script: make test
|
||||||
|
before_install:
|
||||||
|
# Coverage 4.0 no longer supports py3.2 and codecov depends on latest coverage
|
||||||
|
- if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install "coverage<4.0dev"; fi
|
||||||
|
- pip install codecov
|
||||||
|
after_success:
|
||||||
|
- codecov
|
||||||
notifications:
|
notifications:
|
||||||
slack: raspberrypifoundation:YoIHtVdg8Hd6gcA09QEmCYXN
|
slack: raspberrypifoundation:YoIHtVdg8Hd6gcA09QEmCYXN
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ Errors
|
|||||||
|
|
||||||
.. autoexception:: BadEventHandler
|
.. autoexception:: BadEventHandler
|
||||||
|
|
||||||
|
.. autoexception:: BadQueueLen
|
||||||
|
|
||||||
|
.. autoexception:: BadWaitTime
|
||||||
|
|
||||||
.. autoexception:: CompositeDeviceError
|
.. autoexception:: CompositeDeviceError
|
||||||
|
|
||||||
.. autoexception:: CompositeDeviceBadName
|
.. autoexception:: CompositeDeviceBadName
|
||||||
@@ -63,10 +67,6 @@ Errors
|
|||||||
|
|
||||||
.. autoexception:: GPIOPinMissing
|
.. autoexception:: GPIOPinMissing
|
||||||
|
|
||||||
.. autoexception:: GPIOBadQueueLen
|
|
||||||
|
|
||||||
.. autoexception:: GPIOBadSampleWait
|
|
||||||
|
|
||||||
.. autoexception:: InputDeviceError
|
.. autoexception:: InputDeviceError
|
||||||
|
|
||||||
.. autoexception:: OutputDeviceError
|
.. autoexception:: OutputDeviceError
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ There are also several `mixin classes`_:
|
|||||||
* :class:`EventsMixin` which adds activated/deactivated events to devices
|
* :class:`EventsMixin` which adds activated/deactivated events to devices
|
||||||
along with the machinery to trigger those events
|
along with the machinery to trigger those events
|
||||||
|
|
||||||
|
* :class:`HoldMixin` which derives from :class:`EventsMixin` and adds the
|
||||||
|
held event to devices along with the machinery to repeatedly trigger it
|
||||||
|
|
||||||
.. _mixin classes: https://en.wikipedia.org/wiki/Mixin
|
.. _mixin classes: https://en.wikipedia.org/wiki/Mixin
|
||||||
|
|
||||||
The current class hierarchies are displayed below. For brevity, the mixin
|
The current class hierarchies are displayed below. For brevity, the mixin
|
||||||
@@ -141,3 +144,6 @@ Mixin Classes
|
|||||||
.. autoclass:: EventsMixin(...)
|
.. autoclass:: EventsMixin(...)
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: HoldMixin(...)
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ to utilize pins that are part of IO extender chips. For example::
|
|||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
The astute and mischievious reader may note that it is possible to mix pin
|
The astute and mischievous reader may note that it is possible to mix pin
|
||||||
implementations, e.g. using ``RPiGPIOPin`` for one pin, and ``NativePin``
|
implementations, e.g. using ``RPiGPIOPin`` for one pin, and ``NativePin``
|
||||||
for another. This is unsupported, and if it results in your script
|
for another. This is unsupported, and if it results in your script
|
||||||
crashing, your components failing, or your Raspberry Pi turning into an
|
crashing, your components failing, or your Raspberry Pi turning into an
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 56 KiB |
Binary file not shown.
@@ -17,6 +17,8 @@ from .exc import (
|
|||||||
GPIOZeroError,
|
GPIOZeroError,
|
||||||
DeviceClosed,
|
DeviceClosed,
|
||||||
BadEventHandler,
|
BadEventHandler,
|
||||||
|
BadWaitTime,
|
||||||
|
BadQueueLen,
|
||||||
CompositeDeviceError,
|
CompositeDeviceError,
|
||||||
CompositeDeviceBadName,
|
CompositeDeviceBadName,
|
||||||
CompositeDeviceBadOrder,
|
CompositeDeviceBadOrder,
|
||||||
@@ -28,8 +30,6 @@ from .exc import (
|
|||||||
GPIODeviceClosed,
|
GPIODeviceClosed,
|
||||||
GPIOPinInUse,
|
GPIOPinInUse,
|
||||||
GPIOPinMissing,
|
GPIOPinMissing,
|
||||||
GPIOBadQueueLen,
|
|
||||||
GPIOBadSampleWait,
|
|
||||||
InputDeviceError,
|
InputDeviceError,
|
||||||
OutputDeviceError,
|
OutputDeviceError,
|
||||||
OutputDeviceBadValue,
|
OutputDeviceBadValue,
|
||||||
@@ -61,6 +61,7 @@ from .mixins import (
|
|||||||
SourceMixin,
|
SourceMixin,
|
||||||
ValuesMixin,
|
ValuesMixin,
|
||||||
EventsMixin,
|
EventsMixin,
|
||||||
|
HoldMixin,
|
||||||
)
|
)
|
||||||
from .input_devices import (
|
from .input_devices import (
|
||||||
InputDevice,
|
InputDevice,
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ class DeviceClosed(GPIOZeroError):
|
|||||||
class BadEventHandler(GPIOZeroError, ValueError):
|
class BadEventHandler(GPIOZeroError, ValueError):
|
||||||
"Error raised when an event handler with an incompatible prototype is specified"
|
"Error raised when an event handler with an incompatible prototype is specified"
|
||||||
|
|
||||||
|
class BadWaitTime(GPIOZeroError, ValueError):
|
||||||
|
"Error raised when an invalid wait time is specified"
|
||||||
|
|
||||||
|
class BadQueueLen(GPIOZeroError, ValueError):
|
||||||
|
"Error raised when non-positive queue length is specified"
|
||||||
|
|
||||||
class CompositeDeviceError(GPIOZeroError):
|
class CompositeDeviceError(GPIOZeroError):
|
||||||
"Base class for errors specific to the CompositeDevice hierarchy"
|
"Base class for errors specific to the CompositeDevice hierarchy"
|
||||||
|
|
||||||
@@ -49,15 +55,6 @@ class GPIOPinInUse(GPIODeviceError):
|
|||||||
class GPIOPinMissing(GPIODeviceError, ValueError):
|
class GPIOPinMissing(GPIODeviceError, ValueError):
|
||||||
"Error raised when a pin number is not specified"
|
"Error raised when a pin number is not specified"
|
||||||
|
|
||||||
class GPIOBadQueueLen(GPIODeviceError, ValueError):
|
|
||||||
"Error raised when non-positive queue length is specified"
|
|
||||||
|
|
||||||
class GPIOBadSampleWait(GPIODeviceError, ValueError):
|
|
||||||
"Error raised when a negative sampling wait period is specified"
|
|
||||||
|
|
||||||
class GPIOBadSourceDelay(GPIODeviceError, ValueError):
|
|
||||||
"Error raised when a negative source delay is specified"
|
|
||||||
|
|
||||||
class InputDeviceError(GPIODeviceError):
|
class InputDeviceError(GPIODeviceError):
|
||||||
"Base class for errors specific to the InputDevice hierarchy"
|
"Base class for errors specific to the InputDevice hierarchy"
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from threading import Event
|
|||||||
|
|
||||||
from .exc import InputDeviceError, DeviceClosed
|
from .exc import InputDeviceError, DeviceClosed
|
||||||
from .devices import GPIODevice
|
from .devices import GPIODevice
|
||||||
from .mixins import GPIOQueue, EventsMixin
|
from .mixins import GPIOQueue, EventsMixin, HoldMixin
|
||||||
|
|
||||||
|
|
||||||
class InputDevice(GPIODevice):
|
class InputDevice(GPIODevice):
|
||||||
@@ -222,7 +222,7 @@ class SmoothedInputDevice(EventsMixin, InputDevice):
|
|||||||
return self.value > self.threshold
|
return self.value > self.threshold
|
||||||
|
|
||||||
|
|
||||||
class Button(DigitalInputDevice):
|
class Button(HoldMixin, DigitalInputDevice):
|
||||||
"""
|
"""
|
||||||
Extends :class:`DigitalInputDevice` and represents a simple push button
|
Extends :class:`DigitalInputDevice` and represents a simple push button
|
||||||
or switch.
|
or switch.
|
||||||
@@ -254,11 +254,24 @@ class Button(DigitalInputDevice):
|
|||||||
If ``None`` (the default), no software bounce compensation will be
|
If ``None`` (the default), no software bounce compensation will be
|
||||||
performed. Otherwise, this is the length in time (in seconds) that the
|
performed. Otherwise, this is the length in time (in seconds) that the
|
||||||
component will ignore changes in state after an initial change.
|
component will ignore changes in state after an initial change.
|
||||||
|
|
||||||
|
:param float hold_time:
|
||||||
|
The length of time (in seconds) to wait after the button is pushed,
|
||||||
|
until executing the :attr:`when_held` handler.
|
||||||
|
|
||||||
|
:param bool hold_repeat:
|
||||||
|
If ``True``, the :attr:`when_held` handler will be repeatedly executed
|
||||||
|
as long as the device remains active, every *hold_time* seconds.
|
||||||
"""
|
"""
|
||||||
def __init__(self, pin=None, pull_up=True, bounce_time=None):
|
def __init__(
|
||||||
|
self, pin=None, pull_up=True, bounce_time=None,
|
||||||
|
hold_time=1, hold_repeat=False):
|
||||||
super(Button, self).__init__(pin, pull_up, bounce_time)
|
super(Button, self).__init__(pin, pull_up, bounce_time)
|
||||||
|
self.hold_time = hold_time
|
||||||
|
self.hold_repeat = hold_repeat
|
||||||
|
|
||||||
Button.is_pressed = Button.is_active
|
Button.is_pressed = Button.is_active
|
||||||
|
Button.pressed_time = Button.active_time
|
||||||
Button.when_pressed = Button.when_activated
|
Button.when_pressed = Button.when_activated
|
||||||
Button.when_released = Button.when_deactivated
|
Button.when_released = Button.when_deactivated
|
||||||
Button.wait_for_press = Button.wait_for_active
|
Button.wait_for_press = Button.wait_for_active
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import weakref
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from threading import Event
|
from threading import Event
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from time import time
|
||||||
try:
|
try:
|
||||||
from statistics import median
|
from statistics import median
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -20,10 +21,9 @@ except ImportError:
|
|||||||
from .threads import GPIOThread
|
from .threads import GPIOThread
|
||||||
from .exc import (
|
from .exc import (
|
||||||
BadEventHandler,
|
BadEventHandler,
|
||||||
|
BadWaitTime,
|
||||||
|
BadQueueLen,
|
||||||
DeviceClosed,
|
DeviceClosed,
|
||||||
GPIOBadSourceDelay,
|
|
||||||
GPIOBadQueueLen,
|
|
||||||
GPIOBadSampleWait,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class ValuesMixin(object):
|
class ValuesMixin(object):
|
||||||
@@ -89,7 +89,7 @@ class SourceMixin(object):
|
|||||||
@source_delay.setter
|
@source_delay.setter
|
||||||
def source_delay(self, value):
|
def source_delay(self, value):
|
||||||
if value < 0:
|
if value < 0:
|
||||||
raise GPIOBadSourceDelay('source_delay must be 0 or greater')
|
raise BadWaitTime('source_delay must be 0 or greater')
|
||||||
self._source_delay = float(value)
|
self._source_delay = float(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -160,6 +160,7 @@ class EventsMixin(object):
|
|||||||
self._when_activated = None
|
self._when_activated = None
|
||||||
self._when_deactivated = None
|
self._when_deactivated = None
|
||||||
self._last_state = None
|
self._last_state = None
|
||||||
|
self._last_changed = time()
|
||||||
|
|
||||||
def wait_for_active(self, timeout=None):
|
def wait_for_active(self, timeout=None):
|
||||||
"""
|
"""
|
||||||
@@ -223,6 +224,28 @@ class EventsMixin(object):
|
|||||||
def when_deactivated(self, value):
|
def when_deactivated(self, value):
|
||||||
self._when_deactivated = self._wrap_callback(value)
|
self._when_deactivated = self._wrap_callback(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_time(self):
|
||||||
|
"""
|
||||||
|
The length of time (in seconds) that the device has been active for.
|
||||||
|
When the device is inactive, this is ``None``.
|
||||||
|
"""
|
||||||
|
if self._active_event.wait(0):
|
||||||
|
return time() - self._last_changed
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inactive_time(self):
|
||||||
|
"""
|
||||||
|
The length of time (in seconds) that the device has been inactive for.
|
||||||
|
When the device is inactive, this is ``None``.
|
||||||
|
"""
|
||||||
|
if self._inactive_event.wait(0):
|
||||||
|
return time() - self._last_changed
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def _wrap_callback(self, fn):
|
def _wrap_callback(self, fn):
|
||||||
if fn is None:
|
if fn is None:
|
||||||
return None
|
return None
|
||||||
@@ -256,6 +279,16 @@ class EventsMixin(object):
|
|||||||
'value must be a callable which accepts up to one '
|
'value must be a callable which accepts up to one '
|
||||||
'mandatory parameter')
|
'mandatory parameter')
|
||||||
|
|
||||||
|
def _fire_activated(self):
|
||||||
|
# These methods are largely here to be overridden by descendents
|
||||||
|
if self.when_activated:
|
||||||
|
self.when_activated()
|
||||||
|
|
||||||
|
def _fire_deactivated(self):
|
||||||
|
# These methods are largely here to be overridden by descendents
|
||||||
|
if self.when_deactivated:
|
||||||
|
self.when_deactivated()
|
||||||
|
|
||||||
def _fire_events(self):
|
def _fire_events(self):
|
||||||
old_state = self._last_state
|
old_state = self._last_state
|
||||||
new_state = self._last_state = self.is_active
|
new_state = self._last_state = self.is_active
|
||||||
@@ -266,17 +299,143 @@ class EventsMixin(object):
|
|||||||
self._active_event.set()
|
self._active_event.set()
|
||||||
else:
|
else:
|
||||||
self._inactive_event.set()
|
self._inactive_event.set()
|
||||||
else:
|
elif old_state != new_state:
|
||||||
if not old_state and new_state:
|
self._last_changed = time()
|
||||||
|
if new_state:
|
||||||
self._inactive_event.clear()
|
self._inactive_event.clear()
|
||||||
self._active_event.set()
|
self._active_event.set()
|
||||||
if self.when_activated:
|
self._fire_activated()
|
||||||
self.when_activated()
|
else:
|
||||||
elif old_state and not new_state:
|
|
||||||
self._active_event.clear()
|
self._active_event.clear()
|
||||||
self._inactive_event.set()
|
self._inactive_event.set()
|
||||||
if self.when_deactivated:
|
self._fire_deactivated()
|
||||||
self.when_deactivated()
|
|
||||||
|
|
||||||
|
class HoldMixin(EventsMixin):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(HoldMixin, self).__init__(*args, **kwargs)
|
||||||
|
self._when_held = None
|
||||||
|
self._held_from = None
|
||||||
|
self._hold_time = 1
|
||||||
|
self._hold_repeat = False
|
||||||
|
self._hold_thread = HoldThread(self)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._hold_thread:
|
||||||
|
self._hold_thread.stop()
|
||||||
|
self._hold_thread = None
|
||||||
|
try:
|
||||||
|
super(HoldMixin, self).close()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _fire_activated(self):
|
||||||
|
super(HoldMixin, self)._fire_activated()
|
||||||
|
self._hold_thread.holding.set()
|
||||||
|
|
||||||
|
def _fire_deactivated(self):
|
||||||
|
self._held_from = None
|
||||||
|
super(HoldMixin, self)._fire_deactivated()
|
||||||
|
|
||||||
|
def _fire_held(self):
|
||||||
|
if self.when_held:
|
||||||
|
self.when_held()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def when_held(self):
|
||||||
|
"""
|
||||||
|
The function to run when the device has remained active for
|
||||||
|
:attr:`hold_time` seconds.
|
||||||
|
|
||||||
|
This can be set to a function which accepts no (mandatory) parameters,
|
||||||
|
or a Python function which accepts a single mandatory parameter (with
|
||||||
|
as many optional parameters as you like). If the function accepts a
|
||||||
|
single mandatory parameter, the device that activated will be passed
|
||||||
|
as that parameter.
|
||||||
|
|
||||||
|
Set this property to ``None`` (the default) to disable the event.
|
||||||
|
"""
|
||||||
|
return self._when_held
|
||||||
|
|
||||||
|
@when_held.setter
|
||||||
|
def when_held(self, value):
|
||||||
|
self._when_held = self._wrap_callback(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hold_time(self):
|
||||||
|
"""
|
||||||
|
The length of time (in seconds) to wait after the device is activated,
|
||||||
|
until executing the :attr:`when_held` handler. If :attr:`hold_repeat`
|
||||||
|
is True, this is also the length of time between invocations of
|
||||||
|
:attr:`when_held`.
|
||||||
|
"""
|
||||||
|
return self._hold_time
|
||||||
|
|
||||||
|
@hold_time.setter
|
||||||
|
def hold_time(self, value):
|
||||||
|
if value < 0:
|
||||||
|
raise BadWaitTime('source_delay must be 0 or greater')
|
||||||
|
self._hold_time = float(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hold_repeat(self):
|
||||||
|
"""
|
||||||
|
If ``True``, :attr:`when_held` will be executed repeatedly with
|
||||||
|
:attr:`hold_time` seconds between each invocation.
|
||||||
|
"""
|
||||||
|
return self._hold_repeat
|
||||||
|
|
||||||
|
@hold_repeat.setter
|
||||||
|
def hold_repeat(self, value):
|
||||||
|
self._hold_repeat = bool(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_held(self):
|
||||||
|
"""
|
||||||
|
When ``True``, the device has been active for at least
|
||||||
|
:attr:`hold_time` seconds.
|
||||||
|
"""
|
||||||
|
return self._held_from is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def held_time(self):
|
||||||
|
"""
|
||||||
|
The length of time (in seconds) that the device has been held for.
|
||||||
|
This is counted from the first execution of the :attr:`when_held` event
|
||||||
|
rather than when the device activated, in contrast to
|
||||||
|
:attr:`active_time`. If the device is not currently held, this is
|
||||||
|
``None``.
|
||||||
|
"""
|
||||||
|
if self._held_from is not None:
|
||||||
|
return time() - self._held_from
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class HoldThread(GPIOThread):
|
||||||
|
"""
|
||||||
|
Extends :class:`GPIOThread`. Provides a background thread that repeatedly
|
||||||
|
fires the :attr:`HoldMixin.when_held` event as long as the owning
|
||||||
|
device is active.
|
||||||
|
"""
|
||||||
|
def __init__(self, parent):
|
||||||
|
super(HoldThread, self).__init__(target=self.held, args=(parent,))
|
||||||
|
self.holding = Event()
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def held(self, parent):
|
||||||
|
while not self.stopping.wait(0):
|
||||||
|
if self.holding.wait(0.1):
|
||||||
|
self.holding.clear()
|
||||||
|
while not (
|
||||||
|
self.stopping.wait(0) or
|
||||||
|
parent._inactive_event.wait(parent.hold_time)
|
||||||
|
):
|
||||||
|
if parent._held_from is None:
|
||||||
|
parent._held_from = time()
|
||||||
|
parent._fire_held()
|
||||||
|
if not parent.hold_repeat:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
class GPIOQueue(GPIOThread):
|
class GPIOQueue(GPIOThread):
|
||||||
@@ -293,9 +452,9 @@ class GPIOQueue(GPIOThread):
|
|||||||
assert callable(average)
|
assert callable(average)
|
||||||
super(GPIOQueue, self).__init__(target=self.fill)
|
super(GPIOQueue, self).__init__(target=self.fill)
|
||||||
if queue_len < 1:
|
if queue_len < 1:
|
||||||
raise GPIOBadQueueLen('queue_len must be at least one')
|
raise BadQueueLen('queue_len must be at least one')
|
||||||
if sample_wait < 0:
|
if sample_wait < 0:
|
||||||
raise GPIOBadSampleWait('sample_wait must be 0 or greater')
|
raise BadWaitTime('sample_wait must be 0 or greater')
|
||||||
self.queue = deque(maxlen=queue_len)
|
self.queue = deque(maxlen=queue_len)
|
||||||
self.partial = partial
|
self.partial = partial
|
||||||
self.sample_wait = sample_wait
|
self.sample_wait = sample_wait
|
||||||
|
|||||||
Reference in New Issue
Block a user