Coverage for /usr/lib/python3/dist-packages/gpiozero/mixins.py: 43%
250 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 16:40 +0100
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-05 16:40 +0100
1# vim: set fileencoding=utf-8:
2#
3# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
4#
5# Copyright (c) 2016-2021 Dave Jones <dave@waveform.org.uk>
6# Copyright (c) 2018-2021 Ben Nuttall <ben@bennuttall.com>
7# Copyright (c) 2016 Andrew Scheller <github@loowis.durge.org>
8#
9# SPDX-License-Identifier: BSD-3-Clause
11from __future__ import (
12 unicode_literals,
13 print_function,
14 absolute_import,
15 division,
16 )
17nstr = str
18str = type('')
20import inspect
21import weakref
22from functools import wraps, partial
23from threading import Event
24from collections import deque
25try:
26 from statistics import median
27except ImportError:
28 from .compat import median
29import warnings
31from .threads import GPIOThread
32from .exc import (
33 BadEventHandler,
34 BadWaitTime,
35 BadQueueLen,
36 DeviceClosed,
37 CallbackSetToNone,
38 )
40callback_warning = (
41 'The callback was set to None. This may have been unintentional '
42 'e.g. btn.when_pressed = pressed() instead of btn.when_pressed = pressed'
43)
45class ValuesMixin(object):
46 """
47 Adds a :attr:`values` property to the class which returns an infinite
48 generator of readings from the :attr:`~Device.value` property. There is
49 rarely a need to use this mixin directly as all base classes in GPIO Zero
50 include it.
52 .. note::
54 Use this mixin *first* in the parent class list.
55 """
57 @property
58 def values(self):
59 """
60 An infinite iterator of values read from :attr:`value`.
61 """
62 while True:
63 try:
64 yield self.value
65 except DeviceClosed:
66 break
69class SourceMixin(object):
70 """
71 Adds a :attr:`source` property to the class which, given an iterable or a
72 :class:`ValuesMixin` descendent, sets :attr:`~Device.value` to each member
73 of that iterable until it is exhausted. This mixin is generally included in
74 novel output devices to allow their state to be driven from another device.
76 .. note::
78 Use this mixin *first* in the parent class list.
79 """
81 def __init__(self, *args, **kwargs):
82 self._source = None
83 self._source_thread = None
84 self._source_delay = 0.01
85 super(SourceMixin, self).__init__(*args, **kwargs)
87 def close(self):
88 self.source = None
89 super(SourceMixin, self).close()
91 def _copy_values(self, source):
92 for v in source:
93 self.value = v
94 if self._source_thread.stopping.wait(self._source_delay):
95 break
97 @property
98 def source_delay(self):
99 """
100 The delay (measured in seconds) in the loop used to read values from
101 :attr:`source`. Defaults to 0.01 seconds which is generally sufficient
102 to keep CPU usage to a minimum while providing adequate responsiveness.
103 """
104 return self._source_delay
106 @source_delay.setter
107 def source_delay(self, value):
108 if value < 0:
109 raise BadWaitTime('source_delay must be 0 or greater')
110 self._source_delay = float(value)
112 @property
113 def source(self):
114 """
115 The iterable to use as a source of values for :attr:`value`.
116 """
117 return self._source
119 @source.setter
120 def source(self, value):
121 if getattr(self, '_source_thread', None):
122 self._source_thread.stop()
123 self._source_thread = None
124 if isinstance(value, ValuesMixin):
125 value = value.values
126 self._source = value
127 if value is not None:
128 self._source_thread = GPIOThread(self._copy_values, (value,))
129 self._source_thread.start()
132class SharedMixin(object):
133 """
134 This mixin marks a class as "shared". In this case, the meta-class
135 (GPIOMeta) will use :meth:`_shared_key` to convert the constructor
136 arguments to an immutable key, and will check whether any existing
137 instances match that key. If they do, they will be returned by the
138 constructor instead of a new instance. An internal reference counter is
139 used to determine how many times an instance has been "constructed" in this
140 way.
142 When :meth:`~Device.close` is called, an internal reference counter will be
143 decremented and the instance will only close when it reaches zero.
144 """
145 _instances = {}
147 def __del__(self):
148 self._refs = 0
149 super(SharedMixin, self).__del__()
151 @classmethod
152 def _shared_key(cls, *args, **kwargs):
153 """
154 This is called with the constructor arguments to generate a unique
155 key (which must be storable in a :class:`dict` and, thus, immutable
156 and hashable) representing the instance that can be shared. This must
157 be overridden by descendents.
159 The default simply assumes all positional arguments are immutable and
160 returns this as the key but this is almost never the "right" thing to
161 do and almost all descendents should override this method.
162 """
163 # XXX Future 2.x version should change this to raise NotImplementedError
164 return args
167class event(object):
168 """
169 A descriptor representing a callable event on a class descending from
170 :class:`EventsMixin`.
172 Instances of this class are very similar to a :class:`property` but also
173 deal with notifying the owning class when events are assigned (or
174 unassigned) and wrapping callbacks implicitly as appropriate.
175 """
176 def __init__(self, doc=None):
177 self.handlers = {}
178 self.__doc__ = doc
180 def __get__(self, instance, owner=None):
181 if instance is None: 181 ↛ 184line 181 didn't jump to line 184, because the condition on line 181 was never false
182 return self
183 else:
184 return self.handlers.get(id(instance))
186 def __set__(self, instance, value):
187 if value is None: 187 ↛ 188line 187 didn't jump to line 188, because the condition on line 187 was never true
188 try:
189 del self.handlers[id(instance)]
190 except KeyError:
191 warnings.warn(CallbackSetToNone(callback_warning))
192 else:
193 self.handlers[id(instance)] = instance._wrap_callback(value)
194 enabled = any( 194 ↛ exitline 194 didn't jump to the function exit
195 obj.handlers.get(id(instance))
196 for name in dir(type(instance))
197 for obj in (getattr(type(instance), name),)
198 if isinstance(obj, event)
199 )
200 instance._start_stop_events(enabled)
203class EventsMixin(object):
204 """
205 Adds edge-detected :meth:`when_activated` and :meth:`when_deactivated`
206 events to a device based on changes to the :attr:`~Device.is_active`
207 property common to all devices. Also adds :meth:`wait_for_active` and
208 :meth:`wait_for_inactive` methods for level-waiting.
210 .. note::
212 Note that this mixin provides no means of actually firing its events;
213 call :meth:`_fire_events` in sub-classes when device state changes to
214 trigger the events. This should also be called once at the end of
215 initialization to set initial states.
216 """
217 def __init__(self, *args, **kwargs):
218 super(EventsMixin, self).__init__(*args, **kwargs)
219 self._active_event = Event()
220 self._inactive_event = Event()
221 self._last_active = None
222 self._last_changed = self.pin_factory.ticks()
224 def _all_events(self):
225 """
226 Generator function which yields all :class:`event` instances defined
227 against this class.
228 """
229 for name in dir(type(self)):
230 obj = getattr(type(self), name)
231 if isinstance(obj, event):
232 yield obj
234 def close(self):
235 for ev in self._all_events():
236 try:
237 del ev.handlers[id(self)]
238 except KeyError:
239 pass
240 super(EventsMixin, self).close()
242 def wait_for_active(self, timeout=None):
243 """
244 Pause the script until the device is activated, or the timeout is
245 reached.
247 :type timeout: float or None
248 :param timeout:
249 Number of seconds to wait before proceeding. If this is
250 :data:`None` (the default), then wait indefinitely until the device
251 is active.
252 """
253 return self._active_event.wait(timeout)
255 def wait_for_inactive(self, timeout=None):
256 """
257 Pause the script until the device is deactivated, or the timeout is
258 reached.
260 :type timeout: float or None
261 :param timeout:
262 Number of seconds to wait before proceeding. If this is
263 :data:`None` (the default), then wait indefinitely until the device
264 is inactive.
265 """
266 return self._inactive_event.wait(timeout)
268 when_activated = event(
269 """
270 The function to run when the device changes state from inactive to
271 active.
273 This can be set to a function which accepts no (mandatory) parameters,
274 or a Python function which accepts a single mandatory parameter (with
275 as many optional parameters as you like). If the function accepts a
276 single mandatory parameter, the device that activated it will be passed
277 as that parameter.
279 Set this property to :data:`None` (the default) to disable the event.
280 """)
282 when_deactivated = event(
283 """
284 The function to run when the device changes state from active to
285 inactive.
287 This can be set to a function which accepts no (mandatory) parameters,
288 or a Python function which accepts a single mandatory parameter (with
289 as many optional parameters as you like). If the function accepts a
290 single mandatory parameter, the device that deactivated it will be
291 passed as that parameter.
293 Set this property to :data:`None` (the default) to disable the event.
294 """)
296 @property
297 def active_time(self):
298 """
299 The length of time (in seconds) that the device has been active for.
300 When the device is inactive, this is :data:`None`.
301 """
302 if self._active_event.is_set():
303 return self.pin_factory.ticks_diff(self.pin_factory.ticks(),
304 self._last_changed)
305 else:
306 return None
308 @property
309 def inactive_time(self):
310 """
311 The length of time (in seconds) that the device has been inactive for.
312 When the device is active, this is :data:`None`.
313 """
314 if self._inactive_event.is_set():
315 return self.pin_factory.ticks_diff(self.pin_factory.ticks(),
316 self._last_changed)
317 else:
318 return None
320 def _wrap_callback(self, fn):
321 # XXX In 2.x, move this to the event class above
322 if not callable(fn): 322 ↛ 323line 322 didn't jump to line 323, because the condition on line 322 was never true
323 raise BadEventHandler('value must be None or a callable')
324 # If fn is wrapped with partial (i.e. partial, partialmethod, or wraps
325 # has been used to produce it) we need to dig out the "real" function
326 # that's been wrapped along with all the mandatory positional args
327 # used in the wrapper so we can test the binding
328 args = ()
329 wrapped_fn = fn
330 while isinstance(wrapped_fn, partial): 330 ↛ 331line 330 didn't jump to line 331, because the condition on line 330 was never true
331 args = wrapped_fn.args + args
332 wrapped_fn = wrapped_fn.func
333 if inspect.isbuiltin(wrapped_fn): 333 ↛ 339line 333 didn't jump to line 339, because the condition on line 333 was never true
334 # We can't introspect the prototype of builtins. In this case we
335 # assume that the builtin has no (mandatory) parameters; this is
336 # the most reasonable assumption on the basis that pre-existing
337 # builtins have no knowledge of gpiozero, and the sole parameter
338 # we would pass is a gpiozero object
339 return fn
340 else:
341 # Try binding ourselves to the argspec of the provided callable.
342 # If this works, assume the function is capable of accepting no
343 # parameters
344 try:
345 inspect.getcallargs(wrapped_fn, *args)
346 return fn
347 except TypeError:
348 try:
349 # If the above fails, try binding with a single parameter
350 # (ourselves). If this works, wrap the specified callback
351 inspect.getcallargs(wrapped_fn, *(args + (self,)))
352 @wraps(fn)
353 def wrapper():
354 return fn(self)
355 return wrapper
356 except TypeError:
357 raise BadEventHandler(
358 'value must be a callable which accepts up to one '
359 'mandatory parameter')
361 def _fire_activated(self):
362 # These methods are largely here to be overridden by descendents
363 if self.when_activated:
364 self.when_activated()
366 def _fire_deactivated(self):
367 # These methods are largely here to be overridden by descendents
368 if self.when_deactivated:
369 self.when_deactivated()
371 def _fire_events(self, ticks, new_active):
372 """
373 This method should be called by descendents whenever the
374 :attr:`~Device.is_active` property is likely to have changed (for
375 example, in response to a pin's :attr:`~gpiozero.Pin.state` changing).
377 The *ticks* parameter must be set to the time when the change occurred;
378 this can usually be obtained from the pin factory's
379 :meth:`gpiozero.Factory.ticks` method but some pin implementations will
380 implicitly provide the ticks when an event occurs as part of their
381 reporting mechanism.
383 The *new_active* parameter must be set to the device's
384 :attr:`~Device.is_active` value at the time indicated by *ticks* (which
385 is not necessarily the value of :attr:`~Device.is_active` right now, if
386 the pin factory provides means of reporting a pin's historical state).
387 """
388 old_active, self._last_active = self._last_active, new_active
389 if old_active is None: 389 ↛ 396line 389 didn't jump to line 396, because the condition on line 389 was never false
390 # Initial "indeterminate" state; set events but don't fire
391 # callbacks as there's not necessarily an edge
392 if new_active: 392 ↛ 393line 392 didn't jump to line 393, because the condition on line 392 was never true
393 self._active_event.set()
394 else:
395 self._inactive_event.set()
396 elif old_active != new_active:
397 self._last_changed = ticks
398 if new_active:
399 self._inactive_event.clear()
400 self._active_event.set()
401 self._fire_activated()
402 else:
403 self._active_event.clear()
404 self._inactive_event.set()
405 self._fire_deactivated()
407 def _start_stop_events(self, enabled):
408 """
409 This is a stub method that only exists to be overridden by descendents.
410 It is called when :class:`event` properties are assigned (including
411 when set to :data:`None) to permit the owning instance to activate or
412 deactivate monitoring facilities.
414 For example, if a descendent requires a background thread to monitor a
415 device, it would be preferable to only run the thread if event handlers
416 are present to respond to it.
418 The *enabled* parameter is :data:`False` when all :class:`event`
419 properties on the owning class are :data:`None`, and :data:`True`
420 otherwise.
421 """
422 pass
425class HoldMixin(EventsMixin):
426 """
427 Extends :class:`EventsMixin` to add the :attr:`when_held` event and the
428 machinery to fire that event repeatedly (when :attr:`hold_repeat` is
429 :data:`True`) at internals defined by :attr:`hold_time`.
430 """
431 def __init__(self, *args, **kwargs):
432 self._hold_thread = None
433 super(HoldMixin, self).__init__(*args, **kwargs)
434 self._when_held = None
435 self._held_from = None
436 self._hold_time = 1
437 self._hold_repeat = False
438 self._hold_thread = HoldThread(self)
440 def close(self):
441 if self._hold_thread is not None:
442 self._hold_thread.stop()
443 self._hold_thread = None
444 super(HoldMixin, self).close()
446 def _fire_activated(self):
447 super(HoldMixin, self)._fire_activated()
448 self._hold_thread.holding.set()
450 def _fire_deactivated(self):
451 self._held_from = None
452 super(HoldMixin, self)._fire_deactivated()
454 def _fire_held(self):
455 if self.when_held:
456 self.when_held()
458 when_held = event(
459 """
460 The function to run when the device has remained active for
461 :attr:`hold_time` seconds.
463 This can be set to a function which accepts no (mandatory) parameters,
464 or a Python function which accepts a single mandatory parameter (with
465 as many optional parameters as you like). If the function accepts a
466 single mandatory parameter, the device that activated will be passed
467 as that parameter.
469 Set this property to :data:`None` (the default) to disable the event.
470 """)
472 @property
473 def hold_time(self):
474 """
475 The length of time (in seconds) to wait after the device is activated,
476 until executing the :attr:`when_held` handler. If :attr:`hold_repeat`
477 is True, this is also the length of time between invocations of
478 :attr:`when_held`.
479 """
480 return self._hold_time
482 @hold_time.setter
483 def hold_time(self, value):
484 if value < 0: 484 ↛ 485line 484 didn't jump to line 485, because the condition on line 484 was never true
485 raise BadWaitTime('hold_time must be 0 or greater')
486 self._hold_time = float(value)
488 @property
489 def hold_repeat(self):
490 """
491 If :data:`True`, :attr:`when_held` will be executed repeatedly with
492 :attr:`hold_time` seconds between each invocation.
493 """
494 return self._hold_repeat
496 @hold_repeat.setter
497 def hold_repeat(self, value):
498 self._hold_repeat = bool(value)
500 @property
501 def is_held(self):
502 """
503 When :data:`True`, the device has been active for at least
504 :attr:`hold_time` seconds.
505 """
506 return self._held_from is not None
508 @property
509 def held_time(self):
510 """
511 The length of time (in seconds) that the device has been held for.
512 This is counted from the first execution of the :attr:`when_held` event
513 rather than when the device activated, in contrast to
514 :attr:`~EventsMixin.active_time`. If the device is not currently held,
515 this is :data:`None`.
516 """
517 if self._held_from is not None:
518 return self.pin_factory.ticks_diff(self.pin_factory.ticks(),
519 self._held_from)
520 else:
521 return None
524class HoldThread(GPIOThread):
525 """
526 Extends :class:`GPIOThread`. Provides a background thread that repeatedly
527 fires the :attr:`HoldMixin.when_held` event as long as the owning
528 device is active.
529 """
530 def __init__(self, parent):
531 super(HoldThread, self).__init__(
532 target=self.held, args=(weakref.proxy(parent),))
533 self.holding = Event()
534 self.start()
536 def held(self, parent):
537 try:
538 while not self.stopping.is_set():
539 if self.holding.wait(0.1): 539 ↛ 540line 539 didn't jump to line 540, because the condition on line 539 was never true
540 self.holding.clear()
541 while not (
542 self.stopping.is_set() or
543 parent._inactive_event.wait(parent.hold_time)
544 ):
545 if parent._held_from is None:
546 parent._held_from = parent.pin_factory.ticks()
547 parent._fire_held()
548 if not parent.hold_repeat:
549 break
550 except ReferenceError:
551 # Parent is dead; time to die!
552 pass
555class GPIOQueue(GPIOThread):
556 """
557 Extends :class:`GPIOThread`. Provides a background thread that monitors a
558 device's values and provides a running *average* (defaults to median) of
559 those values. If the *parent* device includes the :class:`EventsMixin` in
560 its ancestry, the thread automatically calls
561 :meth:`~EventsMixin._fire_events`.
562 """
563 def __init__(
564 self, parent, queue_len=5, sample_wait=0.0, partial=False,
565 average=median, ignore=None):
566 assert callable(average)
567 if queue_len < 1:
568 raise BadQueueLen('queue_len must be at least one')
569 if sample_wait < 0:
570 raise BadWaitTime('sample_wait must be 0 or greater')
571 if ignore is None:
572 ignore = set()
573 super(GPIOQueue, self).__init__(target=self.fill)
574 self.queue = deque(maxlen=queue_len)
575 self.partial = bool(partial)
576 self.sample_wait = float(sample_wait)
577 self.full = Event()
578 self.parent = weakref.proxy(parent)
579 self.average = average
580 self.ignore = ignore
582 @property
583 def value(self):
584 if not self.partial:
585 self.full.wait()
586 try:
587 return self.average(self.queue)
588 except (ZeroDivisionError, ValueError):
589 # No data == inactive value
590 return 0.0
592 def fill(self):
593 try:
594 while not self.stopping.wait(self.sample_wait):
595 value = self.parent._read()
596 if value not in self.ignore:
597 self.queue.append(value)
598 if not self.full.is_set() and len(self.queue) >= self.queue.maxlen:
599 self.full.set()
600 if (self.partial or self.full.is_set()) and isinstance(self.parent, EventsMixin):
601 self.parent._fire_events(self.parent.pin_factory.ticks(), self.parent.is_active)
602 except ReferenceError:
603 # Parent is dead; time to die!
604 pass