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" | ||||
| install: "pip install -e .[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: | ||||
|     slack: raspberrypifoundation:YoIHtVdg8Hd6gcA09QEmCYXN | ||||
|   | ||||
| @@ -43,6 +43,10 @@ Errors | ||||
|  | ||||
| .. autoexception:: BadEventHandler | ||||
|  | ||||
| .. autoexception:: BadQueueLen | ||||
|  | ||||
| .. autoexception:: BadWaitTime | ||||
|  | ||||
| .. autoexception:: CompositeDeviceError | ||||
|  | ||||
| .. autoexception:: CompositeDeviceBadName | ||||
| @@ -63,10 +67,6 @@ Errors | ||||
|  | ||||
| .. autoexception:: GPIOPinMissing | ||||
|  | ||||
| .. autoexception:: GPIOBadQueueLen | ||||
|  | ||||
| .. autoexception:: GPIOBadSampleWait | ||||
|  | ||||
| .. autoexception:: InputDeviceError | ||||
|  | ||||
| .. autoexception:: OutputDeviceError | ||||
|   | ||||
| @@ -37,6 +37,9 @@ There are also several `mixin classes`_: | ||||
| * :class:`EventsMixin` which adds activated/deactivated events to devices | ||||
|   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 | ||||
|  | ||||
| The current class hierarchies are displayed below. For brevity, the mixin | ||||
| @@ -141,3 +144,6 @@ Mixin Classes | ||||
| .. autoclass:: EventsMixin(...) | ||||
|     :members: | ||||
|  | ||||
| .. autoclass:: HoldMixin(...) | ||||
|     :members: | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
										
											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, | ||||
|     DeviceClosed, | ||||
|     BadEventHandler, | ||||
|     BadWaitTime, | ||||
|     BadQueueLen, | ||||
|     CompositeDeviceError, | ||||
|     CompositeDeviceBadName, | ||||
|     CompositeDeviceBadOrder, | ||||
| @@ -28,8 +30,6 @@ from .exc import ( | ||||
|     GPIODeviceClosed, | ||||
|     GPIOPinInUse, | ||||
|     GPIOPinMissing, | ||||
|     GPIOBadQueueLen, | ||||
|     GPIOBadSampleWait, | ||||
|     InputDeviceError, | ||||
|     OutputDeviceError, | ||||
|     OutputDeviceBadValue, | ||||
| @@ -61,6 +61,7 @@ from .mixins import ( | ||||
|     SourceMixin, | ||||
|     ValuesMixin, | ||||
|     EventsMixin, | ||||
|     HoldMixin, | ||||
| ) | ||||
| from .input_devices import ( | ||||
|     InputDevice, | ||||
|   | ||||
| @@ -16,6 +16,12 @@ class DeviceClosed(GPIOZeroError): | ||||
| class BadEventHandler(GPIOZeroError, ValueError): | ||||
|     "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): | ||||
|     "Base class for errors specific to the CompositeDevice hierarchy" | ||||
|  | ||||
| @@ -49,15 +55,6 @@ class GPIOPinInUse(GPIODeviceError): | ||||
| class GPIOPinMissing(GPIODeviceError, ValueError): | ||||
|     "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): | ||||
|     "Base class for errors specific to the InputDevice hierarchy" | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,7 @@ from threading import Event | ||||
|  | ||||
| from .exc import InputDeviceError, DeviceClosed | ||||
| from .devices import GPIODevice | ||||
| from .mixins import GPIOQueue, EventsMixin | ||||
| from .mixins import GPIOQueue, EventsMixin, HoldMixin | ||||
|  | ||||
|  | ||||
| class InputDevice(GPIODevice): | ||||
| @@ -222,7 +222,7 @@ class SmoothedInputDevice(EventsMixin, InputDevice): | ||||
|         return self.value > self.threshold | ||||
|  | ||||
|  | ||||
| class Button(DigitalInputDevice): | ||||
| class Button(HoldMixin, DigitalInputDevice): | ||||
|     """ | ||||
|     Extends :class:`DigitalInputDevice` and represents a simple push button | ||||
|     or switch. | ||||
| @@ -254,11 +254,24 @@ class Button(DigitalInputDevice): | ||||
|         If ``None`` (the default), no software bounce compensation will be | ||||
|         performed. Otherwise, this is the length in time (in seconds) that the | ||||
|         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) | ||||
|         self.hold_time = hold_time | ||||
|         self.hold_repeat = hold_repeat | ||||
|  | ||||
| Button.is_pressed = Button.is_active | ||||
| Button.pressed_time = Button.active_time | ||||
| Button.when_pressed = Button.when_activated | ||||
| Button.when_released = Button.when_deactivated | ||||
| Button.wait_for_press = Button.wait_for_active | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import weakref | ||||
| from functools import wraps | ||||
| from threading import Event | ||||
| from collections import deque | ||||
| from time import time | ||||
| try: | ||||
|     from statistics import median | ||||
| except ImportError: | ||||
| @@ -20,10 +21,9 @@ except ImportError: | ||||
| from .threads import GPIOThread | ||||
| from .exc import ( | ||||
|     BadEventHandler, | ||||
|     BadWaitTime, | ||||
|     BadQueueLen, | ||||
|     DeviceClosed, | ||||
|     GPIOBadSourceDelay, | ||||
|     GPIOBadQueueLen, | ||||
|     GPIOBadSampleWait, | ||||
|     ) | ||||
|  | ||||
| class ValuesMixin(object): | ||||
| @@ -89,7 +89,7 @@ class SourceMixin(object): | ||||
|     @source_delay.setter | ||||
|     def source_delay(self, value): | ||||
|         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) | ||||
|  | ||||
|     @property | ||||
| @@ -160,6 +160,7 @@ class EventsMixin(object): | ||||
|         self._when_activated = None | ||||
|         self._when_deactivated = None | ||||
|         self._last_state = None | ||||
|         self._last_changed = time() | ||||
|  | ||||
|     def wait_for_active(self, timeout=None): | ||||
|         """ | ||||
| @@ -223,6 +224,28 @@ class EventsMixin(object): | ||||
|     def when_deactivated(self, 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): | ||||
|         if fn is None: | ||||
|             return None | ||||
| @@ -256,6 +279,16 @@ class EventsMixin(object): | ||||
|                         '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 | ||||
| @@ -266,17 +299,143 @@ class EventsMixin(object): | ||||
|                 self._active_event.set() | ||||
|             else: | ||||
|                 self._inactive_event.set() | ||||
|         else: | ||||
|             if not old_state and new_state: | ||||
|         elif old_state != new_state: | ||||
|             self._last_changed = time() | ||||
|             if 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._fire_activated() | ||||
|             else: | ||||
|                 self._active_event.clear() | ||||
|                 self._inactive_event.set() | ||||
|                 if self.when_deactivated: | ||||
|                     self.when_deactivated() | ||||
|                 self._fire_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): | ||||
| @@ -293,9 +452,9 @@ class GPIOQueue(GPIOThread): | ||||
|         assert callable(average) | ||||
|         super(GPIOQueue, self).__init__(target=self.fill) | ||||
|         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: | ||||
|             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.partial = partial | ||||
|         self.sample_wait = sample_wait | ||||
|   | ||||
		Reference in New Issue
	
	Block a user