mirror of
				https://github.com/KevinMidboe/python-gpiozero.git
				synced 2025-10-29 17:50:37 +00:00 
			
		
		
		
	| @@ -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