The source/values toolkit

Me and my big mouth. No sooner do I declare the base classes "relatively
stable" than I go and mess around with it all again. Anyway, this is the
long promised set of utilities to make source/values more interesting.
It includes a few interesting little utility functions, a whole bunch of
examples and introduces the notion of "pseudo" devices with no (obvious)
hardware representation like a time-of-day device.

This necessitated making the event system a little more generic (it's
not exclusive the GPIO devices after all; no reason we can't use it on
composite devices in future) and by this point the mixins have gotten
large enough to justify their own module.

The pseudo-devices are a bit spartan and basic at the moment but I'm
sure there'll be plenty of future ideas...
This commit is contained in:
Dave Jones
2016-04-04 01:34:53 +01:00
parent 365e309af6
commit 69dd8a439a
23 changed files with 1091 additions and 423 deletions

View File

@@ -41,6 +41,8 @@ Errors
.. autoexception:: DeviceClosed
.. autoexception:: BadEventHandler
.. autoexception:: CompositeDeviceError
.. autoexception:: CompositeDeviceBadName

View File

@@ -16,6 +16,9 @@ classes:
* :class:`SPIDevice` represents devices that communicate over an SPI interface
(implemented as four GPIO pins)
* :class:`InternalDevice` represents devices that are entirely internal to
the Pi (usually operating system related services)
* :class:`CompositeDevice` represents devices composed of multiple other
devices like HATs
@@ -31,6 +34,9 @@ There are also several `mixin classes`_:
* :class:`SharedMixin` which causes classes to track their construction and
return existing instances when equivalent constructor arguments are passed
* :class:`EventsMixin` which adds activated/deactivated events to devices
along with the machinery to trigger those events
.. _mixin classes: https://en.wikipedia.org/wiki/Mixin
The current class hierarchies are displayed below. For brevity, the mixin
@@ -47,6 +53,10 @@ Next, the classes below :class:`SPIDevice`:
.. image:: images/spi_device_hierarchy.*
Next, the classes below :class:`InternalDevice`:
.. image:: images/other_device_hierarchy.*
Next, the classes below :class:`CompositeDevice`:
.. image:: images/composite_device_hierarchy.*
@@ -60,15 +70,18 @@ Base Classes
============
.. autoclass:: Device
:members: close, closed
:members: close, closed, value, is_active
.. autoclass:: GPIODevice(pin)
:members:
.. autoclass:: CompositeDevice
.. autoclass:: SPIDevice
:members:
.. autoclass:: SPIDevice
.. autoclass:: InternalDevice
:members:
.. autoclass:: CompositeDevice
:members:
Input Devices
@@ -77,9 +90,6 @@ Input Devices
.. autoclass:: InputDevice(pin, pull_up=False)
:members:
.. autoclass:: WaitableInputDevice
:members:
.. autoclass:: DigitalInputDevice(pin, pull_up=False, bounce_time=None)
:members:
@@ -128,3 +138,6 @@ Mixin Classes
.. autoclass:: SharedMixin(...)
:members: _shared_key
.. autoclass:: EventsMixin(...)
:members:

14
docs/api_other.rst Normal file
View File

@@ -0,0 +1,14 @@
================
Internal Devices
================
.. currentmodule:: gpiozero
GPIO Zero also provides several "internal" devices which represent facilities
provided by the operating system itself. These can be used to react to things
like the time of day, or whether a server is available on the network.
.. autoclass:: TimeOfDay
.. autoclass:: PingServer

49
docs/api_source_tools.rst Normal file
View File

@@ -0,0 +1,49 @@
============
Source Tools
============
.. currentmodule:: gpiozero
GPIO Zero includes several utility routines which are intended to be used with
the :attr:`~SourceMixin.source` and :attr:`~ValuesMixin.values` attributes
common to most devices in the library. Given that ``source`` and ``values``
deal with infinite iterators, another excellent source of utilities is the
:mod:`itertools` module in the standard library.
Single source conversions
=========================
.. autofunction:: negated
.. autofunction:: inverted
.. autofunction:: scaled
.. autofunction:: clamped
.. autofunction:: post_delayed
.. autofunction:: pre_delayed
.. autofunction:: quantized
.. autofunction:: queued
Combining sources
=================
.. autofunction:: conjunction
.. autofunction:: disjunction
.. autofunction:: averaged
Artifical sources
=================
.. autofunction:: random_values
.. autofunction:: sin_values
.. autofunction:: cos_values

View File

