Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Adds when_held event hook to Button (via extension of the EventsMixin
class)
  • Loading branch information
waveform80 committed Apr 7, 2016
1 parent f746ecb commit 9e2cde4
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 8 deletions.
2 changes: 1 addition & 1 deletion docs/api_pins.rst
Expand Up @@ -72,7 +72,7 @@ to utilize pins that are part of IO extender chips. For example::

.. 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``
for another. This is unsupported, and if it results in your script
crashing, your components failing, or your Raspberry Pi turning into an
Expand Down
1 change: 1 addition & 0 deletions gpiozero/__init__.py
Expand Up @@ -61,6 +61,7 @@
SourceMixin,
ValuesMixin,
EventsMixin,
RepeatingMixin,
)
from .input_devices import (
InputDevice,
Expand Down
10 changes: 7 additions & 3 deletions gpiozero/input_devices.py
Expand Up @@ -12,7 +12,7 @@

from .exc import InputDeviceError, DeviceClosed
from .devices import GPIODevice
from .mixins import GPIOQueue, EventsMixin
from .mixins import GPIOQueue, EventsMixin, RepeatingMixin


class InputDevice(GPIODevice):
Expand Down Expand Up @@ -222,7 +222,7 @@ def is_active(self):
return self.value > self.threshold


class Button(DigitalInputDevice):
class Button(RepeatingMixin, DigitalInputDevice):
"""
Extends :class:`DigitalInputDevice` and represents a simple push button
or switch.
Expand Down Expand Up @@ -255,8 +255,12 @@ class Button(DigitalInputDevice):
performed. Otherwise, this is the length in time (in seconds) that the
component will ignore changes in state after an initial change.
"""
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)
self.hold_time = hold_time
self.hold_repeat = hold_repeat

Button.is_pressed = Button.is_active
Button.when_pressed = Button.when_activated
Expand Down
124 changes: 120 additions & 4 deletions gpiozero/mixins.py
Expand Up @@ -256,6 +256,16 @@ def wrapper():
'value must be a callable which accepts up to one '
'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):
old_state = self._last_state
new_state = self._last_state = self.is_active
Expand All @@ -270,13 +280,119 @@ def _fire_events(self):
if not old_state and new_state:
self._inactive_event.clear()
self._active_event.set()
if self.when_activated:
self.when_activated()
self._fire_activated()
elif old_state and not new_state:
self._active_event.clear()
self._inactive_event.set()
if self.when_deactivated:
self.when_deactivated()
self._fire_deactivated()


class RepeatingMixin(EventsMixin):
def __init__(self, *args, **kwargs):
super(RepeatingMixin, self).__init__(*args, **kwargs)
self._when_held = None
self._is_held = False
self._hold_time = 1
self._hold_repeat = False
self._hold_thread = RepeatingThread(self)

def close(self):
if self._hold_thread:
self._hold_thread.stop()
self._hold_thread = None
try:
super(RepeatingMixin, self).close()
except AttributeError:
pass

def _fire_activated(self):
super(RepeatingMixin, self)._fire_activated()
self._hold_thread.holding.set()

def _fire_deactivated(self):
self._hold_thread.holding.clear()
super(RepeatingMixin, 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):
self._hold_time = 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._is_held


class RepeatingThread(GPIOThread):
"""
Extends :class:`GPIOThread`. Provides a background thread that repeatedly
fires the :attr:`RepeatingEventsMixin.when_held` event as long as the
owning device is active.
"""
def __init__(self, parent):
super(RepeatingThread, 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):
while not (
self.stopping.wait(0) or
parent._inactive_event.wait(parent.hold_time)
):
parent._is_held = True
parent._fire_held()
parent._is_held = False


class GPIOQueue(GPIOThread):
Expand Down

0 comments on commit 9e2cde4

Please sign in to comment.