@@ -9,7 +9,6 @@ digraph classes {
node [color="#9ec6e0", fontcolor="#000000"]
Device;
GPIODevice;
WaitableInputDevice;
SmoothedInputDevice;
/* Concrete classes */
@@ -17,9 +16,8 @@ digraph classes {
GPIODevice->Device;
InputDevice->GPIODevice;
WaitableInputDevice->InputDevice;
DigitalInputDevice->WaitableInputDevice;
SmoothedInputDevice->WaitableInputDevice;
DigitalInputDevice->InputDevice;
SmoothedInputDevice->InputDevice;
Button->DigitalInputDevice;
MotionSensor->SmoothedInputDevice;
LightSensor->SmoothedInputDevice;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -4,175 +4,165 @@
<!-- Generated by graphviz version 2.36.0 (20140111.2315)
-->
<!-- Title: classes Pages: 1 -->
<svg width="701pt" height="404pt"
viewBox="0.00 0.00 701.00 404.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 400)">
<svg width="721pt" height="332pt"
viewBox="0.00 0.00 721.00 332.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 328)">
<title>classes</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-400 697,-400 697,4 -4,4"/>
<polygon fill="white" stroke="none" points="-4,4 -4,-328 717,-328 717,4 -4,4"/>
<!-- Device -->
<g id="node1" class="node"><title>Device</title>
<polygon fill="#9ec6e0" stroke="#9ec6e0" points="485,-396 431,-396 431,-360 485,-360 485,-396"/>
<text text-anchor="middle" x="458" y="-375.5" font-family="Sans" font-size="10.00" fill="#000000">Device</text>
<polygon fill="#9ec6e0" stroke="#9ec6e0" points="499,-324 445,-324 445,-288 499,-288 499,-324"/>
<text text-anchor="middle" x="472" y="-303.5" font-family="Sans" font-size="10.00" fill="#000000">Device</text>
</g>
<!-- GPIODevice -->
<g id="node2" class="node"><title>GPIODevice</title>
<polygon fill="#9ec6e0" stroke="#9ec6e0" points="494,-324 422,-324 422,-288 494,-288 494,-324"/>
<text text-anchor="middle" x="458" y="-303.5" font-family="Sans" font-size="10.00" fill="#000000">GPIODevice</text>
<polygon fill="#9ec6e0" stroke="#9ec6e0" points="508,-252 436,-252 436,-216 508,-216 508,-252"/>
<text text-anchor="middle" x="472" y="-231.5" font-family="Sans" font-size="10.00" fill="#000000">GPIODevice</text>
</g>
<!-- GPIODevice&#45;&gt;Device -->
<g id="edge1" class="edge"><title>GPIODevice&#45;&gt;Device</title>
<path fill="none" stroke="black" d="M458,-324.303C458,-332.017 458,-341.288 458,-349.888"/>
<polygon fill="black" stroke="black" points="454.5,-349.896 458,-359.896 461.5,-349.896 454.5,-349.896"/>
</g>
<!-- WaitableInputDevice -->
<g id="node3" class="node"><title>WaitableInputDevice</title>
<polygon fill="#9ec6e0" stroke="#9ec6e0" points="440.5,-180 327.5,-180 327.5,-144 440.5,-144 440.5,-180"/>
<text text-anchor="middle" x="384" y="-159.5" font-family="Sans" font-size="10.00" fill="#000000">WaitableInputDevice</text>
</g>
<!-- InputDevice -->
<g id="node5" class="node"><title>InputDevice</title>
<polygon fill="#2980b9" stroke="#2980b9" points="438.5,-252 365.5,-252 365.5,-216 438.5,-216 438.5,-252"/>
<text text-anchor="middle" x="402" y="-231.5" font-family="Sans" font-size="10.00" fill="#ffffff">InputDevice</text>
</g>
<!-- WaitableInputDevice&#45;&gt;InputDevice -->
<g id="edge3" class="edge"><title>WaitableInputDevice&#45;&gt;InputDevice</title>
<path fill="none" stroke="black" d="M388.449,-180.303C390.455,-188.102 392.869,-197.491 395.101,-206.171"/>
<polygon fill="black" stroke="black" points="391.722,-207.082 397.602,-215.896 398.501,-205.339 391.722,-207.082"/>
<path fill="none" stroke="black" d="M472,-252.303C472,-260.017 472,-269.288 472,-277.888"/>
<polygon fill="black" stroke="black" points="468.5,-277.896 472,-287.896 475.5,-277.896 468.5,-277.896"/>
</g>
<!-- SmoothedInputDevice -->
<g id="node4" class="node"><title>SmoothedInputDevice</title>
<g id="node3" class="node"><title>SmoothedInputDevice</title>
<polygon fill="#9ec6e0" stroke="#9ec6e0" points="289.5,-108 166.5,-108 166.5,-72 289.5,-72 289.5,-108"/>
<text text-anchor="middle" x="228" y="-87.5" font-family="Sans" font-size="10.00" fill="#000000">SmoothedInputDevice</text>
</g>
<!-- SmoothedInputDevice&#45;&gt;WaitableInputDevice -->
<g id="edge5" class="edge"><title>SmoothedInputDevice&#45;&gt;WaitableInputDevice</title>
<path fill="none" stroke="black" d="M266.162,-108.124C287.393,-117.651 314.008,-129.593 336.569,-139.717"/>
<polygon fill="black" stroke="black" points="335.406,-143.031 345.962,-143.932 338.271,-136.644 335.406,-143.031"/>
<!-- InputDevice -->
<g id="node4" class="node"><title>InputDevice</title>
<polygon fill="#2980b9" stroke="#2980b9" points="445.5,-180 372.5,-180 372.5,-144 445.5,-144 445.5,-180"/>
<text text-anchor="middle" x="409" y="-159.5" font-family="Sans" font-size="10.00" fill="#ffffff">InputDevice</text>
</g>
<!-- SmoothedInputDevice&#45;&gt;InputDevice -->
<g id="edge4" class="edge"><title>SmoothedInputDevice&#45;&gt;InputDevice</title>
<path fill="none" stroke="black" d="M272.278,-108.124C299.802,-118.769 335.123,-132.429 362.97,-143.198"/>
<polygon fill="black" stroke="black" points="361.893,-146.535 372.483,-146.877 364.418,-140.006 361.893,-146.535"/>
</g>
<!-- InputDevice&#45;&gt;GPIODevice -->
<g id="edge2" class="edge"><title>InputDevice&#45;&gt;GPIODevice</title>
<path fill="none" stroke="black" d="M415.843,-252.303C422.489,-260.611 430.578,-270.723 437.887,-279.859"/>
<polygon fill="black" stroke="black" points="435.336,-282.273 444.317,-287.896 440.803,-277.901 435.336,-282.273"/>
<path fill="none" stroke="black" d="M424.573,-180.303C432.202,-188.78 441.523,-199.136 449.876,-208.417"/>
<polygon fill="black" stroke="black" points="447.315,-210.804 456.606,-215.896 452.518,-206.121 447.315,-210.804"/>
</g>
<!-- DigitalInputDevice -->
<g id="node6" class="node"><title>DigitalInputDevice</title>
<polygon fill="#2980b9" stroke="#2980b9" points="439.5,-108 336.5,-108 336.5,-72 439.5,-72 439.5,-108"/>
<text text-anchor="middle" x="388" y="-87.5" font-family="Sans" font-size="10.00" fill="#ffffff">DigitalInputDevice</text>
<g id="node5" class="node"><title>DigitalInputDevice</title>
<polygon fill="#2980b9" stroke="#2980b9" points="460.5,-108 357.5,-108 357.5,-72 460.5,-72 460.5,-108"/>
<text text-anchor="middle" x="409" y="-87.5" font-family="Sans" font-size="10.00" fill="#ffffff">DigitalInputDevice</text>
</g>
<!-- DigitalInputDevice&#45;&gt;WaitableInputDevice -->
<g id="edge4" class="edge"><title>DigitalInputDevice&#45;&gt;WaitableInputDevice</title>
<path fill="none" stroke="black" d="M387.011,-108.303C386.57,-116.017 386.041,-125.288 385.549,-133.888"/>
<polygon fill="black" stroke="black" points="382.054,-133.712 384.977,-143.896 389.042,-134.112 382.054,-133.712"/>
<!-- DigitalInputDevice&#45;&gt;InputDevice -->
<g id="edge3" class="edge"><title>DigitalInputDevice&#45;&gt;InputDevice</title>
<path fill="none" stroke="black" d="M409,-108.303C409,-116.017 409,-125.288 409,-133.888"/>
<polygon fill="black" stroke="black" points="405.5,-133.896 409,-143.896 412.5,-133.896 405.5,-133.896"/>
</g>
<!-- Button -->
<g id="node7" class="node"><title>Button</title>
<g id="node6" class="node"><title>Button</title>
<polygon fill="#2980b9" stroke="#2980b9" points="445,-36 391,-36 391,-0 445,-0 445,-36"/>
<text text-anchor="middle" x="418" y="-15.5" font-family="Sans" font-size="10.00" fill="#ffffff">Button</text>
</g>
<!-- Button&#45;&gt;DigitalInputDevice -->
<g id="edge6" class="edge"><title>Button&#45;&gt;DigitalInputDevice</title>
<path fill="none" stroke="black" d="M410.584,-36.3034C407.206,-44.1868 403.13,-53.6958 399.377,-62.4536"/>
<polygon fill="black" stroke="black" points="396.053,-61.3255 395.33,-71.8957 402.487,-64.0829 396.053,-61.3255"/>
<g id="edge5" class="edge"><title>Button&#45;&gt;DigitalInputDevice</title>
<path fill="none" stroke="black" d="M415.775,-36.3034C414.783,-44.0173 413.592,-53.2875 412.486,-61.8876"/>
<polygon fill="black" stroke="black" points="409.003,-61.531 411.199,-71.8957 415.946,-62.4237 409.003,-61.531"/>
</g>
<!-- MotionSensor -->
<g id="node8" class="node"><title>MotionSensor</title>
<g id="node7" class="node"><title>MotionSensor</title>
<polygon fill="#2980b9" stroke="#2980b9" points="82.5,-36 -0.5,-36 -0.5,-0 82.5,-0 82.5,-36"/>
<text text-anchor="middle" x="41" y="-15.5" font-family="Sans" font-size="10.00" fill="#ffffff">MotionSensor</text>
</g>
<!-- MotionSensor&#45;&gt;SmoothedInputDevice -->
<g id="edge7" class="edge"><title>MotionSensor&#45;&gt;SmoothedInputDevice</title>
<g id="edge6" class="edge"><title>MotionSensor&#45;&gt;SmoothedInputDevice</title>
<path fill="none" stroke="black" d="M82.5014,-34.5353C109.086,-44.4869 143.895,-57.5168 172.8,-68.3368"/>
<polygon fill="black" stroke="black" points="171.763,-71.6859 182.356,-71.9139 174.217,-65.1302 171.763,-71.6859"/>
</g>
<!-- LightSensor -->
<g id="node9" class="node"><title>LightSensor</title>
<g id="node8" class="node"><title>LightSensor</title>
<polygon fill="#2980b9" stroke="#2980b9" points="175,-36 101,-36 101,-0 175,-0 175,-36"/>
<text text-anchor="middle" x="138" y="-15.5" font-family="Sans" font-size="10.00" fill="#ffffff">LightSensor</text>
</g>
<!-- LightSensor&#45;&gt;SmoothedInputDevice -->
<g id="edge8" class="edge"><title>LightSensor&#45;&gt;SmoothedInputDevice</title>
<g id="edge7" class="edge"><title>LightSensor&#45;&gt;SmoothedInputDevice</title>
<path fill="none" stroke="black" d="M160.247,-36.3034C171.582,-45.1193 185.53,-55.9679 197.819,-65.5258"/>
<polygon fill="black" stroke="black" points="195.966,-68.519 206.009,-71.8957 200.264,-62.9935 195.966,-68.519"/>
</g>
<!-- LineSensor -->
<g id="node10" class="node"><title>LineSensor</title>
<g id="node9" class="node"><title>LineSensor</title>
<polygon fill="#2980b9" stroke="#2980b9" points="263,-36 193,-36 193,-0 263,-0 263,-36"/>
<text text-anchor="middle" x="228" y="-15.5" font-family="Sans" font-size="10.00" fill="#ffffff">LineSensor</text>
</g>
<!-- LineSensor&#45;&gt;SmoothedInputDevice -->
<g id="edge9" class="edge"><title>LineSensor&#45;&gt;SmoothedInputDevice</title>
<g id="edge8" class="edge"><title>LineSensor&#45;&gt;SmoothedInputDevice</title>
<path fill="none" stroke="black" d="M228,-36.3034C228,-44.0173 228,-53.2875 228,-61.8876"/>
<polygon fill="black" stroke="black" points="224.5,-61.8956 228,-71.8957 231.5,-61.8957 224.5,-61.8956"/>
</g>
<!-- DistanceSensor -->
<g id="node11" class="node"><title>DistanceSensor</title>
<g id="node10" class="node"><title>DistanceSensor</title>
<polygon fill="#2980b9" stroke="#2980b9" points="373,-36 281,-36 281,-0 373,-0 373,-36"/>
<text text-anchor="middle" x="327" y="-15.5" font-family="Sans" font-size="10.00" fill="#ffffff">DistanceSensor</text>
</g>
<!-- DistanceSensor&#45;&gt;SmoothedInputDevice -->
<g id="edge10" class="edge"><title>DistanceSensor&#45;&gt;SmoothedInputDevice</title>
<g id="edge9" class="edge"><title>DistanceSensor&#45;&gt;SmoothedInputDevice</title>
<path fill="none" stroke="black" d="M302.782,-36.1239C290.077,-45.1069 274.336,-56.2375 260.577,-65.9659"/>
<polygon fill="black" stroke="black" points="258.456,-63.1791 252.311,-71.8102 262.497,-68.8947 258.456,-63.1791"/>
</g>
<!-- OutputDevice -->
<g id="node12" class="node"><title>OutputDevice</title>
<polygon fill="#2980b9" stroke="#2980b9" points="556,-252 474,-252 474,-216 556,-216 556,-252"/>
<text text-anchor="middle" x="515" y="-231.5" font-family="Sans" font-size="10.00" fill="#ffffff">OutputDevice</text>
<g id="node11" class="node"><title>OutputDevice</title>
<polygon fill="#2980b9" stroke="#2980b9" points="576,-180 494,-180 494,-144 576,-144 576,-180"/>
<text text-anchor="middle" x="535" y="-159.5" font-family="Sans" font-size="10.00" fill="#ffffff">OutputDevice</text>
</g>
<!-- OutputDevice&#45;&gt;GPIODevice -->
<g id="edge11" class="edge"><title>OutputDevice&#45;&gt;GPIODevice</title>
<path fill="none" stroke="black" d="M500.91,-252.303C494.077,-260.695 485.743,-270.93 478.244,-280.139"/>
<polygon fill="black" stroke="black" points="475.528,-277.931 471.928,-287.896 480.956,-282.351 475.528,-277.931"/>
<g id="edge10" class="edge"><title>OutputDevice&#45;&gt;GPIODevice</title>
<path fill="none" stroke="black" d="M519.427,-180.303C511.798,-188.78 502.477,-199.136 494.124,-208.417"/>
<polygon fill="black" stroke="black" points="491.482,-206.121 487.394,-215.896 496.685,-210.804 491.482,-206.121"/>
</g>
<!-- DigitalOutputDevice -->
<g id="node13" class="node"><title>DigitalOutputDevice</title>
<polygon fill="#2980b9" stroke="#2980b9" points="571,-180 459,-180 459,-144 571,-144 571,-180"/>
<text text-anchor="middle" x="515" y="-159.5" font-family="Sans" font-size="10.00" fill="#ffffff">DigitalOutputDevice</text>
<g id="node12" class="node"><title>DigitalOutputDevice</title>
<polygon fill="#2980b9" stroke="#2980b9" points="591,-108 479,-108 479,-72 591,-72 591,-108"/>
<text text-anchor="middle" x="535" y="-87.5" font-family="Sans" font-size="10.00" fill="#ffffff">DigitalOutputDevice</text>
</g>
<!-- DigitalOutputDevice&#45;&gt;OutputDevice -->
<g id="edge12" class="edge"><title>DigitalOutputDevice&#45;&gt;OutputDevice</title>
<path fill="none" stroke="black" d="M515,-180.303C515,-188.017 515,-197.288 515,-205.888"/>
<polygon fill="black" stroke="black" points="511.5,-205.896 515,-215.896 518.5,-205.896 511.5,-205.896"/>
<g id="edge11" class="edge"><title>DigitalOutputDevice&#45;&gt;OutputDevice</title>
<path fill="none" stroke="black" d="M535,-108.303C535,-116.017 535,-125.288 535,-133.888"/>
<polygon fill="black" stroke="black" points="531.5,-133.896 535,-143.896 538.5,-133.896 531.5,-133.896"/>
</g>
<!-- LED -->
<g id="node14" class="node"><title>LED</title>
<polygon fill="#2980b9" stroke="#2980b9" points="519,-108 465,-108 465,-72 519,-72 519,-108"/>
<text text-anchor="middle" x="492" y="-87.5" font-family="Sans" font-size="10.00" fill="#ffffff">LED</text>
<g id="node13" class="node"><title>LED</title>
<polygon fill="#2980b9" stroke="#2980b9" points="534,-36 480,-36 480,-0 534,-0 534,-36"/>
<text text-anchor="middle" x="507" y="-15.5" font-family="Sans" font-size="10.00" fill="#ffffff">LED</text>
</g>
<!-- LED&#45;&gt;DigitalOutputDevice -->
<g id="edge13" class="edge"><title>LED&#45;&gt;DigitalOutputDevice</title>
<path fill="none" stroke="black" d="M497.685,-108.303C500.248,-116.102 503.333,-125.491 506.185,-134.171"/>
<polygon fill="black" stroke="black" points="502.933,-135.488 509.38,-143.896 509.584,-133.303 502.933,-135.488"/>
<g id="edge12" class="edge"><title>LED&#45;&gt;DigitalOutputDevice</title>
<path fill="none" stroke="black" d="M513.921,-36.3034C517.075,-44.1868 520.878,-53.6958 524.381,-62.4536"/>
<polygon fill="black" stroke="black" points="521.195,-63.9108 528.158,-71.8957 527.694,-61.311 521.195,-63.9108"/>
</g>
<!-- Buzzer -->
<g id="node15" class="node"><title>Buzzer</title>
<polygon fill="#2980b9" stroke="#2980b9" points="591,-108 537,-108 537,-72 591,-72 591,-108"/>
<text text-anchor="middle" x="564" y="-87.5" font-family="Sans" font-size="10.00" fill="#ffffff">Buzzer</text>
<g id="node14" class="node"><title>Buzzer</title>
<polygon fill="#2980b9" stroke="#2980b9" points="606,-36 552,-36 552,-0 606,-0 606,-36"/>
<text text-anchor="middle" x="579" y="-15.5" font-family="Sans" font-size="10.00" fill="#ffffff">Buzzer</text>
</g>
<!-- Buzzer&#45;&gt;DigitalOutputDevice -->
<g id="edge14" class="edge"><title>Buzzer&#45;&gt;DigitalOutputDevice</title>
<path fill="none" stroke="black" d="M551.888,-108.303C546.132,-116.526 539.138,-126.517 532.794,-135.579"/>
<polygon fill="black" stroke="black" points="529.84,-133.696 526.973,-143.896 535.575,-137.71 529.84,-133.696"/>
<g id="edge13" class="edge"><title>Buzzer&#45;&gt;DigitalOutputDevice</title>
<path fill="none" stroke="black" d="M568.124,-36.3034C563.008,-44.4411 556.805,-54.311 551.155,-63.2987"/>
<polygon fill="black" stroke="black" points="548.11,-61.5667 545.751,-71.8957 554.036,-65.2919 548.11,-61.5667"/>
</g>
<!-- PWMOutputDevice -->
<g id="node16" class="node"><title>PWMOutputDevice</title>
<polygon fill="#2980b9" stroke="#2980b9" points="693,-180 589,-180 589,-144 693,-144 693,-180"/>
<text text-anchor="middle" x="641" y="-159.5" font-family="Sans" font-size="10.00" fill="#ffffff">PWMOutputDevice</text>
<g id="node15" class="node"><title>PWMOutputDevice</title>
<polygon fill="#2980b9" stroke="#2980b9" points="713,-108 609,-108 609,-72 713,-72 713,-108"/>
<text text-anchor="middle" x="661" y="-87.5" font-family="Sans" font-size="10.00" fill="#ffffff">PWMOutputDevice</text>
</g>
<!-- PWMOutputDevice&#45;&gt;OutputDevice -->
<g id="edge15" class="edge"><title>PWMOutputDevice&#45;&gt;OutputDevice</title>
<path fill="none" stroke="black" d="M610.177,-180.124C593.55,-189.361 572.835,-200.869 554.981,-210.788"/>
<polygon fill="black" stroke="black" points="552.983,-207.894 545.942,-215.81 556.383,-214.013 552.983,-207.894"/>
<g id="edge14" class="edge"><title>PWMOutputDevice&#45;&gt;OutputDevice</title>
<path fill="none" stroke="black" d="M630.177,-108.124C613.55,-117.361 592.835,-128.869 574.981,-138.788"/>
<polygon fill="black" stroke="black" points="572.983,-135.894 565.942,-143.81 576.383,-142.013 572.983,-135.894"/>
</g>
<!-- PWMLED -->
<g id="node17" class="node"><title>PWMLED</title>
<polygon fill="#2980b9" stroke="#2980b9" points="670,-108 612,-108 612,-72 670,-72 670,-108"/>
<text text-anchor="middle" x="641" y="-87.5" font-family="Sans" font-size="10.00" fill="#ffffff">PWMLED</text>
<g id="node16" class="node"><title>PWMLED</title>
<polygon fill="#2980b9" stroke="#2980b9" points="690,-36 632,-36 632,-0 690,-0 690,-36"/>
<text text-anchor="middle" x="661" y="-15.5" font-family="Sans" font-size="10.00" fill="#ffffff">PWMLED</text>
</g>
<!-- PWMLED&#45;&gt;PWMOutputDevice -->
<g id="edge16" class="edge"><title>PWMLED&#45;&gt;PWMOutputDevice</title>
<path fill="none" stroke="black" d="M641,-108.303C641,-116.017 641,-125.288 641,-133.888"/>
<polygon fill="black" stroke="black" points="637.5,-133.896 641,-143.896 644.5,-133.896 637.5,-133.896"/>
<g id="edge15" class="edge"><title>PWMLED&#45;&gt;PWMOutputDevice</title>
<path fill="none" stroke="black" d="M661,-36.3034C661,-44.0173 661,-53.2875 661,-61.8876"/>
<polygon fill="black" stroke="black" points="657.5,-61.8956 661,-71.8957 664.5,-61.8957 657.5,-61.8956"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,19 @@
/* vim: set et sw=4 sts=4: */
digraph classes {
graph [rankdir=BT];
node [shape=rect, style=filled, fontname=Sans, fontsize=10];
edge [];
/* Abstract classes */
node [color="#9ec6e0", fontcolor="#000000"]
Device;
InternalDevice;
/* Concrete classes */
node [color="#2980b9", fontcolor="#ffffff"];
InternalDevice->Device;
TimeOfDay->InternalDevice;
PingServer->InternalDevice;
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.36.0 (20140111.2315)
-->
<!-- Title: classes Pages: 1 -->
<svg width="163pt" height="188pt"
viewBox="0.00 0.00 163.00 188.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 184)">
<title>classes</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-184 159,-184 159,4 -4,4"/>
<!-- Device -->
<g id="node1" class="node"><title>Device</title>
<polygon fill="#9ec6e0" stroke="#9ec6e0" points="104,-180 50,-180 50,-144 104,-144 104,-180"/>
<text text-anchor="middle" x="77" y="-159.5" font-family="Sans" font-size="10.00" fill="#000000">Device</text>
</g>
<!-- InternalDevice -->
<g id="node2" class="node"><title>InternalDevice</title>
<polygon fill="#9ec6e0" stroke="#9ec6e0" points="119.5,-108 34.5,-108 34.5,-72 119.5,-72 119.5,-108"/>
<text text-anchor="middle" x="77" y="-87.5" font-family="Sans" font-size="10.00" fill="#000000">InternalDevice</text>
</g>
<!-- InternalDevice&#45;&gt;Device -->
<g id="edge1" class="edge"><title>InternalDevice&#45;&gt;Device</title>
<path fill="none" stroke="black" d="M77,-108.303C77,-116.017 77,-125.288 77,-133.888"/>
<polygon fill="black" stroke="black" points="73.5001,-133.896 77,-143.896 80.5001,-133.896 73.5001,-133.896"/>
</g>
<!-- TimeOfDay -->
<g id="node3" class="node"><title>TimeOfDay</title>
<polygon fill="#2980b9" stroke="#2980b9" points="68.5,-36 -0.5,-36 -0.5,-0 68.5,-0 68.5,-36"/>
<text text-anchor="middle" x="34" y="-15.5" font-family="Sans" font-size="10.00" fill="#ffffff">TimeOfDay</text>
</g>
<!-- TimeOfDay&#45;&gt;InternalDevice -->
<g id="edge2" class="edge"><title>TimeOfDay&#45;&gt;InternalDevice</title>
<path fill="none" stroke="black" d="M44.6292,-36.3034C49.6281,-44.4411 55.691,-54.311 61.2121,-63.2987"/>
<polygon fill="black" stroke="black" points="58.2766,-65.2069 66.4931,-71.8957 64.2411,-61.5429 58.2766,-65.2069"/>
</g>
<!-- PingServer -->
<g id="node4" class="node"><title>PingServer</title>
<polygon fill="#2980b9" stroke="#2980b9" points="155,-36 87,-36 87,-0 155,-0 155,-36"/>
<text text-anchor="middle" x="121" y="-15.5" font-family="Sans" font-size="10.00" fill="#ffffff">PingServer</text>
</g>
<!-- PingServer&#45;&gt;InternalDevice -->
<g id="edge3" class="edge"><title>PingServer&#45;&gt;InternalDevice</title>
<path fill="none" stroke="black" d="M110.124,-36.3034C105.008,-44.4411 98.8045,-54.311 93.1551,-63.2987"/>
<polygon fill="black" stroke="black" points="90.1098,-61.5667 87.7513,-71.8957 96.0363,-65.2919 90.1098,-61.5667"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -12,7 +12,9 @@ Table of Contents
api_output
api_spi
api_boards
api_other
api_generic
api_source_tools
api_pins
api_exc
changelog

View File

@@ -11,6 +11,7 @@ from .pins import (
from .exc import (
GPIOZeroError,
DeviceClosed,
BadEventHandler,
CompositeDeviceError,
CompositeDeviceBadName,
SPIError,
@@ -45,13 +46,15 @@ from .devices import (
Device,
GPIODevice,
CompositeDevice,
)
from .mixins import (
SharedMixin,
SourceMixin,
ValuesMixin,
EventsMixin,
)
from .input_devices import (
InputDevice,
WaitableInputDevice,
DigitalInputDevice,
SmoothedInputDevice,
Button,
@@ -103,3 +106,24 @@ from .boards import (
CamJamKitRobot,
Energenie,
)
from .other_devices import (
InternalDevice,
PingServer,
TimeOfDay,
)
from .source_tools import (
averaged,
clamped,
conjunction,
cos_values,
disjunction,
inverted,
negated,
post_delayed,
pre_delayed,
quantized,
queued,
random_values,
scaled,
sin_values,
)

View File

@@ -22,7 +22,8 @@ from .exc import (
from .input_devices import Button
from .output_devices import OutputDevice, LED, PWMLED, Buzzer, Motor
from .threads import GPIOThread
from .devices import Device, CompositeDevice, SharedMixin, SourceMixin
from .devices import Device, CompositeDevice
from .mixins import SharedMixin, SourceMixin
class CompositeOutputDevice(SourceMixin, CompositeDevice):

View File

@@ -15,6 +15,10 @@ from types import FunctionType
from threading import RLock
from .threads import GPIOThread, _threads_shutdown
from .mixins import (
ValuesMixin,
SharedMixin,
)
from .exc import (
DeviceClosed,
GPIOPinMissing,
@@ -201,127 +205,35 @@ class GPIOBase(GPIOMeta(nstr('GPIOBase'), (), {})):
self.close()
class ValuesMixin(object):
"""
Adds a :attr:`values` property to the class which returns an infinite
generator of readings from the :attr:`value` property.
.. note::
Use this mixin *first* in the parent class list.
"""
@property
def values(self):
"""
An infinite iterator of values read from `value`.
"""
while True:
try:
yield self.value
except GPIODeviceClosed:
break
class SourceMixin(object):
"""
Adds a :attr:`source` property to the class which, given an iterable,
sets :attr:`value` to each member of that iterable until it is exhausted.
.. note::
Use this mixin *first* in the parent class list.
"""
def __init__(self, *args, **kwargs):
self._source = None
self._source_thread = None
self._source_delay = 0.01
super(SourceMixin, self).__init__(*args, **kwargs)
def close(self):
try:
super(SourceMixin, self).close()
except AttributeError:
pass
self.source = None
def _copy_values(self, source):
for v in source:
self.value = v
if self._source_thread.stopping.wait(self._source_delay):
break
@property
def source_delay(self):
"""
The delay (measured in seconds) in the loop used to read values from
:attr:`source`. Defaults to 0.01 seconds which is generally sufficient
to keep CPU usage to a minimum while providing adequate responsiveness.
"""
return self._source_delay
@source_delay.setter
def source_delay(self, value):
if value < 0:
raise GPIOBadSourceDelay('source_delay must be 0 or greater')
self._source_delay = float(value)
@property
def source(self):
"""
The iterable to use as a source of values for :attr:`value`.
"""
return self._source
@source.setter
def source(self, value):
if self._source_thread is not None:
self._source_thread.stop()
self._source_thread = None
self._source = value
if value is not None:
self._source_thread = GPIOThread(target=self._copy_values, args=(value,))
self._source_thread.start()
class SharedMixin(object):
"""
This mixin marks a class as "shared". In this case, the meta-class
(GPIOMeta) will use :meth:`_shared_key` to convert the constructor
arguments to an immutable key, and will check whether any existing
instances match that key. If they do, they will be returned by the
constructor instead of a new instance. An internal reference counter is
used to determine how many times an instance has been "constructed" in this
way.
When :meth:`close` is called, an internal reference counter will be
decremented and the instance will only close when it reaches zero.
"""
_INSTANCES = {}
def __del__(self):
self._refs = 0
super(SharedMixin, self).__del__()
@classmethod
def _shared_key(cls, *args, **kwargs):
"""
Given the constructor arguments, returns an immutable key representing
the instance. The default simply assumes all positional arguments are
immutable.
"""
return args
class Device(ValuesMixin, GPIOBase):
"""
Represents a single device of any type; GPIO-based, SPI-based, I2C-based,
etc. This is the base class of the device hierarchy.
etc. This is the base class of the device hierarchy. It defines the
basic services applicable to all devices (specifically thhe :attr:`is_active`
property, the :attr:`value` property, and the :meth:`close` method).
"""
def __repr__(self):
return "<gpiozero.%s object>" % (self.__class__.__name__)
@property
def value(self):
"""
Returns a value representing the device's state. Frequently, this is a
boolean value, or a number between 0 and 1 but some devices use larger
ranges (e.g. -1 to +1) and composite devices usually use tuples to
return the states of all their subordinate components.
"""
return 0
@property
def is_active(self):
"""
Returns ``True`` if the device is currently active and ``False``
otherwise. This property is usually derived from :attr:`value`. Unlike
:attr:`value`, this is *always* a boolean.
"""
return bool(self.value)
class CompositeDevice(Device):
"""
@@ -417,15 +329,16 @@ class CompositeDevice(Device):
def value(self):
return self.tuple(*(device.value for device in self))
@property
def is_active(self):
return any(self.value)
class GPIODevice(Device):
"""
Extends :class:`Device`. Represents a generic GPIO device.
This is the class at the root of the gpiozero class hierarchy. It handles
ensuring that two GPIO devices do not share the same pin, and provides
basic services applicable to all devices (specifically the :attr:`pin`
property, :attr:`is_active` property, and the :attr:`close` method).
Extends :class:`Device`. Represents a generic GPIO device and provides
the services common to all single-pin GPIO devices (like ensuring two
GPIO devices do no share a :attr:`pin`).
:param int pin:
The GPIO pin (in BCM numbering) that the device is connected to. If
@@ -494,14 +407,8 @@ class GPIODevice(Device):
@property
def value(self):
"""
Returns ``True`` if the device is currently active and ``False``
otherwise.
"""
return self._read()
is_active = value
def __repr__(self):
try:
return "<gpiozero.%s object on pin %r, is_active=%s>" % (

View File

@@ -13,6 +13,9 @@ class GPIOZeroError(Exception):
class DeviceClosed(GPIOZeroError):
"Error raised when an operation is attempted on a closed device"
class BadEventHandler(GPIOZeroError, ValueError):
"Error raised when an event handler with an incompatible prototype is specified"
class CompositeDeviceError(GPIOZeroError):
"Base class for errors specific to the CompositeDevice hierarchy"

View File

@@ -7,15 +7,12 @@ from __future__ import (
division,
)
import inspect
import warnings
from functools import wraps
from time import sleep, time
from threading import Event
from .exc import InputDeviceError, GPIODeviceError, GPIODeviceClosed
from .devices import GPIODevice, CompositeDevice
from .threads import GPIOQueue
from .mixins import GPIOQueue, EventsMixin
class InputDevice(GPIODevice):
@@ -65,148 +62,7 @@ class InputDevice(GPIODevice):
return super(InputDevice, self).__repr__()
class WaitableInputDevice(InputDevice):
"""
Represents a generic input device with distinct waitable states.
This class extends :class:`InputDevice` with methods for waiting on the
device's status (:meth:`wait_for_active` and :meth:`wait_for_inactive`),
and properties that hold functions to be called when the device changes
state (:meth:`when_activated` and :meth:`when_deactivated`). These are
aliased appropriately in various subclasses.
.. note::
Note that this class provides no means of actually firing its events;
it's effectively an abstract base class.
"""
def __init__(self, pin=None, pull_up=False):
super(WaitableInputDevice, self).__init__(pin, pull_up)
self._active_event = Event()
self._inactive_event = Event()
self._when_activated = None
self._when_deactivated = None
self._last_state = None
def wait_for_active(self, timeout=None):
"""
Pause the script until the device is activated, or the timeout is
reached.
:param float timeout:
Number of seconds to wait before proceeding. If this is ``None``
(the default), then wait indefinitely until the device is active.
"""
return self._active_event.wait(timeout)
def wait_for_inactive(self, timeout=None):
"""
Pause the script until the device is deactivated, or the timeout is
reached.
:param float timeout:
Number of seconds to wait before proceeding. If this is ``None``
(the default), then wait indefinitely until the device is inactive.
"""
return self._inactive_event.wait(timeout)
@property
def when_activated(self):
"""
The function to run when the device changes state from inactive to
active.
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_activated
@when_activated.setter
def when_activated(self, value):
self._when_activated = self._wrap_callback(value)
@property
def when_deactivated(self):
"""
The function to run when the device changes state from active to
inactive.
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 deactivated will be
passed as that parameter.
Set this property to ``None`` (the default) to disable the event.
"""
return self._when_deactivated
@when_deactivated.setter
def when_deactivated(self, value):
self._when_deactivated = self._wrap_callback(value)
def _wrap_callback(self, fn):
if fn is None:
return None
elif not callable(fn):
raise InputDeviceError('value must be None or a callable')
elif inspect.isbuiltin(fn):
# We can't introspect the prototype of builtins. In this case we
# assume that the builtin has no (mandatory) parameters; this is
# the most reasonable assumption on the basis that pre-existing
# builtins have no knowledge of gpiozero, and the sole parameter
# we would pass is a gpiozero object
return fn
else:
# Try binding ourselves to the argspec of the provided callable.
# If this works, assume the function is capable of accepting no
# parameters
try:
inspect.getcallargs(fn)
return fn
except TypeError:
try:
# If the above fails, try binding with a single parameter
# (ourselves). If this works, wrap the specified callback
inspect.getcallargs(fn, self)
@wraps(fn)
def wrapper():
return fn(self)
return wrapper
except TypeError:
raise InputDeviceError(
'value must be a callable which accepts up to one '
'mandatory parameter')
def _fire_events(self):
old_state = self._last_state
new_state = self._last_state = self.is_active
if old_state is None:
# Initial "indeterminate" state; set events but don't fire
# callbacks as there's not necessarily an edge
if new_state:
self._active_event.set()
else:
self._inactive_event.set()
else:
if not old_state and 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._active_event.clear()
self._inactive_event.set()
if self.when_deactivated:
self.when_deactivated()
class DigitalInputDevice(WaitableInputDevice):
class DigitalInputDevice(EventsMixin, InputDevice):
"""
Represents a generic input device with typical on/off behaviour.
@@ -233,7 +89,7 @@ class DigitalInputDevice(WaitableInputDevice):
raise
class SmoothedInputDevice(WaitableInputDevice):
class SmoothedInputDevice(EventsMixin, InputDevice):
"""
Represents a generic input device which takes its value from the mean of a
queue of historical values.

326
gpiozero/mixins.py Normal file
View File

@@ -0,0 +1,326 @@
from __future__ import (
unicode_literals,
print_function,
absolute_import,
division,
)
nstr = str
str = type('')
import inspect
import weakref
from functools import wraps
from threading import Event
from collections import deque
try:
from statistics import median, mean
except ImportError:
from .compat import median, mean
from .threads import GPIOThread
from .exc import BadEventHandler, DeviceClosed
class ValuesMixin(object):
"""
Adds a :attr:`values` property to the class which returns an infinite
generator of readings from the :attr:`value` property.
.. note::
Use this mixin *first* in the parent class list.
"""
@property
def values(self):
"""
An infinite iterator of values read from `value`.
"""
while True:
try:
yield self.value
except DeviceClosed:
break
class SourceMixin(object):
"""
Adds a :attr:`source` property to the class which, given an iterable,
sets :attr:`value` to each member of that iterable until it is exhausted.
.. note::
Use this mixin *first* in the parent class list.
"""
def __init__(self, *args, **kwargs):
self._source = None
self._source_thread = None
self._source_delay = 0.01
super(SourceMixin, self).__init__(*args, **kwargs)
def close(self):
try:
super(SourceMixin, self).close()
except AttributeError:
pass
self.source = None
def _copy_values(self, source):
for v in source:
self.value = v
if self._source_thread.stopping.wait(self._source_delay):
break
@property
def source_delay(self):
"""
The delay (measured in seconds) in the loop used to read values from
:attr:`source`. Defaults to 0.01 seconds which is generally sufficient
to keep CPU usage to a minimum while providing adequate responsiveness.
"""
return self._source_delay
@source_delay.setter
def source_delay(self, value):
if value < 0:
raise GPIOBadSourceDelay('source_delay must be 0 or greater')
self._source_delay = float(value)
@property
def source(self):
"""
The iterable to use as a source of values for :attr:`value`.
"""
return self._source
@source.setter
def source(self, value):
if self._source_thread is not None:
self._source_thread.stop()
self._source_thread = None
self._source = value
if value is not None:
self._source_thread = GPIOThread(target=self._copy_values, args=(value,))
self._source_thread.start()
class SharedMixin(object):
"""
This mixin marks a class as "shared". In this case, the meta-class
(GPIOMeta) will use :meth:`_shared_key` to convert the constructor
arguments to an immutable key, and will check whether any existing
instances match that key. If they do, they will be returned by the
constructor instead of a new instance. An internal reference counter is
used to determine how many times an instance has been "constructed" in this
way.
When :meth:`close` is called, an internal reference counter will be
decremented and the instance will only close when it reaches zero.
"""
_INSTANCES = {}
def __del__(self):
self._refs = 0
super(SharedMixin, self).__del__()
@classmethod
def _shared_key(cls, *args, **kwargs):
"""
Given the constructor arguments, returns an immutable key representing
the instance. The default simply assumes all positional arguments are
immutable.
"""
return args
class EventsMixin(object):
"""
Adds edge-detected :meth:`when_activated` and :meth:`when_deactivated`
events to a device based on changes to the :attr:`~Device.is_active`
property common to all devices. Also adds :meth:`wait_for_active` and
:meth:`wait_for_inactive` methods for level-waiting.
.. note::
Note that this mixin provides no means of actually firing its events;
call :meth:`_fire_events` in sub-classes when device state changes to
trigger the events. This should also be called once at the end of
initialization to set initial states.
"""
def __init__(self, *args, **kwargs):
super(EventsMixin, self).__init__(*args, **kwargs)
self._active_event = Event()
self._inactive_event = Event()
self._when_activated = None
self._when_deactivated = None
self._last_state = None
def wait_for_active(self, timeout=None):
"""
Pause the script until the device is activated, or the timeout is
reached.
:param float timeout:
Number of seconds to wait before proceeding. If this is ``None``
(the default), then wait indefinitely until the device is active.
"""
return self._active_event.wait(timeout)
def wait_for_inactive(self, timeout=None):
"""
Pause the script until the device is deactivated, or the timeout is
reached.
:param float timeout:
Number of seconds to wait before proceeding. If this is ``None``
(the default), then wait indefinitely until the device is inactive.
"""
return self._inactive_event.wait(timeout)
@property
def when_activated(self):
"""
The function to run when the device changes state from inactive to
active.
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_activated
@when_activated.setter
def when_activated(self, value):
self._when_activated = self._wrap_callback(value)
@property
def when_deactivated(self):
"""
The function to run when the device changes state from active to
inactive.
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 deactivated will be
passed as that parameter.
Set this property to ``None`` (the default) to disable the event.
"""
return self._when_deactivated
@when_deactivated.setter
def when_deactivated(self, value):
self._when_deactivated = self._wrap_callback(value)
def _wrap_callback(self, fn):
if fn is None:
return None
elif not callable(fn):
raise BadEventHandler('value must be None or a callable')
elif inspect.isbuiltin(fn):
# We can't introspect the prototype of builtins. In this case we
# assume that the builtin has no (mandatory) parameters; this is
# the most reasonable assumption on the basis that pre-existing
# builtins have no knowledge of gpiozero, and the sole parameter
# we would pass is a gpiozero object
return fn
else:
# Try binding ourselves to the argspec of the provided callable.
# If this works, assume the function is capable of accepting no
# parameters
try:
inspect.getcallargs(fn)
return fn
except TypeError:
try:
# If the above fails, try binding with a single parameter
# (ourselves). If this works, wrap the specified callback
inspect.getcallargs(fn, self)
@wraps(fn)
def wrapper():
return fn(self)
return wrapper
except TypeError:
raise BadEventHandler(
'value must be a callable which accepts up to one '
'mandatory parameter')
def _fire_events(self):
old_state = self._last_state
new_state = self._last_state = self.is_active
if old_state is None:
# Initial "indeterminate" state; set events but don't fire
# callbacks as there's not necessarily an edge
if new_state:
self._active_event.set()
else:
self._inactive_event.set()
else:
if not old_state and 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._active_event.clear()
self._inactive_event.set()
if self.when_deactivated:
self.when_deactivated()
class GPIOQueue(GPIOThread):
"""
Extends :class:`GPIOThread`. Provides a background thread that monitors a
device's values and provides a running *average* (defaults to median) of
those values. If the *parent* device includes the :class:`EventsMixin` in
its ancestry, the thread automatically calls
:meth:`~EventsMixin._fire_events`.
"""
def __init__(
self, parent, queue_len=5, sample_wait=0.0, partial=False,
average=median):
assert callable(average)
super(GPIOQueue, self).__init__(target=self.fill)
if queue_len < 1:
raise GPIOBadQueueLen('queue_len must be at least one')
if sample_wait < 0:
raise GPIOBadSampleWait('sample_wait must be 0 or greater')
self.queue = deque(maxlen=queue_len)
self.partial = partial
self.sample_wait = sample_wait
self.full = Event()
self.parent = weakref.proxy(parent)
self.average = average
@property
def value(self):
if not self.partial:
self.full.wait()
try:
return self.average(self.queue)
except ZeroDivisionError:
# No data == inactive value
return 0.0
def fill(self):
try:
while (not self.stopping.wait(self.sample_wait) and
len(self.queue) < self.queue.maxlen):
self.queue.append(self.parent._read())
if self.partial and isinstance(self.parent, EventsMixin):
self.parent._fire_events()
self.full.set()
while not self.stopping.wait(self.sample_wait):
self.queue.append(self.parent._read())
if isinstance(self.parent, EventsMixin):
self.parent._fire_events()
except ReferenceError:
# Parent is dead; time to die!
pass

168
gpiozero/other_devices.py Normal file
View File

@@ -0,0 +1,168 @@
# vim: set fileencoding=utf-8:
from __future__ import (
unicode_literals,
print_function,
absolute_import,
division,
)
str = type('')
import os
import io
import subprocess
from datetime import datetime, time
from .devices import Device
from .mixins import EventsMixin
class InternalDevice(EventsMixin, Device):
"""
Extends :class:`Device` to provide a basis for devices which have no
specific hardware representation. This are effectively pseudo-devices and
usually represent operating system services like the internal clock, file
systems or network facilities.
"""
class PingServer(InternalDevice):
"""
Extends :class:`InternalDevice` to provide a device which is active when a
*host* on the network can be pinged.
The following example lights an LED while a server is reachable (note the
use of :attr:`~SourceMixin.source_delay` to ensure the server is not
flooded with pings)::
from gpiozero import PingServer, LED
from signal import pause
server = PingServer('my-server')
led = LED(4)
led.source_delay = 1
led.source = server.values
pause()
:param str host:
The hostname or IP address to attempt to ping.
"""
def __init__(self, host):
self.host = host
super(PingServer, self).__init__()
self._fire_events()
def __repr__(self):
return '<gpiozero.PingDevice host="%s">' % self.host
@property
def value(self):
# XXX This is doing a DNS lookup every time it's queried; should we
# call gethostbyname in the constructor and ping that instead (good
# for consistency, but what if the user *expects* the host to change
# address?)
with io.open(os.devnull, 'wb') as devnull:
try:
subprocess.check_call(
['ping', '-c1', self.host],
stdout=devnull, stderr=devnull)
except subprocess.CalledProcessError:
return False
else:
return True
class TimeOfDay(InternalDevice):
"""
Extends :class:`InternalDevice` to provide a device which is active when
the computer's clock indicates that the current time is between
*start_time* and *end_time* (inclusive) which are :class:`~datetime.time`
instances.
The following example turns on a lamp attached to an :class:`Energenie`
plug between 7 and 8 AM::
from datetime import time
from gpiozero import TimeOfDay, Energenie
from signal import pause
lamp = Energenie(0)
morning = TimeOfDay(time(7), time(8))
morning.when_activated = lamp.on
morning.when_deactivated = lamp.off
pause()
:param ~datetime.time start_time:
The time from which the device will be considered active.
:param ~datetime.time end_time:
The time after which the device will be considered inactive.
:param bool utc:
If ``True`` (the default), a naive UTC time will be used for the
comparison rather than a local time-zone reading.
"""
def __init__(self, start_time, end_time, utc=True):
self._start_time = None
self._end_time = None
self._utc = True
super(TimeOfDay, self).__init__()
self.start_time = start_time
self.end_time = end_time
self.utc = utc
self._fire_events()
def __repr__(self):
return '<gpiozero.TimeOfDay active between %s and %s %s>' % (
self.start_time, self.end_time, ('local', 'UTC')[self.utc])
@property
def start_time(self):
"""
The time of day after which the device will be considered active.
"""
return self._start_time
@start_time.setter
def start_time(self, value):
if isinstance(value, datetime):
value = value.time()
if not isinstance(value, time):
raise ValueError('start_time must be a datetime, or time instance')
self._start_time = value
@property
def end_time(self):
"""
The time of day after which the device will be considered inactive.
"""
return self._end_time
@end_time.setter
def end_time(self, value):
if isinstance(value, datetime):
value = value.time()
if not isinstance(value, time):
raise ValueError('end_time must be a datetime, or time instance')
self._end_time = value
@property
def utc(self):
"""
If ``True``, use a naive UTC time reading for comparison instead of a
local timezone reading.
"""
return self._utc
@utc.setter
def utc(self, value):
self._utc = bool(value)
@property
def value(self):
if self.utc:
return self.start_time <= datetime.utcnow().time() <= self.end_time
else:
return self.start_time <= datetime.now().time() <= self.end_time

View File

@@ -11,7 +11,8 @@ from threading import Lock
from itertools import repeat, cycle, chain
from .exc import OutputDeviceBadValue, GPIOPinMissing
from .devices import GPIODevice, Device, CompositeDevice, SourceMixin
from .devices import GPIODevice, Device, CompositeDevice
from .mixins import SourceMixin
from .threads import GPIOThread

296
gpiozero/source_tools.py Normal file
View File

@@ -0,0 +1,296 @@
# vim: set fileencoding=utf-8:
from __future__ import (
unicode_literals,
print_function,
absolute_import,
division,
)
str = type('')
from random import random
from time import sleep
try:
from itertools import izip as zip
except ImportError:
pass
from itertools import count, cycle
from math import sin, cos, floor
try:
from statistics import mean
except ImportError:
from .compat import mean
def negated(values):
"""
Returns the negation of the supplied values (``True`` becomes ``False``,
and ``False`` becomes ``True``). For example::
from gpiozero import Button, LED, negated
from signal import pause
led = LED(4)
btn = Button(17)
led.source = negated(btn.values)
pause()
"""
for v in values:
yield not v
def inverted(values):
"""
Returns the inversion of the supplied values (1 becomes 0, 0 becomes 1,
0.1 becomes 0.9, etc.). For example::
from gpiozero import MCP3008, PWMLED, inverted
from signal import pause
led = PWMLED(4)
pot = MCP3008(channel=0)
led.source = inverted(pot.values)
pause()
"""
for v in values:
yield 1 - v
def scaled(values, range_min, range_max, domain_min=0, domain_max=1):
"""
Returns *values* scaled from *range_min* to *range_max*, assuming that all
items in *values* lie between *domain_min* and *domain_max* (which default
to 0 and 1 respectively). For example, to control the direction of a motor
(which is represented as a value between -1 and 1) using a potentiometer
(which typically provides values between 0 and 1)::
from gpiozero import Motor, MCP3008, scaled
from signal import pause
motor = Motor(20, 21)
pot = MCP3008(channel=0)
motor.source = scaled(pot.values, -1, 1)
pause()
"""
domain_size = domain_max - domain_min
range_size = range_max - range_min
for v in values:
yield (((v - domain_min) / domain_size) * range_size) + range_min
def clamped(values, range_min=0, range_max=1):
"""
Returns *values* clamped from *range_min* to *range_max*, i.e. any items
less than *range_min* will be returned as *range_min* and any items
larger than *range_max* will be returned as *range_max* (these default to
0 and 1 respectively). For example::
from gpiozero import PWMLED, MCP3008, clamped
from signal import pause
led = PWMLED(4)
pot = MCP3008(channel=0)
led.source = clamped(pot.values, 0.5, 1.0)
pause()
"""
for v in values:
yield min(max(v, range_min), range_max)
def quantized(values, steps, range_min=0, range_max=1):
"""
Returns *values* quantized to *steps* increments. All items in *values* are
assumed to be between *range_min* and *range_max* (use :func:`scaled` to
ensure this if necessary).
For example, to quantize values between 0 and 1 to 5 "steps" (0.0, 0.25,
0.5, 0.75, 1.0)::
from gpiozero import PWMLED, MCP3008, quantized
from signal import pause
led = PWMLED(4)
pot = MCP3008(channel=0)
led.source = quantized(pot.values, 4)
pause()
"""
range_size = range_max - range_min
for v in scaled(values, 0, 1, range_min, range_max):
yield ((int(v * steps) / steps) * range_size) + range_min
def conjunction(*values):
"""
Returns the `logical conjunction`_ of all supplied values (the result is
only ``True`` if and only if all input values are simultaneously ``True``).
One or more *values* can be specified. For example, to light an
:class:`LED` only when *both* buttons are pressed::
from gpiozero import LED, Button, conjunction
from signal import pause
led = LED(4)
btn1 = Button(20)
btn2 = Button(21)
led.source = conjunction(btn1.values, btn2.values)
pause()
.. _logical conjunction: https://en.wikipedia.org/wiki/Logical_conjunction
"""
for v in zip(*values):
yield all(v)
def disjunction(*values):
"""
Returns the `logical disjunction`_ of all supplied values (the result is
``True`` if any of the input values are currently ``True``). One or more
*values* can be specified. For example, the light an :class:`LED` when
*any* button is pressed::
from gpiozero import LED, Button, conjunction
from signal import pause
led = LED(4)
btn1 = Button(20)
btn2 = Button(21)
led.source = disjunction(btn1.values, btn2.values)
pause()
.. _logical disjunction: https://en.wikipedia.org/wiki/Logical_disjunction
"""
for v in zip(*values):
yield any(v)
def averaged(*values):
"""
Returns the mean of all supplied values. One or more *values* can be
specified. For example, to light a :class:`PWMLED` as the average of
several potentiometers connected to an :class:`MCP3008` ADC::
from gpiozero import MCP3008, PWMLED, averaged
from signal import pause
pot1 = MCP3008(channel=0)
pot2 = MCP3008(channel=1)
pot3 = MCP3008(channel=2)
led = PWMLED(4)
led.source = averaged(pot1.values, pot2.values, pot3.values)
pause()
"""
for v in zip(*values):
yield mean(v)
def queued(values, qsize):
"""
Queues up readings from *values* (the number of readings queued is
determined by *qsize*) and begins yielding values only when the queue is
full. For example, to "cascade" values along a sequence of LEDs::
from gpiozero import LEDBoard, Button, queued
from signal import pause
leds = LEDBoard(5, 6, 13, 19, 26)
btn = Button(17)
for i in range(4):
leds[i].source = queued(leds[i + 1].values, 5)
leds[i].source_delay = 0.01
leds[4].source = btn.values
pause()
"""
q = []
it = iter(values)
for i in range(qsize):
q.append(next(it))
for i in cycle(range(qsize)):
yield q[i]
try:
q[i] = next(it)
except StopIteration:
break
def pre_delayed(values, delay):
"""
Waits for *delay* seconds before returning each item from *values*.
"""
for v in values:
sleep(delay)
yield v
def post_delayed(values, delay):
"""
Waits for *delay* seconds after returning each item from *values*.
"""
for v in values:
yield v
sleep(delay)
def random_values():
"""
Provides an infinite source of random values between 0 and 1. For example,
to produce a "flickering candle" effect with an LED::
from gpiozero import PWMLED, random_values
from signal import pause
led = PWMLED(4)
led.source = random_values()
pause()
If you require a wider range than 0 to 1, see :func:`scaled`.
"""
while True:
yield random()
def sin_values():
"""
Provides an infinite source of values representing a sine wave (from -1 to
+1), calculated as the result of applying sign to a simple degrees counter
that increments by one for each requested value. For example, to produce a
"siren" effect with a couple of LEDs::
from gpiozero import PWMLED, sin_values, scaled, inverted
from signal import pause
red = PWMLED(2)
blue = PWMLED(3)
red.source_delay = 0.1
blue.source_delay = 0.1
red.source = scaled(sin_values(), 0, 1, -1, 1)
blue.source = inverted(red.values)
pause()
If you require a wider range than 0 to 1, see :func:`scaled`.
"""
for d in cycle(range(360)):
yield sin(d)
def cos_values():
"""
Provides an infinite source of values representing a cosine wave (from -1
to +1), calculated as the result of applying sign to a simple degrees
counter that increments by one for each requested value. For example, to
produce a "siren" effect with a couple of LEDs::
from gpiozero import PWMLED, cos_values, scaled, inverted
from signal import pause
red = PWMLED(2)
blue = PWMLED(3)
red.source = scaled(cos_values(), 0, 1, -1, 1)
blue.source = inverted(red.values)
pause()
If you require a wider range than 0 to 1, see :func:`scaled`.
"""
for d in cycle(range(360)):
yield cos(d)

View File

@@ -6,13 +6,7 @@ from __future__ import (
)
str = type('')
import weakref
from collections import deque
from threading import Thread, Event, RLock
try:
from statistics import median, mean
except ImportError:
from .compat import median, mean
from threading import Thread, Event
from .exc import (
GPIOBadQueueLen,
@@ -46,46 +40,3 @@ class GPIOThread(Thread):
super(GPIOThread, self).join()
_THREADS.discard(self)
class GPIOQueue(GPIOThread):
def __init__(
self, parent, queue_len=5, sample_wait=0.0, partial=False,
average=median):
assert callable(average)
super(GPIOQueue, self).__init__(target=self.fill)
if queue_len < 1:
raise GPIOBadQueueLen('queue_len must be at least one')
if sample_wait < 0:
raise GPIOBadSampleWait('sample_wait must be 0 or greater')
self.queue = deque(maxlen=queue_len)
self.partial = partial
self.sample_wait = sample_wait
self.full = Event()
self.parent = weakref.proxy(parent)
self.average = average
@property
def value(self):
if not self.partial:
self.full.wait()
try:
return self.average(self.queue)
except ZeroDivisionError:
# No data == inactive value
return 0.0
def fill(self):
try:
while (not self.stopping.wait(self.sample_wait) and
len(self.queue) < self.queue.maxlen):
self.queue.append(self.parent._read())
if self.partial:
self.parent._fire_events()
self.full.set()
while not self.stopping.wait(self.sample_wait):
self.queue.append(self.parent._read())
self.parent._fire_events()
except ReferenceError:
# Parent is dead; time to die!
pass