diff --git a/.gitignore b/.gitignore index 9b62b37..583b828 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ coverage .coverage .tox .cache + +# Generated documentation +docs/_build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 213edc5..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,44 +0,0 @@ -# Contributing - -This module was designed for use in education; particularly for young children. -It is intended to provide a simple interface to everyday components. - -If a proposed change added an advanced feature but made basic usage more -complex, it is unlikely to be added. - -## Suggestions - -Please make suggestions for additional components or enhancements to the -codebase by opening an -[issue](https://github.com/RPi-Distro/python-gpiozero/issues) explaining your -reasoning clearly. - -## Bugs - -Please submit bug reports by opening an -[issue](https://github.com/RPi-Distro/python-gpiozero/issues) explaining the -problem clearly using code examples. - -## Documentation - -The documentation source lives in the -[docs](https://github.com/RPi-Distro/python-gpiozero/tree/master/docs) folder. -Contributions to the documentation are welcome but should be easy to read and -understand. - -## Commit messages and pull requests - -Commit messages should be concise but descriptive, and in the form of a patch -description, i.e. instructional not past tense ("Add LED example" not "Added -LED example"). Commits that close (or intend to close) an issue should use the -phrase "fix #123" where `#123` is the issue number. - -## Backwards compatibility - -Since this library reached v1.0 we aim to maintain backwards-compatability -thereafter. Changes which break backwards-compatability will not be accepted. - -## Python - -- Python 2/3 compatibility -- PEP8-compliance (with exceptions) diff --git a/MANIFEST.in b/MANIFEST.in index 9561fb1..40b7bf4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include README.rst +recursive-include tests *.py diff --git a/README.rst b/README.rst index 95ce7f3..c943958 100644 --- a/README.rst +++ b/README.rst @@ -2,13 +2,19 @@ gpiozero ======== -.. image:: https://badge.fury.io/py/gpiozero.svg - :target: https://badge.fury.io/py/gpiozero - :alt: Latest Version +.. ifconfig:: html_theme == 'sphinx_rtd_theme' -.. image:: https://travis-ci.org/RPi-Distro/python-gpiozero.svg?branch=master - :target: https://travis-ci.org/RPi-Distro/python-gpiozero - :alt: Build Tests + .. image:: https://badge.fury.io/py/gpiozero.svg + :target: https://badge.fury.io/py/gpiozero + :alt: Latest Version + + .. image:: https://travis-ci.org/RPi-Distro/python-gpiozero.svg?branch=master + :target: https://travis-ci.org/RPi-Distro/python-gpiozero + :alt: Build Tests + + .. image:: https://img.shields.io/codecov/c/github/RPi-Distro/python-gpiozero/master.svg?maxAge=2592000 + :target: https://codecov.io/github/RPi-Distro/python-gpiozero + :alt: Code Coverage A simple interface to everyday GPIO components used with Raspberry Pi. @@ -69,7 +75,7 @@ or:: Documentation ============= -Comprehensive documentation is available at https://gpiozero.readthedocs.org/. +Comprehensive documentation is available at https://gpiozero.readthedocs.io/. Development =========== @@ -95,8 +101,8 @@ Contributors .. _Raspberry Pi Foundation: https://www.raspberrypi.org/ .. _GitHub: https://github.com/RPi-Distro/python-gpiozero .. _issues: https://github.com/RPi-Distro/python-gpiozero/issues -.. _recipes: http://gpiozero.readthedocs.org/en/latest/recipes.html -.. _Contribute: CONTRIBUTING.md +.. _recipes: http://gpiozero.readthedocs.io/en/latest/recipes.html +.. _contribute: http://gpiozero.readthedocs.io/en/latest/contributing.html .. _Ben Nuttall: https://github.com/bennuttall .. _Dave Jones: https://github.com/waveform80 .. _Martin O'Hanlon: https://github.com/martinohanlon diff --git a/debian/control b/debian/control index 72f2eb4..77a9cf3 100644 --- a/debian/control +++ b/debian/control @@ -11,8 +11,9 @@ X-Python3-Version: >= 3.2 Package: python-gpiozero Architecture: all Section: python -Depends: ${misc:Depends}, ${python:Depends}, python-rpi.gpio -Suggests: python-spidev, python-gpiozero-docs +Depends: ${misc:Depends}, ${python:Depends} +Recommends: python-rpi.gpio, python-spidev +Suggests: python-gpiozero-docs Description: Simple API for controlling devices attached to the GPIO pins. gpiozero builds on RPi.GPIO to provide a set of classes designed to simplify interaction with devices connected to the GPIO pins, from simple buttons and @@ -24,8 +25,9 @@ Description: Simple API for controlling devices attached to the GPIO pins. Package: python3-gpiozero Architecture: all Section: python -Depends: ${misc:Depends}, ${python3:Depends}, python3-rpi.gpio -Suggests: python3-spidev, python-gpiozero-docs +Depends: ${misc:Depends}, ${python3:Depends} +Recommends: python3-rpi.gpio, python3-spidev +Suggests: python-gpiozero-docs Description: Simple API for controlling devices attached to the GPIO pins. gpiozero builds on RPi.GPIO to provide a set of classes designed to simplify interaction with devices connected to the GPIO pins, from simple buttons and diff --git a/docs/api_boards.rst b/docs/api_boards.rst index 622bf05..ca3620b 100644 --- a/docs/api_boards.rst +++ b/docs/api_boards.rst @@ -29,6 +29,13 @@ LEDBarGraph :inherited-members: :members: +ButtonBoard +=========== + +.. autoclass:: ButtonBoard(\*pins, pull_up=True, bounce_time=None, hold_time=1, hold_repeat=False, \*\*named_pins) + :inherited-members: + :members: + TrafficLights ============= @@ -36,6 +43,13 @@ TrafficLights :inherited-members: :members: +LedBorg +======= + +.. autoclass:: LedBorg + :inherited-members: + :members: + PiLITEr ======= @@ -106,6 +120,13 @@ Energenie :inherited-members: :members: +SnowPi +====== + +.. autoclass:: SnowPi + :inherited-members: + :members: + Base Classes ============ diff --git a/docs/api_input.rst b/docs/api_input.rst index d61ca6b..5facbae 100644 --- a/docs/api_input.rst +++ b/docs/api_input.rst @@ -17,7 +17,7 @@ Button ====== .. autoclass:: Button(pin, pull_up=True, bounce_time=None) - :members: wait_for_press, wait_for_release, pin, is_pressed, pull_up, when_pressed, when_released + :members: wait_for_press, wait_for_release, pin, is_pressed, is_held, hold_time, held_time, hold_repeat, pull_up, when_pressed, when_released, when_held Line Sensor (TRCT5000) diff --git a/docs/api_other.rst b/docs/api_other.rst index c83c3e4..a8efaa4 100644 --- a/docs/api_other.rst +++ b/docs/api_other.rst @@ -25,6 +25,11 @@ PingServer .. autoclass:: PingServer +CPUTemperature +============== + +.. autoclass:: CPUTemperature + Base Classes ============ diff --git a/docs/api_output.rst b/docs/api_output.rst index 845079c..7bd888a 100644 --- a/docs/api_output.rst +++ b/docs/api_output.rst @@ -23,13 +23,13 @@ PWMLED ====== .. autoclass:: PWMLED(pin, active_high=True, initial_value=0, frequency=100) - :members: on, off, toggle, blink, pin, is_lit, value + :members: on, off, toggle, blink, pulse, pin, is_lit, value RGBLED ====== -.. autoclass:: RGBLED(red, green, blue, active_high=True, initial_value=(0, 0, 0)) - :members: on, off, toggle, blink, red, green, blue, is_lit, color +.. autoclass:: RGBLED(red, green, blue, active_high=True, initial_value=(0, 0, 0), pwm=True) + :members: on, off, toggle, blink, pulse, red, green, blue, is_lit, color Buzzer ====== @@ -40,9 +40,23 @@ Buzzer Motor ===== -.. autoclass:: Motor(forward, backward) +.. autoclass:: Motor(forward, backward, pwm=True) :members: forward, backward, stop +Servo +===== + +.. autoclass:: Servo(pin, initial_value=0, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000) + :inherited-members: + :members: + +AngularServo +============ + +.. autoclass:: AngularServo(pin, initial_angle=0, min_angle=-90, max_angle=90, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000) + :inherited-members: + :members: + Base Classes ============ diff --git a/docs/api_pins.rst b/docs/api_pins.rst index 83a9dae..b640af4 100644 --- a/docs/api_pins.rst +++ b/docs/api_pins.rst @@ -25,35 +25,39 @@ integer number instead, it uses one of the following classes to provide the 4. :class:`gpiozero.pins.native.NativePin` You can change the default pin implementation by over-writing the -``DefaultPin`` global in the ``devices`` module like so:: +``pin_factory`` global in the ``devices`` module like so:: from gpiozero.pins.native import NativePin import gpiozero.devices # Force the default pin implementation to be NativePin - gpiozero.devices.DefaultPin = NativePin + gpiozero.devices.pin_factory = NativePin from gpiozero import LED # This will now use NativePin instead of RPiGPIOPin led = LED(16) -Alternatively, instead of passing an integer to the device constructor, you -can pass a :class:`Pin` object itself:: +``pin_factory`` is a concrete descendent of the abstract :class:`Pin` class. +The descendent may take additional parameters in its constructor provided they +are optional; GPIO Zero will expect to be able to construct instances with +nothing more than an integer pin number. + +However, the descendent may take default information from additional sources. +For example, to default to creating pins with +:class:`gpiozero.pins.pigpiod.PiGPIOPin` on a remote pi called ``remote-pi`` +you can set the :envvar:`PIGPIO_ADDR` environment variable when running your +script:: + + $ PIGPIO_ADDR=remote-pi python my_script.py + +It is worth noting that instead of passing an integer to device constructors, +you can pass an object derived from :class:`Pin` itself:: from gpiozero.pins.native import NativePin from gpiozero import LED led = LED(NativePin(16)) -This is particularly useful with implementations that can take extra parameters -such as :class:`~gpiozero.pins.pigpiod.PiGPIOPin` which can address pins on -remote machines:: - - from gpiozero.pins.pigpiod import PiGPIOPin - from gpiozero import LED - - led = LED(PiGPIOPin(16, host='my_other_pi')) - In future, this separation of pins and devices should also permit the library to utilize pins that are part of IO extender chips. For example:: @@ -110,6 +114,13 @@ Abstract Pin :members: +Local Pin +========= + +.. autoclass:: LocalPin + :members: + + Utilities ========= diff --git a/docs/api_tools.rst b/docs/api_tools.rst index f23cf71..69b2a60 100644 --- a/docs/api_tools.rst +++ b/docs/api_tools.rst @@ -10,7 +10,7 @@ the :attr:`~gpiozero.SourceMixin.source` and library. These utility routines are in the ``tools`` module of GPIO Zero and are typically imported as follows:: - from gpiozero.tools import scaled, negated, conjunction + from gpiozero.tools import scaled, negated, all_values Given that :attr:`~gpiozero.SourceMixin.source` and :attr:`~gpiozero.ValuesMixin.values` deal with infinite iterators, another @@ -29,6 +29,8 @@ Single source conversions .. autofunction:: absoluted +.. autofunction:: booleanized + .. autofunction:: clamped .. autofunction:: inverted @@ -37,12 +39,18 @@ Single source conversions .. autofunction:: post_delayed +.. autofunction:: post_periodic_filtered + .. autofunction:: pre_delayed +.. autofunction:: pre_periodic_filtered + .. autofunction:: quantized .. autofunction:: queued +.. autofunction:: smoothed + .. autofunction:: scaled Combining sources @@ -54,8 +62,12 @@ Combining sources .. autofunction:: averaged -Artifical sources -================= +.. autofunction:: multiplied + +.. autofunction:: summed + +Artificial sources +================== .. autofunction:: cos_values diff --git a/docs/conf.py b/docs/conf.py index fea2f1c..d73e36e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,7 +46,12 @@ sys.modules['spidev'] = Mock() # -- General configuration ------------------------------------------------ -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] +extensions = [ + 'sphinx.ext.autodoc', # support for automethod, autoclass, etc. + 'sphinx.ext.viewcode', # support for "Source" links in output + 'sphinx.ext.intersphinx', # support links to Python library docs etc. + 'sphinx.ext.ifconfig', # support for ifconfig conditional includes + ] templates_path = ['_templates'] source_suffix = '.rst' #source_encoding = 'utf-8-sig' @@ -74,6 +79,7 @@ autodoc_member_order = 'groupwise' intersphinx_mapping = { 'python': ('http://docs.python.org/3.4', None), + 'picamera': ('http://picamera.readthedocs.io/en/latest', None), } # -- Options for HTML output ---------------------------------------------- diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..d1c6d17 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,52 @@ +.. _contributing: + +============ +Contributing +============ + +This module was designed for use in education; particularly for young children. +It is intended to provide a simple interface to everyday components. + +If a proposed change added an advanced feature but made basic usage more +complex, it is unlikely to be added. + +Suggestions +=========== + +Please make suggestions for additional components or enhancements to the +codebase by opening an `issue`_ explaining your reasoning clearly. + +Bugs +==== + +Please submit bug reports by opening an `issue`_ explaining the problem clearly +using code examples. + +Documentation +============= + +The documentation source lives in the `docs`_ folder. Contributions to the +documentation are welcome but should be easy to read and understand. + +Commit messages and pull requests +================================= + +Commit messages should be concise but descriptive, and in the form of a patch +description, i.e. instructional not past tense ("Add LED example" not "Added +LED example"). Commits that close (or intend to close) an issue should use the +phrase "fix #123" where ``#123`` is the issue number. + +Backwards compatibility +======================= + +Since this library reached v1.0 we aim to maintain backwards-compatibility +thereafter. Changes which break backwards-compatibility will not be accepted. + +Python +====== + +* Python 2/3 compatibility +* PEP8-compliance (with exceptions) + +.. _docs: https://github.com/RPi-Distro/python-gpiozero/tree/master/docs +.. _issue: https://github.com/RPi-Distro/python-gpiozero/issues diff --git a/docs/examples/all_on_1.py b/docs/examples/all_on_1.py new file mode 100644 index 0000000..978e4e1 --- /dev/null +++ b/docs/examples/all_on_1.py @@ -0,0 +1,9 @@ +from gpiozero import FishDish +from signal import pause + +fish = FishDish() + +fish.button.when_pressed = fish.on +fish.button.when_released = fish.off + +pause() diff --git a/docs/examples/all_on_2.py b/docs/examples/all_on_2.py new file mode 100644 index 0000000..0dccc78 --- /dev/null +++ b/docs/examples/all_on_2.py @@ -0,0 +1,9 @@ +from gpiozero import TrafficHat +from signal import pause + +th = TrafficHat() + +th.button.when_pressed = th.on +th.button.when_released = th.off + +pause() diff --git a/docs/examples/all_on_3.py b/docs/examples/all_on_3.py new file mode 100644 index 0000000..682adb4 --- /dev/null +++ b/docs/examples/all_on_3.py @@ -0,0 +1,23 @@ +from gpiozero import LED, Buzzer, Button +from signal import pause + +button = Button(2) +buzzer = Buzzer(3) +red = LED(4) +amber = LED(5) +green = LED(6) + +things = [red, amber, green, buzzer] + +def things_on(): + for thing in things: + thing.on() + +def things_off(): + for thing in things: + thing.off() + +button.when_pressed = things_on +button.when_released = things_off + +pause() diff --git a/docs/examples/button_1.py b/docs/examples/button_1.py new file mode 100644 index 0000000..5ea7e49 --- /dev/null +++ b/docs/examples/button_1.py @@ -0,0 +1,9 @@ +from gpiozero import Button + +button = Button(2) + +while True: + if button.is_pressed: + print("Button is pressed") + else: + print("Button is not pressed") diff --git a/docs/examples/button_2.py b/docs/examples/button_2.py new file mode 100644 index 0000000..4067627 --- /dev/null +++ b/docs/examples/button_2.py @@ -0,0 +1,6 @@ +from gpiozero import Button + +button = Button(2) + +button.wait_for_press() +print("Button was pressed") diff --git a/docs/examples/button_3.py b/docs/examples/button_3.py new file mode 100644 index 0000000..55694e1 --- /dev/null +++ b/docs/examples/button_3.py @@ -0,0 +1,11 @@ +from gpiozero import Button +from signal import pause + +def say_hello(): + print("Hello!") + +button = Button(2) + +button.when_pressed = say_hello + +pause() diff --git a/docs/examples/button_4.py b/docs/examples/button_4.py new file mode 100644 index 0000000..d3f9d34 --- /dev/null +++ b/docs/examples/button_4.py @@ -0,0 +1,15 @@ +from gpiozero import Button +from signal import pause + +def say_hello(): + print("Hello!") + +def say_goodbye(): + print("Goodbye!") + +button = Button(2) + +button.when_pressed = say_hello +button.when_released = say_goodbye + +pause() diff --git a/docs/examples/button_camera_1.py b/docs/examples/button_camera_1.py new file mode 100644 index 0000000..0affb17 --- /dev/null +++ b/docs/examples/button_camera_1.py @@ -0,0 +1,15 @@ +from gpiozero import Button +from picamera import PiCamera +from datetime import datetime +from signal import pause + +button = Button(2) +camera = PiCamera() + +def capture(): + datetime = datetime.now().isoformat() + camera.capture('/home/pi/%s.jpg' % datetime) + +button.when_pressed = capture + +pause() diff --git a/docs/examples/button_camera_2.py b/docs/examples/button_camera_2.py new file mode 100644 index 0000000..18552d4 --- /dev/null +++ b/docs/examples/button_camera_2.py @@ -0,0 +1,18 @@ +from gpiozero import Button +from picamera import PiCamera +from datetime import datetime +from signal import pause + +left_button = Button(2) +right_button = Button(3) +camera = PiCamera() + +def capture(): + datetime = datetime.now().isoformat() + camera.capture('/home/pi/%s.jpg' % datetime) + +left_button.when_pressed = camera.start_preview +left_button.when_released = camera.stop_preview +right_button.when_pressed = capture + +pause() diff --git a/docs/examples/button_led_1.py b/docs/examples/button_led_1.py new file mode 100644 index 0000000..3dce993 --- /dev/null +++ b/docs/examples/button_led_1.py @@ -0,0 +1,10 @@ +from gpiozero import LED, Button +from signal import pause + +led = LED(17) +button = Button(2) + +button.when_pressed = led.on +button.when_released = led.off + +pause() diff --git a/docs/examples/button_led_2.py b/docs/examples/button_led_2.py new file mode 100644 index 0000000..ba66df5 --- /dev/null +++ b/docs/examples/button_led_2.py @@ -0,0 +1,9 @@ +from gpiozero import LED, Button +from signal import pause + +led = LED(17) +button = Button(2) + +led.source = button.values + +pause() diff --git a/docs/examples/button_shutdown.py b/docs/examples/button_shutdown.py new file mode 100644 index 0000000..2a91a40 --- /dev/null +++ b/docs/examples/button_shutdown.py @@ -0,0 +1,11 @@ +from gpiozero import Button +from subprocess import check_call +from signal import pause + +def shutdown(): + check_call(['sudo', 'poweroff']) + +shutdown_btn = Button(17, hold_time=2) +shutdown_btn.when_held = shutdown + +pause() diff --git a/docs/examples/button_stop_motion.py b/docs/examples/button_stop_motion.py new file mode 100644 index 0000000..8111b48 --- /dev/null +++ b/docs/examples/button_stop_motion.py @@ -0,0 +1,12 @@ +from gpiozero import Button +from picamera import PiCamera + +button = Button(2) +camera = PiCamera() + +camera.start_preview() +frame = 1 +while True: + button.wait_for_press() + camera.capture('/home/pi/frame%03d.jpg' % frame) + frame += 1 diff --git a/docs/examples/distance_sensor_1.py b/docs/examples/distance_sensor_1.py new file mode 100644 index 0000000..c777aa9 --- /dev/null +++ b/docs/examples/distance_sensor_1.py @@ -0,0 +1,8 @@ +from gpiozero import DistanceSensor +from time import sleep + +sensor = DistanceSensor(23, 24) + +while True: + print('Distance to nearest object is', sensor.distance, 'm') + sleep(1) diff --git a/docs/examples/distance_sensor_2.py b/docs/examples/distance_sensor_2.py new file mode 100644 index 0000000..86dfe74 --- /dev/null +++ b/docs/examples/distance_sensor_2.py @@ -0,0 +1,10 @@ +from gpiozero import DistanceSensor, LED +from signal import pause + +sensor = DistanceSensor(23, 24, max_distance=1, threshold_distance=0.2) +led = LED(16) + +sensor.when_in_range = led.on +sensor.when_out_of_range = led.off + +pause() diff --git a/docs/examples/led_1.py b/docs/examples/led_1.py new file mode 100644 index 0000000..5e031b9 --- /dev/null +++ b/docs/examples/led_1.py @@ -0,0 +1,10 @@ +from gpiozero import LED +from time import sleep + +red = LED(17) + +while True: + red.on() + sleep(1) + red.off() + sleep(1) diff --git a/docs/examples/led_2.py b/docs/examples/led_2.py new file mode 100644 index 0000000..c09e5dd --- /dev/null +++ b/docs/examples/led_2.py @@ -0,0 +1,8 @@ +from gpiozero import LED +from signal import pause + +red = LED(17) + +red.blink() + +pause() diff --git a/docs/examples/led_bargraph_1.py b/docs/examples/led_bargraph_1.py new file mode 100644 index 0000000..96534b6 --- /dev/null +++ b/docs/examples/led_bargraph_1.py @@ -0,0 +1,15 @@ +from gpiozero import LEDBarGraph +from time import sleep + +graph = LEDBarGraph(5, 6, 13, 19, 26, 20) + +graph.value = 1 # (1, 1, 1, 1, 1, 1) +sleep(1) +graph.value = 1/2 # (1, 1, 1, 0, 0, 0) +sleep(1) +graph.value = -1/2 # (0, 0, 0, 1, 1, 1) +sleep(1) +graph.value = 1/4 # (1, 0, 0, 0, 0, 0) +sleep(1) +graph.value = -1 # (1, 1, 1, 1, 1, 1) +sleep(1) diff --git a/docs/examples/led_bargraph_2.py b/docs/examples/led_bargraph_2.py new file mode 100644 index 0000000..9544990 --- /dev/null +++ b/docs/examples/led_bargraph_2.py @@ -0,0 +1,15 @@ +from gpiozero import LEDBarGraph +from time import sleep + +graph = LEDBarGraph(5, 6, 13, 19, 26, pwm=True) + +graph.value = 1/10 # (0.5, 0, 0, 0, 0) +sleep(1) +graph.value = 3/10 # (1, 0.5, 0, 0, 0) +sleep(1) +graph.value = -3/10 # (0, 0, 0, 0.5, 1) +sleep(1) +graph.value = 9/10 # (1, 1, 1, 1, 0.5) +sleep(1) +graph.value = 95/100 # (1, 1, 1, 1, 0.75) +sleep(1) diff --git a/docs/examples/led_board_1.py b/docs/examples/led_board_1.py new file mode 100644 index 0000000..e4a3a68 --- /dev/null +++ b/docs/examples/led_board_1.py @@ -0,0 +1,15 @@ +from gpiozero import LEDBoard +from time import sleep +from signal import pause + +leds = LEDBoard(5, 6, 13, 19, 26) + +leds.on() +sleep(1) +leds.off() +sleep(1) +leds.value = (1, 0, 1, 0, 1) +sleep(1) +leds.blink() + +pause() diff --git a/docs/examples/led_board_2.py b/docs/examples/led_board_2.py new file mode 100644 index 0000000..2b01a6d --- /dev/null +++ b/docs/examples/led_board_2.py @@ -0,0 +1,5 @@ +from gpiozero import LEDBoard + +leds = LEDBoard(5, 6, 13, 19, 26, pwm=True) + +leds.value = (0.2, 0.4, 0.6, 0.8, 1.0) diff --git a/docs/examples/led_builtin.py b/docs/examples/led_builtin.py new file mode 100644 index 0000000..80a06ff --- /dev/null +++ b/docs/examples/led_builtin.py @@ -0,0 +1,9 @@ +from gpiozero import LED +from signal import pause + +power = LED(35) # /sys/class/leds/led1 +activity = LED(47) # /sys/class/leds/led0 + +activity.blink() +power.blink() +pause() diff --git a/docs/examples/led_pulse.py b/docs/examples/led_pulse.py new file mode 100644 index 0000000..bc6ac49 --- /dev/null +++ b/docs/examples/led_pulse.py @@ -0,0 +1,8 @@ +from gpiozero import PWMLED +from signal import pause + +led = PWMLED(17) + +led.pulse() + +pause() diff --git a/docs/examples/led_travis.py b/docs/examples/led_travis.py new file mode 100644 index 0000000..c5b237d --- /dev/null +++ b/docs/examples/led_travis.py @@ -0,0 +1,19 @@ +from travispy import TravisPy +from gpiozero import LED +from gpiozero.tools import negated +from time import sleep +from signal import pause + +def build_passed(repo='RPi-Distro/python-gpiozero', delay=3600): + t = TravisPy() + r = t.repo(repo) + while True: + yield r.last_build_state == 'passed' + sleep(delay) # Sleep an hour before hitting travis again + +red = LED(12) +green = LED(16) + +red.source = negated(green.values) +green.source = build_passed() +pause() diff --git a/docs/examples/led_variable_brightness.py b/docs/examples/led_variable_brightness.py new file mode 100644 index 0000000..4b9c232 --- /dev/null +++ b/docs/examples/led_variable_brightness.py @@ -0,0 +1,12 @@ +from gpiozero import PWMLED +from time import sleep + +led = PWMLED(17) + +while True: + led.value = 0 # off + sleep(1) + led.value = 0.5 # half brightness + sleep(1) + led.value = 1 # full brightness + sleep(1) diff --git a/docs/examples/light_sensor_1.py b/docs/examples/light_sensor_1.py new file mode 100644 index 0000000..48ed945 --- /dev/null +++ b/docs/examples/light_sensor_1.py @@ -0,0 +1,9 @@ +from gpiozero import LightSensor + +sensor = LightSensor(18) + +while True: + sensor.wait_for_light() + print("It's light! :)") + sensor.wait_for_dark() + print("It's dark :(") diff --git a/docs/examples/light_sensor_2.py b/docs/examples/light_sensor_2.py new file mode 100644 index 0000000..b618365 --- /dev/null +++ b/docs/examples/light_sensor_2.py @@ -0,0 +1,10 @@ +from gpiozero import LightSensor, LED +from signal import pause + +sensor = LightSensor(18) +led = LED(16) + +sensor.when_dark = led.on +sensor.when_light = led.off + +pause() diff --git a/docs/examples/light_sensor_3.py b/docs/examples/light_sensor_3.py new file mode 100644 index 0000000..fb22dec --- /dev/null +++ b/docs/examples/light_sensor_3.py @@ -0,0 +1,9 @@ +from gpiozero import LightSensor, PWMLED +from signal import pause + +sensor = LightSensor(18) +led = PWMLED(16) + +led.source = sensor.values + +pause() diff --git a/docs/examples/motion_sensor.py b/docs/examples/motion_sensor.py new file mode 100644 index 0000000..0ff122a --- /dev/null +++ b/docs/examples/motion_sensor.py @@ -0,0 +1,10 @@ +from gpiozero import MotionSensor, LED +from signal import pause + +pir = MotionSensor(4) +led = LED(16) + +pir.when_motion = led.on +pir.when_no_motion = led.off + +pause() diff --git a/docs/examples/motor.py b/docs/examples/motor.py new file mode 100644 index 0000000..a0d6fd3 --- /dev/null +++ b/docs/examples/motor.py @@ -0,0 +1,10 @@ +from gpiozero import Motor +from time import sleep + +motor = Motor(forward=4, backward=14) + +while True: + motor.forward() + sleep(5) + motor.backward() + sleep(5) diff --git a/docs/examples/music_box.py b/docs/examples/music_box.py new file mode 100644 index 0000000..872b08e --- /dev/null +++ b/docs/examples/music_box.py @@ -0,0 +1,18 @@ +from gpiozero import Button +import pygame.mixer +from pygame.mixer import Sound +from signal import pause + +pygame.mixer.init() + +sound_pins = { + 2: Sound("samples/drum_tom_mid_hard.wav"), + 3: Sound("samples/drum_cymbal_open.wav"), +} + +buttons = [Button(pin) for pin in sound_pins] +for button in buttons: + sound = sound_pins[button.pin.number] + button.when_pressed = sound.play + +pause() diff --git a/docs/examples/pot_1.py b/docs/examples/pot_1.py new file mode 100644 index 0000000..1a8e703 --- /dev/null +++ b/docs/examples/pot_1.py @@ -0,0 +1,6 @@ +from gpiozero import MCP3008 + +pot = MCP3008(channel=0) + +while True: + print(pot.value) diff --git a/docs/examples/pot_2.py b/docs/examples/pot_2.py new file mode 100644 index 0000000..b770edf --- /dev/null +++ b/docs/examples/pot_2.py @@ -0,0 +1,7 @@ +from gpiozero import LEDBarGraph, MCP3008 +from signal import pause + +graph = LEDBarGraph(5, 6, 13, 19, 26, pwm=True) +pot = MCP3008(channel=0) +graph.source = pot.values +pause() diff --git a/docs/examples/reaction_game.py b/docs/examples/reaction_game.py new file mode 100644 index 0000000..f8e3a52 --- /dev/null +++ b/docs/examples/reaction_game.py @@ -0,0 +1,22 @@ +from gpiozero import Button, LED +from time import sleep +import random + +led = LED(17) + +player_1 = Button(2) +player_2 = Button(3) + +time = random.uniform(5, 10) +sleep(time) +led.on() + +while True: + if player_1.is_pressed: + print("Player 1 wins!") + break + if player_2.is_pressed: + print("Player 2 wins!") + break + +led.off() diff --git a/docs/examples/rgbled.py b/docs/examples/rgbled.py new file mode 100644 index 0000000..b80c9dc --- /dev/null +++ b/docs/examples/rgbled.py @@ -0,0 +1,28 @@ +from gpiozero import RGBLED +from time import sleep + +led = RGBLED(red=9, green=10, blue=11) + +led.red = 1 # full red +sleep(1) +led.red = 0.5 # half red +sleep(1) + +led.color = (0, 1, 0) # full green +sleep(1) +led.color = (1, 0, 1) # magenta +sleep(1) +led.color = (1, 1, 0) # yellow +sleep(1) +led.color = (0, 1, 1) # cyan +sleep(1) +led.color = (1, 1, 1) # white +sleep(1) + +led.color = (0, 0, 0) # off +sleep(1) + +# slowly increase intensity of blue +for n in range(100): + led.blue = n/100 + sleep(0.1) diff --git a/docs/examples/rgbled_pot_1.py b/docs/examples/rgbled_pot_1.py new file mode 100644 index 0000000..1efa5d0 --- /dev/null +++ b/docs/examples/rgbled_pot_1.py @@ -0,0 +1,11 @@ +from gpiozero import RGBLED, MCP3008 + +led = RGBLED(red=2, green=3, blue=4) +red_pot = MCP3008(channel=0) +green_pot = MCP3008(channel=1) +blue_pot = MCP3008(channel=2) + +while True: + led.red = red_pot.value + led.green = green_pot.value + led.blue = blue_pot.value diff --git a/docs/examples/rgbled_pot_2.py b/docs/examples/rgbled_pot_2.py new file mode 100644 index 0000000..a5d49cb --- /dev/null +++ b/docs/examples/rgbled_pot_2.py @@ -0,0 +1,11 @@ +from gpiozero import RGBLED, MCP3008 +from signal import pause + +led = RGBLED(2, 3, 4) +red_pot = MCP3008(0) +green_pot = MCP3008(1) +blue_pot = MCP3008(2) + +led.source = zip(red_pot.values, green_pot.values, blue_pot.values) + +pause() diff --git a/docs/examples/robot_1.py b/docs/examples/robot_1.py new file mode 100644 index 0000000..8c0a791 --- /dev/null +++ b/docs/examples/robot_1.py @@ -0,0 +1,10 @@ +from gpiozero import Robot +from time import sleep + +robot = Robot(left=(4, 14), right=(17, 18)) + +for i in range(4): + robot.forward() + sleep(10) + robot.right() + sleep(1) diff --git a/docs/examples/robot_2.py b/docs/examples/robot_2.py new file mode 100644 index 0000000..55107be --- /dev/null +++ b/docs/examples/robot_2.py @@ -0,0 +1,9 @@ +from gpiozero import Robot, DistanceSensor +from signal import pause + +sensor = DistanceSensor(23, 24, max_distance=1, threshold_distance=0.2) +robot = Robot(left=(4, 14), right=(17, 18)) + +sensor.when_in_range = robot.backward +sensor.when_out_of_range = robot.stop +pause() diff --git a/docs/examples/robot_buttons.py b/docs/examples/robot_buttons.py new file mode 100644 index 0000000..58a285c --- /dev/null +++ b/docs/examples/robot_buttons.py @@ -0,0 +1,23 @@ +from gpiozero import Robot, Button +from signal import pause + +robot = Robot(left=(4, 14), right=(17, 18)) + +left = Button(26) +right = Button(16) +fw = Button(21) +bw = Button(20) + +fw.when_pressed = robot.forward +fw.when_released = robot.stop + +left.when_pressed = robot.left +left.when_released = robot.stop + +right.when_pressed = robot.right +right.when_released = robot.stop + +bw.when_pressed = robot.backward +bw.when_released = robot.stop + +pause() diff --git a/docs/examples/robot_keyboard_1.py b/docs/examples/robot_keyboard_1.py new file mode 100644 index 0000000..366993a --- /dev/null +++ b/docs/examples/robot_keyboard_1.py @@ -0,0 +1,34 @@ +import curses +from gpiozero import Robot + +robot = Robot(left=(4, 14), right=(17, 18)) + +actions = { + curses.KEY_UP: robot.forward, + curses.KEY_DOWN: robot.backward, + curses.KEY_LEFT: robot.left, + curses.KEY_RIGHT: robot.right, + } + +def main(window): + next_key = None + while True: + curses.halfdelay(1) + if next_key is None: + key = window.getch() + else: + key = next_key + next_key = None + if key != -1: + # KEY DOWN + curses.halfdelay(3) + action = actions.get(key) + if action is not None: + action() + next_key = key + while next_key == key: + next_key = window.getch() + # KEY UP + robot.stop() + +curses.wrapper(main) diff --git a/docs/examples/robot_keyboard_2.py b/docs/examples/robot_keyboard_2.py new file mode 100644 index 0000000..abd7e50 --- /dev/null +++ b/docs/examples/robot_keyboard_2.py @@ -0,0 +1,36 @@ +from gpiozero import Robot +from evdev import InputDevice, list_devices, ecodes + +robot = Robot(left=(4, 14), right=(17, 18)) + +# Get the list of available input devices +devices = [InputDevice(device) for device in list_devices()] +# Filter out everything that's not a keyboard. Keyboards are defined as any +# device which has keys, and which specifically has keys 1..31 (roughly Esc, +# the numeric keys, the first row of QWERTY plus a few more) and which does +# *not* have key 0 (reserved) +must_have = {i for i in range(1, 32)} +must_not_have = {0} +devices = [ + dev + for dev in devices + for keys in (set(dev.capabilities().get(ecodes.EV_KEY, [])),) + if must_have.issubset(keys) + and must_not_have.isdisjoint(keys) +] +# Pick the first keyboard +keyboard = devices[0] + +keypress_actions = { + ecodes.KEY_UP: robot.forward, + ecodes.KEY_DOWN: robot.backward, + ecodes.KEY_LEFT: robot.left, + ecodes.KEY_RIGHT: robot.right, +} + +for event in keyboard.read_loop(): + if event.type == ecodes.EV_KEY and event.code in keypress_actions: + if event.value == 1: # key down + keypress_actions[event.code]() + if event.value == 0: # key up + robot.stop() diff --git a/docs/examples/robot_motion_1.py b/docs/examples/robot_motion_1.py new file mode 100644 index 0000000..11ae2a4 --- /dev/null +++ b/docs/examples/robot_motion_1.py @@ -0,0 +1,10 @@ +from gpiozero import Robot, MotionSensor +from signal import pause + +robot = Robot(left=(4, 14), right=(17, 18)) +pir = MotionSensor(5) + +pir.when_motion = robot.forward +pir.when_no_motion = robot.stop + +pause() diff --git a/docs/examples/robot_motion_2.py b/docs/examples/robot_motion_2.py new file mode 100644 index 0000000..35580cb --- /dev/null +++ b/docs/examples/robot_motion_2.py @@ -0,0 +1,9 @@ +from gpiozero import Robot, MotionSensor +from signal import pause + +robot = Robot(left=(4, 14), right=(17, 18)) +pir = MotionSensor(5) + +robot.source = zip(pir.values, pir.values) + +pause() diff --git a/docs/examples/thermometer.py b/docs/examples/thermometer.py new file mode 100644 index 0000000..d18b6f7 --- /dev/null +++ b/docs/examples/thermometer.py @@ -0,0 +1,12 @@ +from gpiozero import MCP3008 +from time import sleep + +def convert_temp(gen): + for value in gen: + yield (value * 3.3 - 0.5) * 100 + +adc = MCP3008(channel=0) + +for temp in convert_temp(adc.values): + print('The temperature is', temp, 'C') + sleep(1) diff --git a/docs/examples/traffic_lights_1.py b/docs/examples/traffic_lights_1.py new file mode 100644 index 0000000..d102b0b --- /dev/null +++ b/docs/examples/traffic_lights_1.py @@ -0,0 +1,20 @@ +from gpiozero import TrafficLights +from time import sleep + +lights = TrafficLights(2, 3, 4) + +lights.green.on() + +while True: + sleep(10) + lights.green.off() + lights.amber.on() + sleep(1) + lights.amber.off() + lights.red.on() + sleep(10) + lights.amber.on() + sleep(1) + lights.green.on() + lights.amber.off() + lights.red.off() diff --git a/docs/examples/traffic_lights_2.py b/docs/examples/traffic_lights_2.py new file mode 100644 index 0000000..a3f866c --- /dev/null +++ b/docs/examples/traffic_lights_2.py @@ -0,0 +1,20 @@ +from gpiozero import TrafficLights +from time import sleep +from signal import pause + +lights = TrafficLights(2, 3, 4) + +def traffic_light_sequence(): + while True: + yield (0, 0, 1) # green + sleep(10) + yield (0, 1, 0) # amber + sleep(1) + yield (1, 0, 0) # red + sleep(10) + yield (1, 1, 0) # red+amber + sleep(1) + +lights.source = traffic_light_sequence() + +pause() diff --git a/docs/examples/traffic_lights_3.py b/docs/examples/traffic_lights_3.py new file mode 100644 index 0000000..8bf0aa5 --- /dev/null +++ b/docs/examples/traffic_lights_3.py @@ -0,0 +1,24 @@ +from gpiozero import LED +from time import sleep + +red = LED(2) +amber = LED(3) +green = LED(4) + +green.on() +amber.off() +red.off() + +while True: + sleep(10) + green.off() + amber.on() + sleep(1) + amber.off() + red.on() + sleep(10) + amber.on() + sleep(1) + green.on() + amber.off() + red.off() diff --git a/docs/images/composed_devices.dot b/docs/images/composed_devices.dot index fa43d3b..5a1fe74 100644 --- a/docs/images/composed_devices.dot +++ b/docs/images/composed_devices.dot @@ -5,15 +5,20 @@ digraph classes { node [shape=rect, style=filled, color="#298029", fontname=Sans, fontcolor="#ffffff", fontsize=10]; edge [arrowhead=onormal, style=dashed]; + RGBLED->LED; RGBLED->PWMLED; LEDBoard->LED; LEDBoard->PWMLED; LEDBarGraph->LED; LEDBarGraph->PWMLED; + ButtonBoard->Button; + TrafficLightsBuzzer->TrafficLights; TrafficLightsBuzzer->Buzzer; TrafficLightsBuzzer->Button; Robot->Motor; + Motor->DigitalOutputDevice; + Motor->PWMOutputDevice; } diff --git a/docs/images/composed_devices.pdf b/docs/images/composed_devices.pdf index e334ce7..d1178ac 100644 Binary files a/docs/images/composed_devices.pdf and b/docs/images/composed_devices.pdf differ diff --git a/docs/images/composed_devices.png b/docs/images/composed_devices.png index 07965ef..ad25571 100644 Binary files a/docs/images/composed_devices.png and b/docs/images/composed_devices.png differ diff --git a/docs/images/composed_devices.svg b/docs/images/composed_devices.svg index fed5a09..1fd2dff 100644 --- a/docs/images/composed_devices.svg +++ b/docs/images/composed_devices.svg @@ -1,113 +1,148 @@ - - - + + classes - + RGBLED - -RGBLED - - -PWMLED - -PWMLED - - -RGBLED->PWMLED - - - - -LEDBoard - -LEDBoard - - -LEDBoard->PWMLED - - + +RGBLED -LED - -LED +LED + +LED + + +RGBLED->LED + + + + +PWMLED + +PWMLED + + +RGBLED->PWMLED + + + + +LEDBoard + +LEDBoard -LEDBoard->LED - - +LEDBoard->LED + + + + +LEDBoard->PWMLED + + LEDBarGraph - -LEDBarGraph - - -LEDBarGraph->PWMLED - - + +LEDBarGraph -LEDBarGraph->LED - - +LEDBarGraph->LED + + - -TrafficLightsBuzzer - -TrafficLightsBuzzer + +LEDBarGraph->PWMLED + + - -TrafficLights - -TrafficLights - - -TrafficLightsBuzzer->TrafficLights - - - - -Buzzer - -Buzzer - - -TrafficLightsBuzzer->Buzzer - - + +ButtonBoard + +ButtonBoard -Button - -Button +Button + +Button + + +ButtonBoard->Button + + + + +TrafficLightsBuzzer + +TrafficLightsBuzzer -TrafficLightsBuzzer->Button - - +TrafficLightsBuzzer->Button + + + + +TrafficLights + +TrafficLights + + +TrafficLightsBuzzer->TrafficLights + + + + +Buzzer + +Buzzer + + +TrafficLightsBuzzer->Buzzer + + -Robot - -Robot +Robot + +Robot -Motor - -Motor +Motor + +Motor -Robot->Motor - - +Robot->Motor + + + + +DigitalOutputDevice + +DigitalOutputDevice + + +Motor->DigitalOutputDevice + + + + +PWMOutputDevice + +PWMOutputDevice + + +Motor->PWMOutputDevice + + diff --git a/docs/images/composite_device_hierarchy.dot b/docs/images/composite_device_hierarchy.dot index d57cc2d..2d8c5aa 100644 --- a/docs/images/composite_device_hierarchy.dot +++ b/docs/images/composite_device_hierarchy.dot @@ -32,5 +32,8 @@ digraph classes { RyanteckRobot->Robot; CamJamKitRobot->Robot; Motor->CompositeDevice; + Servo->CompositeDevice; + AngularServo->Servo; Energenie->Device; + ButtonBoard->CompositeDevice; } diff --git a/docs/images/composite_device_hierarchy.pdf b/docs/images/composite_device_hierarchy.pdf index 05ad156..8d7b3f3 100644 Binary files a/docs/images/composite_device_hierarchy.pdf and b/docs/images/composite_device_hierarchy.pdf differ diff --git a/docs/images/composite_device_hierarchy.png b/docs/images/composite_device_hierarchy.png index ef7d226..81f75d7 100644 Binary files a/docs/images/composite_device_hierarchy.png and b/docs/images/composite_device_hierarchy.png differ diff --git a/docs/images/composite_device_hierarchy.svg b/docs/images/composite_device_hierarchy.svg index 441ce3f..4c423d1 100644 --- a/docs/images/composite_device_hierarchy.svg +++ b/docs/images/composite_device_hierarchy.svg @@ -4,25 +4,25 @@ - + classes - + Device - -Device + +Device CompositeDevice - -CompositeDevice + +CompositeDevice CompositeDevice->Device - - + + CompositeOutputDevice @@ -31,8 +31,8 @@ CompositeOutputDevice->CompositeDevice - - + + LEDCollection @@ -136,13 +136,13 @@ Robot - -Robot + +Robot Robot->CompositeDevice - - + + RyanteckRobot @@ -151,8 +151,8 @@ RyanteckRobot->Robot - - + + CamJamKitRobot @@ -161,28 +161,58 @@ CamJamKitRobot->Robot - - + + Motor - -Motor + +Motor Motor->CompositeDevice - - + + + + +Servo + +Servo + + +Servo->CompositeDevice + + + + +AngularServo + +AngularServo + + +AngularServo->Servo + + -Energenie - -Energenie +Energenie + +Energenie -Energenie->Device - - +Energenie->Device + + + + +ButtonBoard + +ButtonBoard + + +ButtonBoard->CompositeDevice + + diff --git a/docs/images/output_device_hierarchy.dot b/docs/images/output_device_hierarchy.dot index 1cd5e02..6a92a52 100644 --- a/docs/images/output_device_hierarchy.dot +++ b/docs/images/output_device_hierarchy.dot @@ -21,5 +21,6 @@ digraph classes { PWMOutputDevice->OutputDevice; PWMLED->PWMOutputDevice; RGBLED->Device; + LedBorg->RGBLED; } diff --git a/docs/images/output_device_hierarchy.pdf b/docs/images/output_device_hierarchy.pdf index 91cb8c1..b7ffe95 100644 Binary files a/docs/images/output_device_hierarchy.pdf and b/docs/images/output_device_hierarchy.pdf differ diff --git a/docs/images/output_device_hierarchy.png b/docs/images/output_device_hierarchy.png index 61bf956..7afa368 100644 Binary files a/docs/images/output_device_hierarchy.png and b/docs/images/output_device_hierarchy.png differ diff --git a/docs/images/output_device_hierarchy.svg b/docs/images/output_device_hierarchy.svg index a2a922c..4f1cad2 100644 --- a/docs/images/output_device_hierarchy.svg +++ b/docs/images/output_device_hierarchy.svg @@ -11,18 +11,18 @@ Device - -Device + +Device GPIODevice - -GPIODevice + +GPIODevice GPIODevice->Device - - + + OutputDevice @@ -31,8 +31,8 @@ OutputDevice->GPIODevice - - + + DigitalOutputDevice @@ -86,13 +86,23 @@ RGBLED - -RGBLED + +RGBLED RGBLED->Device - - + + + + +LedBorg + +LedBorg + + +LedBorg->RGBLED + + diff --git a/docs/index.rst b/docs/index.rst index cb32ed0..256db1e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,7 @@ Table of Contents recipes notes + contributing api_input api_output api_spi @@ -19,3 +20,4 @@ Table of Contents api_exc changelog license + diff --git a/docs/notes.rst b/docs/notes.rst index f111d80..7c4ba69 100644 --- a/docs/notes.rst +++ b/docs/notes.rst @@ -70,3 +70,26 @@ In this case, all references to items within GPIO Zero must be prefixed:: button = gpiozero.Button(2) +How can I tell what version of gpiozero I have installed? +========================================================= + +The gpiozero library relies on the setuptools package for installation +services. You can use the setuptools ``pkg_resources`` API to query which +version of gpiozero is available in your Python environment like so:: + + >>> from pkg_resources import require + >>> require('gpiozero') + [gpiozero 1.2.0 (/usr/local/lib/python2.7/dist-packages)] + >>> require('gpiozero')[0].version + '1.2.0' + +If you have multiple versions installed (e.g. from ``pip`` and ``apt-get``) +they will not show up in the list returned by the ``require`` method. However, +the first entry in the list will be the version that ``import gpiozero`` will +import. + +If you receive the error "No module named pkg_resources", you need to install +the ``pip`` utility. This can be done with the following command in Raspbian:: + + $ sudo apt-get install python-pip + diff --git a/docs/recipes.rst b/docs/recipes.rst index ed02720..6ca48d7 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -9,6 +9,8 @@ library. Please note that all recipes are written assuming Python 3. Recipes *may* work under Python 2, but no guarantees! +.. _pin_numbering: + Pin Numbering ============= @@ -18,39 +20,25 @@ configurable. .. _RPi.GPIO: https://pypi.python.org/pypi/RPi.GPIO -Any pin marked ``GPIO`` in the diagram below can be used for generic -components: +Any pin marked "GPIO" in the diagram below can be used as a pin number. For +example, if an LED was attached to "GPIO17" you would specify the pin number as +17 rather than 11: .. image:: images/pin_layout.* + LED === .. image:: images/led.* -Turn an :class:`LED` on and off repeatedly:: +Turn an :class:`LED` on and off repeatedly: - from gpiozero import LED - from time import sleep +.. literalinclude:: examples/led_1.py - red = LED(17) +Alternatively: - while True: - red.on() - sleep(1) - red.off() - sleep(1) - -Alternatively:: - - from gpiozero import LED - from signal import pause - - red = LED(17) - - red.blink() - - pause() +.. literalinclude:: examples/led_2.py .. note:: @@ -59,45 +47,51 @@ Alternatively:: :ref:`keep-your-script-running` for more information. +LED with variable brightness +============================ + +Any regular LED can have its brightness value set using PWM +(pulse-width-modulation). In GPIO Zero, this can be achieved using +:class:`PWMLED` using values between 0 and 1: + +.. literalinclude:: examples/led_variable_brightness.py + +Similarly to blinking on and off continuously, a PWMLED can pulse (fade in and +out continuously): + +.. literalinclude:: examples/led_pulse.py + + Button ====== .. image:: images/button.* -Check if a :class:`Button` is pressed:: +Check if a :class:`Button` is pressed: - from gpiozero import Button +.. literalinclude:: examples/button_1.py - button = Button(2) +Wait for a button to be pressed before continuing: - while True: - if button.is_pressed: - print("Button is pressed") - else: - print("Button is not pressed") +.. literalinclude:: examples/button_2.py -Wait for a button to be pressed before continuing:: +Run a function every time the button is pressed: - from gpiozero import Button +.. literalinclude:: examples/button_3.py + :emphasize-lines: 9 - button = Button(2) +.. note:: - button.wait_for_press() - print("Button was pressed") + Note that the line ``button.when_pressed = say_hello`` does not run the + function ``say_hello``, rather it creates a reference to the function to + be called when the button is pressed. Accidental use of + ``button.when_pressed = say_hello()`` would set the ``when_pressed`` action + to ``None`` (the return value of this function) which would mean nothing + happens when the button is pressed. -Run a function every time the button is pressed:: +Similarly, functions can be attached to button releases: - from gpiozero import Button - from signal import pause - - def say_hello(): - print("Hello!") - - button = Button(2) - - button.when_pressed = say_hello - - pause() +.. literalinclude:: examples/button_4.py Button controlled LED @@ -105,30 +99,71 @@ Button controlled LED .. image:: images/led_button_bb.* -Turn on an :class:`LED` when a :class:`Button` is pressed:: +Turn on an :class:`LED` when a :class:`Button` is pressed: - from gpiozero import LED, Button - from signal import pause +.. literalinclude:: examples/button_led_1.py - led = LED(17) - button = Button(2) +Alternatively: - button.when_pressed = led.on - button.when_released = led.off +.. literalinclude:: examples/button_led_2.py - pause() -Alternatively:: +Button controlled camera +======================== - from gpiozero import LED, Button - from signal import pause +Using the button press to trigger :class:`~picamera.PiCamera` to take a picture +using ``button.when_pressed = camera.capture`` would not work because the +:meth:`~picamera.PiCamera.capture` method requires an ``output`` parameter. +However, this can be achieved using a custom function which requires no +parameters: - led = LED(17) - button = Button(2) +.. literalinclude:: examples/button_camera_1.py + :emphasize-lines: 9-11 - led.source = button.values +Another example could use one button to start and stop the camera preview, and +another to capture: - pause() +.. literalinclude:: examples/button_camera_2.py + + +Shutdown button +=============== + +The :class:`Button` class also provides the ability to run a function when the +button has been held for a given length of time. This example will shut down +the Raspberry Pi when the button is held for 2 seconds: + +.. literalinclude:: examples/button_shutdown.py + + +LEDBoard +======== + +A collection of LEDs can be accessed using :class:`LEDBoard`: + +.. literalinclude:: examples/led_board_1.py + +Using :class:`LEDBoard` with ``pwm=True`` allows each LED's brightness to be +controlled: + +.. literalinclude:: examples/led_board_2.py + + +LEDBarGraph +=========== + +A collection of LEDs can be treated like a bar graph using +:class:`LEDBarGraph`: + +.. literalinclude:: examples/led_bargraph_2.py + +Note values are essentially rounded to account for the fact LEDs can only be on +or off when ``pwm=False`` (the default). + +However, using :class:`LEDBarGraph` with ``pwm=True`` allows more precise +values using LED brightness: + +.. literalinclude:: examples/led_bargraph_2.py Traffic Lights @@ -138,97 +173,37 @@ Traffic Lights A full traffic lights system. -Using a :class:`TrafficLights` kit like Pi-Stop:: +Using a :class:`TrafficLights` kit like Pi-Stop: - from gpiozero import TrafficLights - from time import sleep +.. literalinclude:: examples/traffic_lights_1.py - lights = TrafficLights(2, 3, 4) +Alternatively: - lights.green.on() +.. literalinclude:: examples/traffic_lights_2.py - while True: - sleep(10) - lights.green.off() - lights.amber.on() - sleep(1) - lights.amber.off() - lights.red.on() - sleep(10) - lights.amber.on() - sleep(1) - lights.green.on() - lights.amber.off() - lights.red.off() +Using :class:`LED` components: -Alternatively:: +.. literalinclude:: examples/traffic_lights_3.py - from gpiozero import TrafficLights - from time import sleep - from signal import pause - lights = TrafficLights(2, 3, 4) +Travis build LED indicator +========================== - def traffic_light_sequence(): - while True: - yield (0, 0, 1) # green - sleep(10) - yield (0, 1, 0) # amber - sleep(1) - yield (1, 0, 0) # red - sleep(10) - yield (1, 1, 0) # red+amber - sleep(1) +Use LEDs to indicate the status of a Travis build. A green light means the +tests are passing, a red light means the build is broken: - lights.source = traffic_light_sequence() +.. literalinclude:: examples/led_travis.py - pause() - -Using :class:`LED` components:: - - from gpiozero import LED - from time import sleep - - red = LED(2) - amber = LED(3) - green = LED(4) - - green.on() - amber.off() - red.off() - - while True: - sleep(10) - green.off() - amber.on() - sleep(1) - amber.off() - red.on() - sleep(10) - amber.on() - sleep(1) - green.on() - amber.off() - red.off() +Note this recipe requires `travispy`_. Install with ``sudo pip3 install +travispy``. Push button stop motion ======================= -Capture a picture with the camera module every time a button is pressed:: +Capture a picture with the camera module every time a button is pressed: - from gpiozero import Button - from picamera import PiCamera - - button = Button(2) - - with PiCamera() as camera: - camera.start_preview() - frame = 1 - while True: - button.wait_for_press() - camera.capture('/home/pi/frame%03d.jpg' % frame) - frame += 1 +.. literalinclude:: examples/button_stop_motion.py See `Push Button Stop Motion`_ for a full resource. @@ -240,30 +215,7 @@ Reaction Game When you see the light come on, the first person to press their button wins! -:: - - from gpiozero import Button, LED - from time import sleep - import random - - led = LED(17) - - player_1 = Button(2) - player_2 = Button(3) - - time = random.uniform(5, 10) - sleep(time) - led.on() - - while True: - if player_1.is_pressed: - print("Player 1 wins!") - break - if player_2.is_pressed: - print("Player 2 wins!") - break - - led.off() +.. literalinclude:: examples/reaction_game.py See `Quick Reaction Game`_ for a full resource. @@ -273,26 +225,7 @@ GPIO Music Box Each button plays a different sound! -:: - - from gpiozero import Button - import pygame.mixer - from pygame.mixer import Sound - from signal import pause - - pygame.mixer.init() - - sound_pins = { - 2: Sound("samples/drum_tom_mid_hard.wav"), - 3: Sound("samples/drum_cymbal_open.wav"), - } - - buttons = [Button(pin) for pin in sound_pins] - for button in buttons: - sound = sound_pins[button.pin.number] - button.when_pressed = sound.play - - pause() +.. literalinclude:: examples/music_box.py See `GPIO Music Box`_ for a full resource. @@ -302,92 +235,27 @@ All on when pressed While the button is pressed down, the buzzer and all the lights come on. -:class:`FishDish`:: +:class:`FishDish`: - from gpiozero import FishDish - from signal import pause +.. literalinclude:: examples/all_on_1.py - fish = FishDish() +Ryanteck :class:`TrafficHat`: - fish.button.when_pressed = fish.on - fish.button.when_released = fish.off +.. literalinclude:: examples/all_on_2.py - pause() +Using :class:`LED`, :class:`Buzzer`, and :class:`Button` components: -Ryanteck :class:`TrafficHat`:: - - from gpiozero import TrafficHat - from signal import pause - - th = TrafficHat() - - th.button.when_pressed = th.on - th.button.when_released = th.off - - pause() - -Using :class:`LED`, :class:`Buzzer`, and :class:`Button` components:: - - from gpiozero import LED, Buzzer, Button - from signal import pause - - button = Button(2) - buzzer = Buzzer(3) - red = LED(4) - amber = LED(5) - green = LED(6) - - things = [red, amber, green, buzzer] - - def things_on(): - for thing in things: - thing.on() - - def things_off(): - for thing in things: - thing.off() - - button.when_pressed = things_on - button.when_released = things_off - - pause() +.. literalinclude:: examples/all_on_3.py -RGB LED -======= +Full color LED +============== .. image:: images/rgb_led_bb.* -Making colours with an :class:`RGBLED`:: +Making colours with an :class:`RGBLED`: - from gpiozero import RGBLED - from time import sleep - - led = RGBLED(red=9, green=10, blue=11) - - led.red = 1 # full red - sleep(1) - led.red = 0.5 # half red - sleep(1) - - led.color = (0, 1, 0) # full green - sleep(1) - led.color = (1, 0, 1) # magenta - sleep(1) - led.color = (1, 1, 0) # yellow - sleep(1) - led.color = (0, 1, 1) # cyan - sleep(1) - led.color = (1, 1, 1) # white - sleep(1) - - led.color = (0, 0, 0) # off - sleep(1) - - # slowly increase intensity of blue - for n in range(100): - led.blue = n/100 - sleep(0.1) +.. literalinclude:: examples/rgbled.py Motion sensor @@ -395,18 +263,9 @@ Motion sensor .. image:: images/motion_sensor_bb.* -Light an :class:`LED` when a :class:`MotionSensor` detects motion:: +Light an :class:`LED` when a :class:`MotionSensor` detects motion: - from gpiozero import MotionSensor, LED - from signal import pause - - pir = MotionSensor(4) - led = LED(16) - - pir.when_motion = led.on - pir.when_no_motion = led.off - - pause() +.. literalinclude:: examples/motion_sensor.py Light sensor @@ -414,43 +273,18 @@ Light sensor .. image:: images/light_sensor_bb.* -Have a :class:`LightSensor` detect light and dark:: +Have a :class:`LightSensor` detect light and dark: - from gpiozero import LightSensor +.. literalinclude:: examples/light_sensor_1.py - sensor = LightSensor(18) +Run a function when the light changes: - while True: - sensor.wait_for_light() - print("It's light! :)") - sensor.wait_for_dark() - print("It's dark :(") - -Run a function when the light changes:: - - from gpiozero import LightSensor, LED - from signal import pause - - sensor = LightSensor(18) - led = LED(16) - - sensor.when_dark = led.on - sensor.when_light = led.off - - pause() +.. literalinclude:: examples/light_sensor_2.py Or make a :class:`PWMLED` change brightness according to the detected light -level:: +level: - from gpiozero import LightSensor, LED - from signal import pause - - sensor = LightSensor(18) - led = PWMLED(16) - - led.source = sensor.values - - pause() +.. literalinclude:: examples/light_sensor_3.py Distance sensor @@ -458,29 +292,13 @@ Distance sensor .. IMAGE TBD -Have a :class:`DistanceSensor` detect the distance to the nearest object:: +Have a :class:`DistanceSensor` detect the distance to the nearest object: - from gpiozero import DistanceSensor - from time import sleep +.. literalinclude:: examples/distance_sensor_1.py - sensor = DistanceSensor(23, 24) +Run a function when something gets near the sensor: - while True: - print('Distance to nearest object is', sensor.distance, 'm') - sleep(1) - -Run a function when something gets near the sensor:: - - from gpiozero import DistanceSensor, LED - from signal import pause - - sensor = DistanceSensor(23, 24, max_distance=1, threshold_distance=0.2) - led = LED(16) - - sensor.when_in_range = led.on - sensor.when_out_of_range = led.off - - pause() +.. literalinclude:: examples/distance_sensor_2.py Motors @@ -488,18 +306,9 @@ Motors .. image:: images/motor_bb.* -Spin a :class:`Motor` around forwards and backwards:: +Spin a :class:`Motor` around forwards and backwards: - from gpiozero import Motor - from time import sleep - - motor = Motor(forward=4, back=14) - - while True: - motor.forward() - sleep(5) - motor.backward() - sleep(5) +.. literalinclude:: examples/motor.py Robot @@ -507,163 +316,59 @@ Robot .. IMAGE TBD -Make a :class:`Robot` drive around in (roughly) a square:: +Make a :class:`Robot` drive around in (roughly) a square: - from gpiozero import Robot - from time import sleep - - robot = Robot(left=(4, 14), right=(17, 18)) - - for i in range(4): - robot.forward() - sleep(10) - robot.right() - sleep(1) +.. literalinclude:: examples/robot_1.py Make a robot with a distance sensor that runs away when things get within -20cm of it:: +20cm of it: - from gpiozero import Robot, DistanceSensor - from signal import pause - - sensor = DistanceSensor(23, 24, max_distance=1, threshold_distance=0.2) - robot = Robot(left=(4, 14), right=(17, 18)) - - sensor.when_in_range = robot.backward - sensor.when_out_of_range = robot.stop - pause() +.. literalinclude:: examples/robot_2.py Button controlled robot ======================= -Use four GPIO buttons as forward/back/left/right controls for a robot:: +Use four GPIO buttons as forward/back/left/right controls for a robot: - from gpiozero import RyanteckRobot, Button - from signal import pause - - robot = RyanteckRobot() - - left = Button(26) - right = Button(16) - fw = Button(21) - bw = Button(20) - - fw.when_pressed = robot.forward - fw.when_released = robot.stop - - left.when_pressed = robot.left - left.when_released = robot.stop - - right.when_pressed = robot.right - right.when_released = robot.stop - - bw.when_pressed = robot.backward - bw.when_released = robot.stop - - pause() +.. literalinclude:: examples/robot_buttons.py Keyboard controlled robot ========================= -Use up/down/left/right keys to control a robot:: +Use up/down/left/right keys to control a robot: - import curses - from gpiozero import RyanteckRobot - - robot = RyanteckRobot() - - actions = { - curses.KEY_UP: robot.forward, - curses.KEY_DOWN: robot.backward, - curses.KEY_LEFT: robot.left, - curses.KEY_RIGHT: robot.right, - } - - def main(window): - next_key = None - while True: - curses.halfdelay(1) - if next_key is None: - key = window.getch() - else: - key = next_key - next_key = None - if key != -1: - # KEY DOWN - curses.halfdelay(3) - action = actions.get(key) - if action is not None: - action() - next_key = key - while next_key == key: - next_key = window.getch() - # KEY UP - robot.stop() - - curses.wrapper(main) +.. literalinclude:: examples/robot_keyboard_1.py .. note:: - This recipe uses the ``curses`` module. This module requires that Python is - running in a terminal in order to work correctly, hence this recipe will - *not* work in environments like IDLE. + This recipe uses the standard :mod:`curses` module. This module requires + that Python is running in a terminal in order to work correctly, hence this + recipe will *not* work in environments like IDLE. If you prefer a version that works under IDLE, the following recipe should -suffice, but will require that you install the evdev library with ``sudo pip -install evdev`` first:: +suffice: - from gpiozero import RyanteckRobot - from evdev import InputDevice, list_devices, ecodes +.. literalinclude:: examples/robot_keyboard_2.py - robot = RyanteckRobot() +.. note:: - devices = [InputDevice(device) for device in list_devices()] - keyboard = devices[0] # this may vary - - keypress_actions = { - ecodes.KEY_UP: robot.forward, - ecodes.KEY_DOWN: robot.backward, - ecodes.KEY_LEFT: robot.left, - ecodes.KEY_RIGHT: robot.right, - } - - for event in keyboard.read_loop(): - if event.type == ecodes.EV_KEY: - if event.value == 1: # key down - keypress_actions[event.code]() - if event.value == 0: # key up - robot.stop() + This recipe uses the third-party ``evdev`` module. Install this library + with ``sudo pip3 install evdev`` first. Be aware that ``evdev`` will only + work with local input devices; this recipe will *not* work over SSH. Motion sensor robot =================== -Make a robot drive forward when it detects motion:: +Make a robot drive forward when it detects motion: - from gpiozero import Robot, MotionSensor - from signal import pause +.. literalinclude:: examples/robot_motion_1.py - robot = Robot(left=(4, 14), right=(17, 18)) - pir = MotionSensor(5) +Alternatively: - pir.when_motion = robot.forward - pir.when_no_motion = robot.stop - - pause() - -Alternatively:: - - from gpiozero import Robot, MotionSensor - from signal import pause - - robot = Robot(left=(4, 14), right=(17, 18)) - pir = MotionSensor(5) - - robot.source = zip(pir.values, pir.values) - - pause() +.. literalinclude:: examples/robot_motion_2.py Potentiometer @@ -672,24 +377,14 @@ Potentiometer .. image:: images/potentiometer_bb.* Continually print the value of a potentiometer (values between 0 and 1) -connected to a :class:`MCP3008` analog to digital converter:: +connected to a :class:`MCP3008` analog to digital converter: - from gpiozero import MCP3008 - - while True: - with MCP3008(channel=0) as pot: - print(pot.value) +.. literalinclude:: examples/pot_1.py Present the value of a potentiometer on an LED bar graph using PWM to represent -states that won't "fill" an LED:: +states that won't "fill" an LED: - from gpiozero import LEDBarGraph, MCP3008 - from signal import pause - - graph = LEDBarGraph(5, 6, 13, 19, 26, pwm=True) - pot = MCP3008(channel=0) - graph.source = pot.values - pause() +.. literalinclude:: examples/pot_2.py Measure temperature with an ADC @@ -698,54 +393,24 @@ Measure temperature with an ADC .. IMAGE TBD Wire a TMP36 temperature sensor to the first channel of an :class:`MCP3008` -analog to digital converter:: +analog to digital converter: - from gpiozero import MCP3008 - from time import sleep - - def convert_temp(gen): - for value in gen: - yield (value * 3.3 - 0.5) * 100 - - adc = MCP3008(channel=0) - - for temp in convert_temp(adc.values): - print('The temperature is', temp, 'C') - sleep(1) +.. literalinclude:: examples/thermometer.py Full color LED controlled by 3 potentiometers ============================================= Wire up three potentiometers (for red, green and blue) and use each of their -values to make up the colour of the LED:: +values to make up the colour of the LED: - from gpiozero import RGBLED, MCP3008 - - led = RGBLED(red=2, green=3, blue=4) - red_pot = MCP3008(channel=0) - green_pot = MCP3008(channel=1) - blue_pot = MCP3008(channel=2) - - while True: - led.red = red_pot.value - led.green = green_pot.value - led.blue = blue_pot.value +.. literalinclude:: examples/rgbled_pot_1.py Alternatively, the following example is identical, but uses the -:attr:`~SourceMixin.source` property rather than a :keyword:`while` loop:: +:attr:`~SourceMixin.source` property rather than a :keyword:`while` loop: - from gpiozero import RGBLED, MCP3008 - from signal import pause - - led = RGBLED(2, 3, 4) - red_pot = MCP3008(0) - green_pot = MCP3008(1) - blue_pot = MCP3008(2) - - led.source = zip(red_pot.values, green_pot.values, blue_pot.values) - - pause() +.. literalinclude:: examples/rgbled_pot_2.py + :emphasize-lines: 8 Please note the example above requires Python 3. In Python 2, :func:`zip` doesn't support lazy evaluation so the script will simply hang. @@ -765,17 +430,9 @@ be done from the terminal with the following commands:: $ echo none | sudo tee /sys/class/leds/led0/trigger $ echo gpio | sudo tee /sys/class/leds/led1/trigger -Now you can control the LEDs with gpiozero like so:: +Now you can control the LEDs with gpiozero like so: - from gpiozero import LED - from signal import pause - - power = LED(35) - activity = LED(47) - - activity.blink() - power.blink() - pause() +.. literalinclude:: examples/led_builtin.py To revert the LEDs to their usual purpose you can either reboot your Pi or run the following commands:: @@ -788,7 +445,7 @@ run the following commands:: On the Pi Zero you can control the activity LED with this recipe, but there's no separate power LED to control (it's also worth noting the activity LED is active low, so set ``active_high=False`` when constructing - your LED component. + your LED component). On the original Pi 1 (model A or B), the activity LED can be controlled with GPIO16 (after disabling its trigger as above) but the power LED is @@ -798,6 +455,7 @@ run the following commands:: accessible from gpiozero (yet). +.. _travispy: https://travispy.readthedocs.io/ .. _Push Button Stop Motion: https://www.raspberrypi.org/learning/quick-reaction-game/ .. _Quick Reaction Game: https://www.raspberrypi.org/learning/quick-reaction-game/ .. _GPIO Music Box: https://www.raspberrypi.org/learning/gpio-music-box/ diff --git a/gpiozero/__init__.py b/gpiozero/__init__.py index 63191c1..925a709 100644 --- a/gpiozero/__init__.py +++ b/gpiozero/__init__.py @@ -7,6 +7,7 @@ from __future__ import ( from .pins import ( Pin, + LocalPin, ) from .pins.data import ( PiBoardInfo, @@ -99,13 +100,17 @@ from .output_devices import ( LED, Buzzer, Motor, + Servo, + AngularServo, RGBLED, ) from .boards import ( CompositeOutputDevice, + ButtonBoard, LEDCollection, LEDBoard, LEDBarGraph, + LedBorg, PiLiter, PiLiterBarGraph, TrafficLights, @@ -122,5 +127,6 @@ from .boards import ( from .other_devices import ( InternalDevice, PingServer, + CPUTemperature, TimeOfDay, ) diff --git a/gpiozero/boards.py b/gpiozero/boards.py index 904f137..ec8ccbe 100644 --- a/gpiozero/boards.py +++ b/gpiozero/boards.py @@ -12,18 +12,27 @@ except ImportError: from time import sleep from itertools import repeat, cycle, chain from threading import Lock +from collections import OrderedDict from .exc import ( DeviceClosed, GPIOPinMissing, EnergenieSocketMissing, EnergenieBadSocket, + OutputDeviceBadValue, ) from .input_devices import Button -from .output_devices import OutputDevice, LED, PWMLED, Buzzer, Motor +from .output_devices import ( + OutputDevice, + LED, + PWMLED, + RGBLED, + Buzzer, + Motor, + ) from .threads import GPIOThread from .devices import Device, CompositeDevice -from .mixins import SharedMixin, SourceMixin +from .mixins import SharedMixin, SourceMixin, HoldMixin class CompositeOutputDevice(SourceMixin, CompositeDevice): @@ -44,7 +53,7 @@ class CompositeOutputDevice(SourceMixin, CompositeDevice): """ Turn all the output devices on. """ - for device in self.all: + for device in self: if isinstance(device, (OutputDevice, CompositeOutputDevice)): device.on() @@ -52,7 +61,7 @@ class CompositeOutputDevice(SourceMixin, CompositeDevice): """ Turn all the output devices off. """ - for device in self.all: + for device in self: if isinstance(device, (OutputDevice, CompositeOutputDevice)): device.off() @@ -61,7 +70,7 @@ class CompositeOutputDevice(SourceMixin, CompositeDevice): Toggle all the output devices. For each device, if it's on, turn it off; if it's off, turn it on. """ - for device in self.all: + for device in self: if isinstance(device, (OutputDevice, CompositeOutputDevice)): device.toggle() @@ -75,18 +84,126 @@ class CompositeOutputDevice(SourceMixin, CompositeDevice): @value.setter def value(self, value): - for device, v in zip(self.all, value): + for device, v in zip(self, value): if isinstance(device, (OutputDevice, CompositeOutputDevice)): device.value = v # Simply ignore values for non-output devices +class ButtonBoard(HoldMixin, CompositeDevice): + """ + Extends :class:`CompositeDevice` and represents a generic button board or + collection of buttons. + + :param int \*pins: + Specify the GPIO pins that the buttons of the board are attached to. + You can designate as many pins as necessary. + + :param bool pull_up: + If ``True`` (the default), the GPIO pins will be pulled high by + default. In this case, connect the other side of the buttons to + ground. If ``False``, the GPIO pins will be pulled low by default. In + this case, connect the other side of the buttons to 3V3. This + parameter can only be specified as a keyword parameter. + + :param float bounce_time: + If ``None`` (the default), no software bounce compensation will be + performed. Otherwise, this is the length of time (in seconds) that the + buttons will ignore changes in state after an initial change. This + parameter can only be specified as a keyword parameter. + + :param float hold_time: + The length of time (in seconds) to wait after any button is pushed, + until executing the :attr:`when_held` handler. Defaults to ``1``. This + parameter can only be specified as a keyword parameter. + + :param bool hold_repeat: + If ``True``, the :attr:`when_held` handler will be repeatedly executed + as long as any buttons remain held, every *hold_time* seconds. If + ``False`` (the default) the :attr:`when_held` handler will be only be + executed once per hold. This parameter can only be specified as a + keyword parameter. + + :param \*\*named_pins: + Specify GPIO pins that buttons of the board are attached to, + associating each button with a property name. You can designate as + many pins as necessary and use any names, provided they're not already + in use by something else. + """ + def __init__(self, *args, **kwargs): + pull_up = kwargs.pop('pull_up', True) + bounce_time = kwargs.pop('bounce_time', None) + hold_time = kwargs.pop('hold_time', 1) + hold_repeat = kwargs.pop('hold_repeat', False) + order = kwargs.pop('_order', None) + super(ButtonBoard, self).__init__( + *( + Button(pin, pull_up, bounce_time, hold_time, hold_repeat) + for pin in args + ), + _order=order, + **{ + name: Button(pin, pull_up, bounce_time, hold_time, hold_repeat) + for name, pin in kwargs.items() + }) + def get_new_handler(device): + def fire_both_events(): + device._fire_events() + self._fire_events() + return fire_both_events + for button in self: + button.pin.when_changed = get_new_handler(button) + self._when_changed = None + self._last_value = None + # Call _fire_events once to set initial state of events + self._fire_events() + self.hold_time = hold_time + self.hold_repeat = hold_repeat + + @property + def pull_up(self): + """ + If ``True``, the device uses a pull-up resistor to set the GPIO pin + "high" by default. + """ + return self[0].pull_up + + @property + def when_changed(self): + return self._when_changed + + @when_changed.setter + def when_changed(self, value): + self._when_changed = self._wrap_callback(value) + + def _fire_changed(self): + if self.when_changed: + self.when_changed() + + def _fire_events(self): + super(ButtonBoard, self)._fire_events() + old_value = self._last_value + new_value = self._last_value = self.value + if old_value is None: + # Initial "indeterminate" value; don't do anything + pass + elif old_value != new_value: + self._fire_changed() + + +ButtonBoard.is_pressed = ButtonBoard.is_active +ButtonBoard.pressed_time = ButtonBoard.active_time +ButtonBoard.when_pressed = ButtonBoard.when_activated +ButtonBoard.when_released = ButtonBoard.when_deactivated +ButtonBoard.wait_for_press = ButtonBoard.wait_for_active +ButtonBoard.wait_for_release = ButtonBoard.wait_for_inactive + + class LEDCollection(CompositeOutputDevice): """ Extends :class:`CompositeOutputDevice`. Abstract base class for :class:`LEDBoard` and :class:`LEDBarGraph`. """ - def __init__(self, *args, **kwargs): self._blink_thread = None pwm = kwargs.pop('pwm', False) @@ -108,19 +225,26 @@ class LEDCollection(CompositeOutputDevice): LEDClass(pin_or_collection, active_high, initial_value) for name, pin_or_collection in kwargs.items() }) + leds = [] + for item in self: + if isinstance(item, LEDCollection): + for subitem in item.leds: + leds.append(subitem) + else: + leds.append(item) + self._leds = tuple(leds) @property def leds(self): """ - A flat iterator over all LEDs contained in this collection (and all + A flat tuple of all LEDs contained in this collection (and all sub-collections). """ - for item in self: - if isinstance(item, LEDCollection): - for subitem in item.leds: - yield subitem - else: - yield item + return self._leds + + @property + def active_high(self): + return self[0].active_high class LEDBoard(LEDCollection): @@ -148,21 +272,23 @@ class LEDBoard(LEDCollection): :param bool active_high: If ``True`` (the default), the :meth:`on` method will set all the - associates pins to HIGH. If ``False``, the :meth:`on` method will set - all pins to LOW (the :meth:`off` method always does the opposite). + associated pins to HIGH. If ``False``, the :meth:`on` method will set + all pins to LOW (the :meth:`off` method always does the opposite). This + parameter can only be specified as a keyword parameter. :param bool initial_value: If ``False`` (the default), all LEDs will be off initially. If ``None``, each device will be left in whatever state the pin is found in when configured for output (warning: this can be on). If ``True``, - the device will be switched on initially. + the device will be switched on initially. This parameter can only be + specified as a keyword parameter. :param \*\*named_pins: - Sepcify GPIO pins that LEDs of the board are attached to, associated + Specify GPIO pins that LEDs of the board are attached to, associating each LED with a property name. You can designate as many pins as - necessary and any name provided it's not already in use by something - else. You can also specify :class:`LEDBoard` instances to create - trees of LEDs. + necessary and use any names, provided they're not already in use by + something else. You can also specify :class:`LEDBoard` instances to + create trees of LEDs. """ def __init__(self, *args, **kwargs): self._blink_leds = [] @@ -345,15 +471,21 @@ class LEDBarGraph(LEDCollection): Specify the GPIO pins that the LEDs of the bar graph are attached to. You can designate as many pins as necessary. - :param float initial_value: - The initial :attr:`value` of the graph given as a float between -1 and - +1. Defaults to 0.0. This parameter can only be specified as a keyword - parameter. - :param bool pwm: If ``True``, construct :class:`PWMLED` instances for each pin. If ``False`` (the default), construct regular :class:`LED` instances. This parameter can only be specified as a keyword parameter. + + :param bool active_high: + If ``True`` (the default), the :meth:`on` method will set all the + associated pins to HIGH. If ``False``, the :meth:`on` method will set + all pins to LOW (the :meth:`off` method always does the opposite). This + parameter can only be specified as a keyword parameter. + + :param float initial_value: + The initial :attr:`value` of the graph given as a float between -1 and + +1. Defaults to ``0.0``. This parameter can only be specified as a + keyword parameter. """ def __init__(self, *pins, **kwargs): @@ -361,10 +493,11 @@ class LEDBarGraph(LEDCollection): for pin in pins: assert not isinstance(pin, LEDCollection) pwm = kwargs.pop('pwm', False) - initial_value = kwargs.pop('initial_value', 0) + active_high = kwargs.pop('active_high', True) + initial_value = kwargs.pop('initial_value', 0.0) if kwargs: raise TypeError('unexpected keyword argument: %s' % kwargs.popitem()[0]) - super(LEDBarGraph, self).__init__(*pins, pwm=pwm) + super(LEDBarGraph, self).__init__(*pins, pwm=pwm, active_high=active_high) try: self.value = initial_value except: @@ -402,6 +535,8 @@ class LEDBarGraph(LEDCollection): @value.setter def value(self, value): + if not -1 <= value <= 1: + raise OutputDeviceBadValue('LEDBarGraph value must be between -1 and 1') count = len(self) leds = self if value < 0: @@ -415,6 +550,36 @@ class LEDBarGraph(LEDCollection): led.value = calc_value(index) +class LedBorg(RGBLED): + """ + Extends :class:`RGBLED` for the `PiBorg LedBorg`_: an add-on board + containing a very bright RGB LED. + + The LedBorg pins are fixed and therefore there's no need to specify them + when constructing this class. The following example turns the LedBorg + purple:: + + from gpiozero import LedBorg + + led = LedBorg() + led.color = (1, 0, 1) + + :param tuple initial_value: + The initial color for the LedBorg. Defaults to black ``(0, 0, 0)``. + + :param bool pwm: + If ``True`` (the default), construct :class:`PWMLED` instances for + each component of the LedBorg. If ``False``, construct regular + :class:`LED` instances, which prevents smooth color graduations. + + .. _PiBorg LedBorg: https://www.piborg.org/ledborg + """ + + def __init__(self, initial_value=(0, 0, 0), pwm=True): + super(LedBorg, self).__init__(red=17, green=27, blue=22, + pwm=pwm, initial_value=initial_value) + + class PiLiter(LEDBoard): """ Extends :class:`LEDBoard` for the `Ciseco Pi-LITEr`_: a strip of 8 very bright @@ -431,14 +596,20 @@ class PiLiter(LEDBoard): :param bool pwm: If ``True``, construct :class:`PWMLED` instances for each pin. If - ``False`` (the default), construct regular :class:`LED` instances. This - parameter can only be specified as a keyword parameter. + ``False`` (the default), construct regular :class:`LED` instances. + + :param bool initial_value: + If ``False`` (the default), all LEDs will be off initially. If + ``None``, each device will be left in whatever state the pin is found + in when configured for output (warning: this can be on). If ``True``, + the device will be switched on initially. .. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/ """ - def __init__(self, pwm=False): - super(PiLiter, self).__init__(4, 17, 27, 18, 22, 23, 24, 25, pwm=pwm) + def __init__(self, pwm=False, initial_value=False): + super(PiLiter, self).__init__(4, 17, 27, 18, 22, 23, 24, 25, + pwm=pwm, initial_value=initial_value) class PiLiterBarGraph(LEDBarGraph): @@ -455,25 +626,30 @@ class PiLiterBarGraph(LEDBarGraph): graph = PiLiterBarGraph() graph.value = 0.5 - :param bool initial_value: - The initial value of the graph given as a float between -1 and +1. - Defaults to 0.0. + :param bool pwm: + If ``True``, construct :class:`PWMLED` instances for each pin. If + ``False`` (the default), construct regular :class:`LED` instances. + + :param float initial_value: + The initial :attr:`value` of the graph given as a float between -1 and + +1. Defaults to ``0.0``. .. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/ """ - def __init__(self, initial_value=0): - super(PiLiterBarGraph, self).__init__( - 4, 17, 27, 18, 22, 23, 24, 25, initial_value=initial_value) + def __init__(self, pwm=False, initial_value=0.0): + pins = (4, 17, 27, 18, 22, 23, 24, 25) + super(PiLiterBarGraph, self).__init__(*pins, + pwm=pwm, initial_value=initial_value) class TrafficLights(LEDBoard): """ - Extends :class:`LEDBoard` for devices containing red, amber, and green + Extends :class:`LEDBoard` for devices containing red, yellow, and green LEDs. The following example initializes a device connected to GPIO pins 2, 3, - and 4, then lights the amber LED attached to GPIO 3:: + and 4, then lights the amber (yellow) LED attached to GPIO 3:: from gpiozero import TrafficLights @@ -493,15 +669,53 @@ class TrafficLights(LEDBoard): If ``True``, construct :class:`PWMLED` instances to represent each LED. If ``False`` (the default), construct regular :class:`LED` instances. + + :param bool initial_value: + If ``False`` (the default), all LEDs will be off initially. If + ``None``, each device will be left in whatever state the pin is found + in when configured for output (warning: this can be on). If ``True``, + the device will be switched on initially. + + :param int yellow: + The GPIO pin that the yellow LED is attached to. This is merely an + alias for the ``amber`` parameter - you can't specify both ``amber`` + and ``yellow``. """ - def __init__(self, red=None, amber=None, green=None, pwm=False): - if not all([red, amber, green]): + def __init__(self, red=None, amber=None, green=None, + pwm=False, initial_value=False, yellow=None): + if amber is not None and yellow is not None: + raise OutputDeviceBadValue( + 'Only one of amber or yellow can be specified' + ) + devices = OrderedDict((('red', red), )) + self._display_yellow = amber is None and yellow is not None + if self._display_yellow: + devices['yellow'] = yellow + else: + devices['amber'] = amber + devices['green'] = green + if not all(p is not None for p in devices.values()): raise GPIOPinMissing( - 'red, amber and green pins must be provided' + ', '.join(devices.keys())+' pins must be provided' ) super(TrafficLights, self).__init__( - red=red, amber=amber, green=green, pwm=pwm, - _order=('red', 'amber', 'green')) + pwm=pwm, initial_value=initial_value, + _order=devices.keys(), + **devices) + + def __getattr__(self, name): + if name == 'amber' and self._display_yellow: + name = 'yellow' + elif name == 'yellow' and not self._display_yellow: + name = 'amber' + return super(TrafficLights, self).__getattr__(name) + + def __setattr__(self, name, value): + if name == 'amber' and self._display_yellow: + name = 'yellow' + elif name == 'yellow' and not self._display_yellow: + name = 'amber' + return super(TrafficLights, self).__setattr__(name, value) class PiTraffic(TrafficLights): @@ -521,11 +735,22 @@ class PiTraffic(TrafficLights): To use the PI-TRAFFIC board when attached to a non-standard set of pins, simply use the parent class, :class:`TrafficLights`. + :param bool pwm: + If ``True``, construct :class:`PWMLED` instances to represent each + LED. If ``False`` (the default), construct regular :class:`LED` + instances. + + :param bool initial_value: + If ``False`` (the default), all LEDs will be off initially. If + ``None``, each device will be left in whatever state the pin is found + in when configured for output (warning: this can be on). If ``True``, + the device will be switched on initially. + .. _Low Voltage Labs PI-TRAFFIC: http://lowvoltagelabs.com/products/pi-traffic/ """ - - def __init__(self): - super(PiTraffic, self).__init__(9, 10, 11) + def __init__(self, pwm=False, initial_value=False): + super(PiTraffic, self).__init__(9, 10, 11, + pwm=pwm, initial_value=initial_value) class SnowPi(LEDBoard): @@ -548,31 +773,41 @@ class SnowPi(LEDBoard): LED. If ``False`` (the default), construct regular :class:`LED` instances. + :param bool initial_value: + If ``False`` (the default), all LEDs will be off initially. If + ``None``, each device will be left in whatever state the pin is found + in when configured for output (warning: this can be on). If ``True``, + the device will be switched on initially. + .. _Ryanteck SnowPi: https://ryanteck.uk/raspberry-pi/114-snowpi-the-gpio-snowman-for-raspberry-pi-0635648608303.html """ - def __init__(self, pwm=False): + def __init__(self, pwm=False, initial_value=False): super(SnowPi, self).__init__( arms=LEDBoard( left=LEDBoard( - top=17, middle=18, bottom=22, pwm=pwm, + top=17, middle=18, bottom=22, + pwm=pwm, initial_value=initial_value, _order=('top', 'middle', 'bottom')), right=LEDBoard( - top=7, middle=8, bottom=9, pwm=pwm, + top=7, middle=8, bottom=9, + pwm=pwm, initial_value=initial_value, _order=('top', 'middle', 'bottom')), _order=('left', 'right') ), eyes=LEDBoard( - left=23, right=24, pwm=pwm, + left=23, right=24, + pwm=pwm, initial_value=initial_value, _order=('left', 'right') ), - nose=25, pwm=pwm, + nose=25, + pwm=pwm, initial_value=initial_value, _order=('eyes', 'nose', 'arms') ) class TrafficLightsBuzzer(CompositeOutputDevice): """ - Extends :class:`CompositeDevice` and is a generic class for HATs with + Extends :class:`CompositeOutputDevice` and is a generic class for HATs with traffic lights, a button and a buzzer. :param TrafficLights lights: @@ -594,7 +829,7 @@ class TrafficLightsBuzzer(CompositeOutputDevice): class FishDish(TrafficLightsBuzzer): """ - Extends :class:`TrafficLightsBuzzer` for the Pi Supply FishDish: traffic + Extends :class:`TrafficLightsBuzzer` for the `Pi Supply FishDish`_: traffic light LEDs, a button and a buzzer. The FishDish pins are fixed and therefore there's no need to specify them @@ -611,6 +846,8 @@ class FishDish(TrafficLightsBuzzer): If ``True``, construct :class:`PWMLED` instances to represent each LED. If ``False`` (the default), construct regular :class:`LED` instances. + + .. _Pi Supply FishDish: https://www.pi-supply.com/product/fish-dish-raspberry-pi-led-buzzer-board/ """ def __init__(self, pwm=False): @@ -623,7 +860,7 @@ class FishDish(TrafficLightsBuzzer): class TrafficHat(TrafficLightsBuzzer): """ - Extends :class:`TrafficLightsBuzzer` for the Ryanteck Traffic HAT: traffic + Extends :class:`TrafficLightsBuzzer` for the `Ryanteck Traffic HAT`_: traffic light LEDs, a button and a buzzer. The Traffic HAT pins are fixed and therefore there's no need to specify @@ -640,6 +877,8 @@ class TrafficHat(TrafficLightsBuzzer): If ``True``, construct :class:`PWMLED` instances to represent each LED. If ``False`` (the default), construct regular :class:`LED` instances. + + .. _Ryanteck Traffic HAT: https://ryanteck.uk/hats/1-traffichat-0635648607122.html """ def __init__(self, pwm=False): @@ -658,12 +897,12 @@ class Robot(SourceMixin, CompositeDevice): backward pins of the left and right controllers respectively. For example, if the left motor's controller is connected to GPIOs 4 and 14, while the right motor's controller is connected to GPIOs 17 and 18 then the following - example will turn the robot left:: + example will drive the robot forward:: from gpiozero import Robot robot = Robot(left=(4, 14), right=(17, 18)) - robot.left() + robot.forward() :param tuple left: A tuple of two GPIO pins representing the forward and backward inputs @@ -680,6 +919,20 @@ class Robot(SourceMixin, CompositeDevice): right_motor=Motor(*right), _order=('left_motor', 'right_motor')) + @property + def value(self): + """ + Represents the motion of the robot as a tuple of (left_motor_speed, + right_motor_speed) with ``(-1, -1)`` representing full speed backwards, + ``(1, 1)`` representing full speed forwards, and ``(0, 0)`` + representing stopped. + """ + return super(Robot, self).value + + @value.setter + def value(self, value): + self.left_motor.value, self.right_motor.value = value + def forward(self, speed=1): """ Drive the robot forward by running both motors forward. @@ -746,16 +999,18 @@ class Robot(SourceMixin, CompositeDevice): class RyanteckRobot(Robot): """ - Extends :class:`Robot` for the Ryanteck MCB robot. + Extends :class:`Robot` for the `Ryanteck MCB`_ robot. The Ryanteck MCB pins are fixed and therefore there's no need to specify - them when constructing this class. The following example turns the robot - left:: + them when constructing this class. The following example drives the robot + forward:: from gpiozero import RyanteckRobot robot = RyanteckRobot() - robot.left() + robot.forward() + + .. _Ryanteck MCB: https://ryanteck.uk/add-ons/6-ryanteck-rpi-motor-controller-board-0635648607160.html """ def __init__(self): @@ -767,13 +1022,13 @@ class CamJamKitRobot(Robot): Extends :class:`Robot` for the `CamJam #3 EduKit`_ robot controller. The CamJam robot controller pins are fixed and therefore there's no need - to specify them when constructing this class. The following example turns - the robot left:: + to specify them when constructing this class. The following example drives + the robot forward:: from gpiozero import CamJamKitRobot robot = CamJamKitRobot() - robot.left() + robot.forward() .. _CamJam #3 EduKit: http://camjam.me/?page_id=1035 """ @@ -805,7 +1060,7 @@ class _EnergenieMaster(SharedMixin, CompositeOutputDevice): with self._lock: try: code = (8 * bool(enable)) + (8 - socket) - for bit in self.all[:4]: + for bit in self[:4]: bit.value = (code & 1) code >>= 1 sleep(0.1) @@ -846,6 +1101,7 @@ class Energenie(SourceMixin, Device): raise EnergenieSocketMissing('socket number must be provided') if not (1 <= socket <= 4): raise EnergenieBadSocket('socket number must be between 1 and 4') + self._value = None super(Energenie, self).__init__() self._socket = socket self._master = _EnergenieMaster() @@ -877,8 +1133,9 @@ class Energenie(SourceMixin, Device): @value.setter def value(self, value): - self._master.transmit(self._socket, bool(value)) - self._value = bool(value) + value = bool(value) + self._master.transmit(self._socket, value) + self._value = value def on(self): self.value = True diff --git a/gpiozero/compat.py b/gpiozero/compat.py index 8387038..31e5f35 100644 --- a/gpiozero/compat.py +++ b/gpiozero/compat.py @@ -9,6 +9,9 @@ from __future__ import ( str = type('') import cmath +import collections +import operator +import functools # Back-ported from python 3.5; see @@ -51,3 +54,30 @@ def median(data): i = n // 2 return (data[i - 1] + data[i]) / 2 + +# Copied from the MIT-licensed https://github.com/slezica/python-frozendict +class frozendict(collections.Mapping): + def __init__(self, *args, **kwargs): + self.__dict = dict(*args, **kwargs) + self.__hash = None + + def __getitem__(self, key): + return self.__dict[key] + + def copy(self, **add_or_replace): + return frozendict(self, **add_or_replace) + + def __iter__(self): + return iter(self.__dict) + + def __len__(self): + return len(self.__dict) + + def __repr__(self): + return '' % repr(self.__dict) + + def __hash__(self): + if self.__hash is None: + hashes = map(hash, self.items()) + self.__hash = functools.reduce(operator.xor, hashes, 0) + return self.__hash diff --git a/gpiozero/devices.py b/gpiozero/devices.py index 7aa5e32..b5a2648 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -7,6 +7,7 @@ from __future__ import ( nstr = str str = type('') +import os import atexit import weakref from collections import namedtuple @@ -14,12 +15,16 @@ from itertools import chain from types import FunctionType from threading import RLock +import pkg_resources + from .threads import _threads_shutdown +from .pins import _pins_shutdown from .mixins import ( ValuesMixin, SharedMixin, ) from .exc import ( + BadPinFactory, DeviceClosed, CompositeDeviceBadName, CompositeDeviceBadOrder, @@ -28,26 +33,34 @@ from .exc import ( GPIOPinInUse, GPIODeviceClosed, ) +from .compat import frozendict -# Get a pin implementation to use as the default; we prefer RPi.GPIO's here -# as it supports PWM, and all Pi revisions. If no third-party libraries are -# available, however, we fall back to a pure Python implementation which -# supports platforms like PyPy -from .pins import _pins_shutdown -try: - from .pins.rpigpio import RPiGPIOPin - DefaultPin = RPiGPIOPin -except ImportError: - try: - from .pins.rpio import RPIOPin - DefaultPin = RPIOPin - except ImportError: - try: - from .pins.pigipod import PiGPIOPin - DefaultPin = PiGPIOPin - except ImportError: - from .pins.native import NativePin - DefaultPin = NativePin + +def _default_pin_factory(name=os.getenv('GPIOZERO_PIN_FACTORY', None)): + group = 'gpiozero_pin_factories' + if name is None: + # If no factory is explicitly specified, try various names in + # "preferred" order. Note that in this case we only select from + # gpiozero distribution so without explicitly specifying a name (via + # the environment) it's impossible to auto-select a factory from + # outside the base distribution + # + # We prefer RPi.GPIO here as it supports PWM, and all Pi revisions. If + # no third-party libraries are available, however, we fall back to a + # pure Python implementation which supports platforms like PyPy + dist = pkg_resources.get_distribution('gpiozero') + for name in ('RPiGPIOPin', 'RPIOPin', 'PiGPIOPin', 'NativePin'): + try: + return pkg_resources.load_entry_point(dist, group, name) + except ImportError: + pass + raise BadPinFactory('Unable to locate any default pin factory!') + else: + for factory in pkg_resources.iter_entry_points(group, name): + return factory.load() + raise BadPinFactory('Unable to locate pin factory "%s"' % name) + +pin_factory = _default_pin_factory() _PINS = set() @@ -263,14 +276,15 @@ class CompositeDevice(Device): """ def __init__(self, *args, **kwargs): self._all = () - self._named = {} + self._named = frozendict({}) self._namedtuple = None self._order = kwargs.pop('_order', None) if self._order is None: self._order = sorted(kwargs.keys()) + else: + for missing_name in set(kwargs.keys()) - set(self._order): + raise CompositeDeviceBadOrder('%s missing from _order' % missing_name) self._order = tuple(self._order) - for missing_name in set(kwargs.keys()) - set(self._order): - raise CompositeDeviceBadOrder('%s missing from _order' % missing_name) super(CompositeDevice, self).__init__() for name in set(self._order) & set(dir(self)): raise CompositeDeviceBadName('%s is a reserved name' % name) @@ -278,7 +292,7 @@ class CompositeDevice(Device): for dev in self._all: if not isinstance(dev, Device): raise CompositeDeviceBadDevice("%s doesn't inherit from Device" % dev) - self._named = kwargs + self._named = frozendict(kwargs) self._namedtuple = namedtuple('%sValue' % self.__class__.__name__, chain( (str(i) for i in range(len(args))), self._order), rename=True) @@ -286,7 +300,7 @@ class CompositeDevice(Device): def __getattr__(self, name): # if _named doesn't exist yet, pretend it's an empty dict if name == '_named': - return {} + return frozendict({}) try: return self._named[name] except KeyError: @@ -303,7 +317,7 @@ class CompositeDevice(Device): self._check_open() return "" % ( self.__class__.__name__, - len(self), ','.join(self._named), + len(self), ','.join(self._order), len(self) - len(self._named) ) except DeviceClosed: @@ -365,7 +379,7 @@ class GPIODevice(Device): if pin is None: raise GPIOPinMissing('No pin given') if isinstance(pin, int): - pin = DefaultPin(pin) + pin = pin_factory(pin) with _PINS_LOCK: if pin in _PINS: raise GPIOPinInUse( @@ -376,9 +390,12 @@ class GPIODevice(Device): self._active_state = True self._inactive_state = False + def _state_to_value(self, state): + return bool(state == self._active_state) + def _read(self): try: - return self.pin.state == self._active_state + return self._state_to_value(self.pin.state) except (AttributeError, TypeError): self._check_open() raise diff --git a/gpiozero/exc.py b/gpiozero/exc.py index 3e8ed50..1afd214 100644 --- a/gpiozero/exc.py +++ b/gpiozero/exc.py @@ -22,6 +22,9 @@ class BadWaitTime(GPIOZeroError, ValueError): class BadQueueLen(GPIOZeroError, ValueError): "Error raised when non-positive queue length is specified" +class BadPinFactory(GPIOZeroError, ImportError): + "Error raised when an unknown pin factory name is specified" + class CompositeDeviceError(GPIOZeroError): "Base class for errors specific to the CompositeDevice hierarchy" diff --git a/gpiozero/input_devices.py b/gpiozero/input_devices.py index 3807d84..8a4e592 100644 --- a/gpiozero/input_devices.py +++ b/gpiozero/input_devices.py @@ -50,7 +50,7 @@ class InputDevice(GPIODevice): def pull_up(self): """ If ``True``, the device uses a pull-up resistor to set the GPIO pin - "high" by default. Defaults to ``False``. + "high" by default. """ return self.pin.pull == 'up' @@ -71,7 +71,7 @@ class DigitalInputDevice(EventsMixin, InputDevice): straight forward on / off states with (reasonably) clean transitions between the two. - :param float bouncetime: + :param float bounce_time: Specifies the length of time (in seconds) that the component will ignore changes in state after an initial change. This defaults to ``None`` which indicates that no bounce compensation will be performed. @@ -162,10 +162,10 @@ class SmoothedInputDevice(EventsMixin, InputDevice): except DeviceClosed: return super(SmoothedInputDevice, self).__repr__() else: - if self.partial or self._queue.full.wait(0): + if self.partial or self._queue.full.is_set(): return super(SmoothedInputDevice, self).__repr__() else: - return "" % ( + return "" % ( self.__class__.__name__, self.pin, self.pull_up) @property @@ -240,8 +240,8 @@ class Button(HoldMixin, DigitalInputDevice): print("The button was pressed!") :param int pin: - The GPIO pin which the button is attached to. See :doc:`notes` for - valid pin numbers. + The GPIO pin which the button is attached to. See :ref:`pin_numbering` + for valid pin numbers. :param bool pull_up: If ``True`` (the default), the GPIO pin will be pulled high by default. @@ -251,16 +251,18 @@ class Button(HoldMixin, DigitalInputDevice): :param float bounce_time: 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 of time (in seconds) that the component will ignore changes in state after an initial change. :param float hold_time: The length of time (in seconds) to wait after the button is pushed, - until executing the :attr:`when_held` handler. + until executing the :attr:`when_held` handler. Defaults to ``1``. :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. + as long as the device remains active, every *hold_time* seconds. If + ``False`` (the default) the :attr:`when_held` handler will be only be + executed once per hold. """ def __init__( self, pin=None, pull_up=True, bounce_time=None, @@ -279,7 +281,7 @@ Button.wait_for_release = Button.wait_for_inactive class LineSensor(SmoothedInputDevice): """ - Extends :class:`DigitalInputDevice` and represents a single pin line sensor + Extends :class:`SmoothedInputDevice` and represents a single pin line sensor like the TCRT5000 infra-red proximity sensor found in the `CamJam #3 EduKit`_. @@ -300,8 +302,8 @@ class LineSensor(SmoothedInputDevice): pause() :param int pin: - The GPIO pin which the button is attached to. See :doc:`notes` for - valid pin numbers. + The GPIO pin which the sensor is attached to. See :ref:`pin_numbering` + for valid pin numbers. :param int queue_len: The length of the queue used to store values read from the sensor. This @@ -369,8 +371,8 @@ class MotionSensor(SmoothedInputDevice): print("Motion detected!") :param int pin: - The GPIO pin which the button is attached to. See :doc:`notes` for - valid pin numbers. + The GPIO pin which the sensor is attached to. See :ref:`pin_numbering` + for valid pin numbers. :param int queue_len: The length of the queue used to store values read from the sensor. This @@ -418,7 +420,7 @@ class LightSensor(SmoothedInputDevice): Extends :class:`SmoothedInputDevice` and represents a light dependent resistor (LDR). - Connect one leg of the LDR to the 3V3 pin; connect one leg of a 1µf + Connect one leg of the LDR to the 3V3 pin; connect one leg of a 1µF capacitor to a ground pin; connect the other leg of the LDR and the other leg of the capacitor to the same GPIO pin. This class repeatedly discharges the capacitor, then times the duration it takes to charge (which will vary @@ -433,8 +435,8 @@ class LightSensor(SmoothedInputDevice): print("Light detected!") :param int pin: - The GPIO pin which the button is attached to. See :doc:`notes` for - valid pin numbers. + The GPIO pin which the sensor is attached to. See :ref:`pin_numbering` + for valid pin numbers. :param int queue_len: The length of the queue used to store values read from the circuit. @@ -443,7 +445,7 @@ class LightSensor(SmoothedInputDevice): :param float charge_time_limit: If the capacitor in the circuit takes longer than this length of time to charge, it is assumed to be dark. The default (0.01 seconds) is - appropriate for a 0.01µf capacitor coupled with the LDR from the + appropriate for a 1µF capacitor coupled with the LDR from the `CamJam #2 EduKit`_. You may need to adjust this value for different valued capacitors or LDRs. @@ -534,18 +536,18 @@ class DistanceSensor(SmoothedInputDevice): from gpiozero import DistanceSensor from time import sleep - sensor = DistanceSensor(18, 17) + sensor = DistanceSensor(echo=18, trigger=17) while True: print('Distance: ', sensor.distance * 100) sleep(1) :param int echo: - The GPIO pin which the ECHO pin is attached to. See :doc:`notes` for - valid pin numbers. + The GPIO pin which the ECHO pin is attached to. See + :ref:`pin_numbering` for valid pin numbers. :param int trigger: - The GPIO pin which the TRIG pin is attached to. See :doc:`notes` for - valid pin numbers. + The GPIO pin which the TRIG pin is attached to. See + :ref:`pin_numbering` for valid pin numbers. :param int queue_len: The length of the queue used to store values read from the sensor. @@ -583,11 +585,19 @@ class DistanceSensor(SmoothedInputDevice): self._max_distance = max_distance self._trigger = GPIODevice(trigger) self._echo = Event() + self._echo_rise = None + self._echo_fall = None self._trigger.pin.function = 'output' self._trigger.pin.state = False self.pin.edges = 'both' self.pin.bounce = None - self.pin.when_changed = self._echo.set + def callback(): + if self._echo_rise is None: + self._echo_rise = time() + else: + self._echo_fall = time() + self._echo.set() + self.pin.when_changed = callback self._queue.start() except: self.close() @@ -615,7 +625,7 @@ class DistanceSensor(SmoothedInputDevice): @max_distance.setter def max_distance(self, value): - if not (value > 0): + if value <= 0: raise ValueError('invalid maximum distance (must be positive)') t = self.threshold_distance self._max_distance = value @@ -670,14 +680,15 @@ class DistanceSensor(SmoothedInputDevice): self._trigger.pin.state = False # Wait up to 1 second for the echo pin to rise if self._echo.wait(1): - start = time() self._echo.clear() # Wait up to 40ms for the echo pin to fall (35ms is maximum pulse # time so any longer means something's gone wrong). Calculate # distance as time for echo multiplied by speed of sound divided by # two to compensate for travel to and from the reflector - if self._echo.wait(0.04): - distance = (time() - start) * self.speed_of_sound / 2.0 + if self._echo.wait(0.04) and self._echo_fall is not None and self._echo_rise is not None: + distance = (self._echo_fall - self._echo_rise) * self.speed_of_sound / 2.0 + self._echo_fall = None + self._echo_rise = None return min(1.0, distance / self._max_distance) else: # If we only saw one edge it means we missed the echo because diff --git a/gpiozero/mixins.py b/gpiozero/mixins.py index fd110ae..62eed6c 100644 --- a/gpiozero/mixins.py +++ b/gpiozero/mixins.py @@ -234,7 +234,7 @@ class EventsMixin(object): 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): + if self._active_event.is_set(): return time() - self._last_changed else: return None @@ -245,7 +245,7 @@ class EventsMixin(object): The length of time (in seconds) that the device has been inactive for. When the device is active, this is ``None``. """ - if self._inactive_event.wait(0): + if self._inactive_event.is_set(): return time() - self._last_changed else: return None @@ -434,11 +434,11 @@ class HoldThread(GPIOThread): self.start() def held(self, parent): - while not self.stopping.wait(0): + while not self.stopping.is_set(): if self.holding.wait(0.1): self.holding.clear() while not ( - self.stopping.wait(0) or + self.stopping.is_set() or parent._inactive_event.wait(parent.hold_time) ): if parent._held_from is None: diff --git a/gpiozero/other_devices.py b/gpiozero/other_devices.py index 47ca6ae..ccc77b5 100644 --- a/gpiozero/other_devices.py +++ b/gpiozero/other_devices.py @@ -73,6 +73,81 @@ class PingServer(InternalDevice): return True +class CPUTemperature(InternalDevice): + """ + Extends :class:`InternalDevice` to provide a device which is active when + the CPU temperature exceeds the *threshold* value. + + The following example plots the CPU's temperature on an LED bar graph:: + + from gpiozero import LEDBarGraph, CPUTemperature + from signal import pause + + # Use minimums and maximums that are closer to "normal" usage so the + # bar graph is a bit more "lively" + temp = CPUTemperature(min_temp=50, max_temp=90) + graph = LEDBarGraph(5, 6, 13, 19, 25, pwm=True) + graph.source = temp.values + pause() + + :param str sensor_file: + The file from which to read the temperature. This defaults to the + sysfs file :file:`/sys/class/thermal/thermal_zone0/temp`. Whatever + file is specified is expected to contain a single line containing the + temperature in milli-degrees celsius. + + :param float min_temp: + The temperature at which :attr:`value` will read 0.0. This defaults to + 0.0. + + :param float max_temp: + The temperature at which :attr:`value` will read 1.0. This defaults to + 100.0. + + :param float threshold: + The temperature above which the device will be considered "active". + This defaults to 80.0. + """ + def __init__(self, sensor_file='/sys/class/thermal/thermal_zone0/temp', + min_temp=0.0, max_temp=100.0, threshold=80.0): + self.sensor_file = sensor_file + super(CPUTemperature, self).__init__() + self.min_temp = min_temp + self.max_temp = max_temp + self.threshold = threshold + self._fire_events() + + def __repr__(self): + return '' % self.temperature + + @property + def temperature(self): + """ + Returns the current CPU temperature in degrees celsius. + """ + with io.open(self.sensor_file, 'r') as f: + return float(f.readline().strip()) / 1000 + + @property + def value(self): + """ + Returns the current CPU temperature as a value between 0.0 + (representing the *min_temp* value) and 1.0 (representing the + *max_temp* value). These default to 0.0 and 100.0 respectively, hence + :attr:`value` is :attr:`temperature` divided by 100 by default. + """ + temp_range = self.max_temp - self.min_temp + return (self.temperature - self.min_temp) / temp_range + + @property + def is_active(self): + """ + Returns ``True`` when the CPU :attr:`temperature` exceeds the + :attr:`threshold`. + """ + return self.temperature > self.threshold + + class TimeOfDay(InternalDevice): """ Extends :class:`InternalDevice` to provide a device which is active when diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index 037f749..1b3c2e2 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -43,16 +43,15 @@ class OutputDevice(SourceMixin, GPIODevice): self.active_high = active_high if initial_value is None: self.pin.function = 'output' - elif initial_value: - self.pin.output_with_state(self._active_state) else: - self.pin.output_with_state(self._inactive_state) + self.pin.output_with_state(self._value_to_state(initial_value)) + + def _value_to_state(self, value): + return bool(self._active_state if value else self._inactive_state) def _write(self, value): - if not self.active_high: - value = not value try: - self.pin.state = bool(value) + self.pin.state = self._value_to_state(value) except AttributeError: self._check_open() raise @@ -218,8 +217,8 @@ class LED(DigitalOutputDevice): led.on() :param int pin: - The GPIO pin which the LED is attached to. See :doc:`notes` for valid - pin numbers. + The GPIO pin which the LED is attached to. See :ref:`pin_numbering` for + valid pin numbers. :param bool active_high: If ``True`` (the default), the LED will operate normally with the @@ -253,8 +252,8 @@ class Buzzer(DigitalOutputDevice): bz.on() :param int pin: - The GPIO pin which the buzzer is attached to. See :doc:`notes` for - valid pin numbers. + The GPIO pin which the buzzer is attached to. See :ref:`pin_numbering` + for valid pin numbers. :param bool active_high: If ``True`` (the default), the buzzer will operate normally with the @@ -277,15 +276,15 @@ class PWMOutputDevice(OutputDevice): Generic output device configured for pulse-width modulation (PWM). :param int pin: - The GPIO pin which the device is attached to. See :doc:`notes` for - valid pin numbers. + The GPIO pin which the device is attached to. See :ref:`pin_numbering` + for valid pin numbers. :param bool active_high: If ``True`` (the default), the :meth:`on` method will set the GPIO to HIGH. If ``False``, the :meth:`on` method will set the GPIO to LOW (the :meth:`off` method always does the opposite). - :param bool initial_value: + :param float initial_value: If ``0`` (the default), the device's duty cycle will be 0 initially. Other values between 0 and 1 can be specified as an initial duty cycle. Note that ``None`` cannot be specified (unlike the parent class) as @@ -300,7 +299,7 @@ class PWMOutputDevice(OutputDevice): self._controller = None if not 0 <= initial_value <= 1: raise OutputDeviceBadValue("initial_value must be between 0 and 1") - super(PWMOutputDevice, self).__init__(pin, active_high) + super(PWMOutputDevice, self).__init__(pin, active_high, initial_value=None) try: # XXX need a way of setting these together self.pin.frequency = frequency @@ -318,23 +317,16 @@ class PWMOutputDevice(OutputDevice): pass super(PWMOutputDevice, self).close() - def _read(self): - self._check_open() - if self.active_high: - return self.pin.state - else: - return 1 - self.pin.state + def _state_to_value(self, state): + return float(state if self.active_high else 1 - state) + + def _value_to_state(self, value): + return float(value if self.active_high else 1 - value) def _write(self, value): - if not self.active_high: - value = 1 - value if not 0 <= value <= 1: raise OutputDeviceBadValue("PWM value must be between 0 and 1") - try: - self.pin.state = value - except AttributeError: - self._check_open() - raise + super(PWMOutputDevice, self)._write(value) @property def value(self): @@ -435,12 +427,12 @@ class PWMOutputDevice(OutputDevice): Number of seconds to spend fading out. Defaults to 1. :param int n: - Number of times to blink; ``None`` (the default) means forever. + Number of times to pulse; ``None`` (the default) means forever. :param bool background: If ``True`` (the default), start a background thread to continue - blinking and return immediately. If ``False``, only return when the - blink is finished (warning: the default value of *n* will result in + pulsing and return immediately. If ``False``, only return when the + pulse is finished (warning: the default value of *n* will result in this method never returning). """ on_time = off_time = 0 @@ -491,7 +483,7 @@ class PWMLED(PWMOutputDevice): an optional resistor to prevent the LED from burning out. :param int pin: - The GPIO pin which the LED is attached to. See :doc:`notes` for + The GPIO pin which the LED is attached to. See :ref:`pin_numbering` for valid pin numbers. :param bool active_high: @@ -499,7 +491,7 @@ class PWMLED(PWMOutputDevice): HIGH. If ``False``, the :meth:`on` method will set the GPIO to LOW (the :meth:`off` method always does the opposite). - :param bool initial_value: + :param float initial_value: If ``0`` (the default), the LED will be off initially. Other values between 0 and 1 can be specified as an initial brightness for the LED. Note that ``None`` cannot be specified (unlike the parent class) as @@ -553,18 +545,24 @@ class RGBLED(SourceMixin, Device): Set to ``True`` (the default) for common cathode RGB LEDs. If you are using a common anode RGB LED, set this to ``False``. - :param bool initial_value: - The initial color for the LED. Defaults to black ``(0, 0, 0)``. + :param tuple initial_value: + The initial color for the RGB LED. Defaults to black ``(0, 0, 0)``. + + :param bool pwm: + If ``True`` (the default), construct :class:`PWMLED` instances for + each component of the RGBLED. If ``False``, construct regular + :class:`LED` instances, which prevents smooth color graduations. """ def __init__( self, red=None, green=None, blue=None, active_high=True, - initial_value=(0, 0, 0)): + initial_value=(0, 0, 0), pwm=True): self._leds = () self._blink_thread = None - if not all([red, green, blue]): + if not all(p is not None for p in [red, green, blue]): raise GPIOPinMissing('red, green, and blue pins must be provided') + LEDClass = PWMLED if pwm else LED super(RGBLED, self).__init__() - self._leds = tuple(PWMLED(pin, active_high) for pin in (red, green, blue)) + self._leds = tuple(LEDClass(pin, active_high) for pin in (red, green, blue)) self.value = initial_value red = _led_property(0) @@ -581,13 +579,14 @@ class RGBLED(SourceMixin, Device): @property def closed(self): - return bool(self._leds) + return len(self._leds) == 0 @property def value(self): """ Represents the color of the LED as an RGB 3-tuple of ``(red, green, - blue)`` where each value is between 0 and 1. + blue)`` where each value is between 0 and 1 if ``pwm`` was ``True`` + when the class was constructed (and only 0 or 1 if not). For example, purple would be ``(1, 0, 1)`` and yellow would be ``(1, 1, 0)``, while orange would be ``(1, 0.5, 0)``. @@ -596,6 +595,12 @@ class RGBLED(SourceMixin, Device): @value.setter def value(self, value): + for component in value: + if not 0 <= component <= 1: + raise OutputDeviceBadValue('each RGB color component must be between 0 and 1') + if isinstance(self._leds[0], LED): + if component not in (0, 1): + raise OutputDeviceBadValue('each RGB color component must be 0 or 1 with non-PWM RGBLEDs') self._stop_blink() self.red, self.green, self.blue = value @@ -647,10 +652,14 @@ class RGBLED(SourceMixin, Device): Number of seconds off. Defaults to 1 second. :param float fade_in_time: - Number of seconds to spend fading in. Defaults to 0. + Number of seconds to spend fading in. Defaults to 0. Must be 0 if + ``pwm`` was ``False`` when the class was constructed + (:exc:`ValueError` will be raised if not). :param float fade_out_time: - Number of seconds to spend fading out. Defaults to 0. + Number of seconds to spend fading out. Defaults to 0. Must be 0 if + ``pwm`` was ``False`` when the class was constructed + (:exc:`ValueError` will be raised if not). :param tuple on_color: The color to use when the LED is "on". Defaults to white. @@ -667,16 +676,57 @@ class RGBLED(SourceMixin, Device): blink is finished (warning: the default value of *n* will result in this method never returning). """ + if isinstance(self._leds[0], LED): + if fade_in_time: + raise ValueError('fade_in_time must be 0 with non-PWM RGBLEDs') + if fade_out_time: + raise ValueError('fade_out_time must be 0 with non-PWM RGBLEDs') self._stop_blink() self._blink_thread = GPIOThread( target=self._blink_device, - args=(on_time, off_time, fade_in_time, fade_out_time, on_color, off_color, n) + args=( + on_time, off_time, fade_in_time, fade_out_time, + on_color, off_color, n + ) ) self._blink_thread.start() if not background: self._blink_thread.join() self._blink_thread = None + def pulse( + self, fade_in_time=1, fade_out_time=1, + on_color=(1, 1, 1), off_color=(0, 0, 0), n=None, background=True): + """ + Make the device fade in and out repeatedly. + + :param float fade_in_time: + Number of seconds to spend fading in. Defaults to 1. + + :param float fade_out_time: + Number of seconds to spend fading out. Defaults to 1. + + :param tuple on_color: + The color to use when the LED is "on". Defaults to white. + + :param tuple off_color: + The color to use when the LED is "off". Defaults to black. + + :param int n: + Number of times to pulse; ``None`` (the default) means forever. + + :param bool background: + If ``True`` (the default), start a background thread to continue + pulsing and return immediately. If ``False``, only return when the + pulse is finished (warning: the default value of *n* will result in + this method never returning). + """ + on_time = off_time = 0 + self.blink( + on_time, off_time, fade_in_time, fade_out_time, + on_color, off_color, n, background + ) + def _stop_blink(self, led=None): # If this is called with a single led, we stop all blinking anyway if self._blink_thread: @@ -746,22 +796,31 @@ class Motor(SourceMixin, CompositeDevice): :param int backward: The GPIO pin that the backward input of the motor driver chip is connected to. + + :param bool pwm: + If ``True`` (the default), construct :class:`PWMOutputDevice` + instances for the motor controller pins, allowing both direction and + variable speed control. If ``False``, construct + :class:`DigitalOutputDevice` instances, allowing only direction + control. """ - def __init__(self, forward=None, backward=None): - if not all([forward, backward]): + def __init__(self, forward=None, backward=None, pwm=True): + if not all(p is not None for p in [forward, backward]): raise GPIOPinMissing( 'forward and backward pins must be provided' ) + PinClass = PWMOutputDevice if pwm else DigitalOutputDevice super(Motor, self).__init__( - forward_device=PWMOutputDevice(forward), - backward_device=PWMOutputDevice(backward), + forward_device=PinClass(forward), + backward_device=PinClass(backward), _order=('forward_device', 'backward_device')) @property def value(self): """ Represents the speed of the motor as a floating point value between -1 - (full speed backward) and 1 (full speed forward). + (full speed backward) and 1 (full speed forward), with 0 representing + stopped. """ return self.forward_device.value - self.backward_device.value @@ -770,9 +829,15 @@ class Motor(SourceMixin, CompositeDevice): if not -1 <= value <= 1: raise OutputDeviceBadValue("Motor value must be between -1 and 1") if value > 0: - self.forward(value) + try: + self.forward(value) + except ValueError as e: + raise OutputDeviceBadValue(e) elif value < 0: - self.backward(-value) + try: + self.backward(-value) + except ValueError as e: + raise OutputDeviceBadValue(e) else: self.stop() @@ -790,8 +855,14 @@ class Motor(SourceMixin, CompositeDevice): :param float speed: The speed at which the motor should turn. Can be any value between - 0 (stopped) and the default 1 (maximum speed). + 0 (stopped) and the default 1 (maximum speed) if ``pwm`` was + ``True`` when the class was constructed (and only 0 or 1 if not). """ + if not 0 <= speed <= 1: + raise ValueError('forward speed must be between 0 and 1') + if isinstance(self.forward_device, DigitalOutputDevice): + if speed not in (0, 1): + raise ValueError('forward speed must be 0 or 1 with non-PWM Motors') self.backward_device.off() self.forward_device.value = speed @@ -801,8 +872,14 @@ class Motor(SourceMixin, CompositeDevice): :param float speed: The speed at which the motor should turn. Can be any value between - 0 (stopped) and the default 1 (maximum speed). + 0 (stopped) and the default 1 (maximum speed) if ``pwm`` was + ``True`` when the class was constructed (and only 0 or 1 if not). """ + if not 0 <= speed <= 1: + raise ValueError('backward speed must be between 0 and 1') + if isinstance(self.backward_device, DigitalOutputDevice): + if speed not in (0, 1): + raise ValueError('backward speed must be 0 or 1 with non-PWM Motors') self.forward_device.off() self.backward_device.value = speed @@ -820,3 +897,314 @@ class Motor(SourceMixin, CompositeDevice): """ self.forward_device.off() self.backward_device.off() + + +class Servo(SourceMixin, CompositeDevice): + """ + Extends :class:`CompositeDevice` and represents a PWM-controlled servo + motor connected to a GPIO pin. + + Connect a power source (e.g. a battery pack or the 5V pin) to the power + cable of the servo (this is typically colored red); connect the ground + cable of the servo (typically colored black or brown) to the negative of + your battery pack, or a GND pin; connect the final cable (typically colored + white or orange) to the GPIO pin you wish to use for controlling the servo. + + The following code will make the servo move between its minimum, maximum, + and mid-point positions with a pause between each:: + + from gpiozero import Servo + from time import sleep + + servo = Servo(17) + while True: + servo.min() + sleep(1) + servo.mid() + sleep(1) + servo.max() + sleep(1) + + :param int pin: + The GPIO pin which the device is attached to. See :ref:`pin_numbering` + for valid pin numbers. + + :param float initial_value: + If ``0`` (the default), the device's mid-point will be set + initially. Other values between -1 and +1 can be specified as an + initial position. ``None`` means to start the servo un-controlled (see + :attr:`value`). + + :param float min_pulse_width: + The pulse width corresponding to the servo's minimum position. This + defaults to 1ms. + + :param float max_pulse_width: + The pulse width corresponding to the servo's maximum position. This + defaults to 2ms. + + :param float frame_width: + The length of time between servo control pulses measured in seconds. + This defaults to 20ms which is a common value for servos. + """ + def __init__( + self, pin=None, initial_value=0.0, + min_pulse_width=1/1000, max_pulse_width=2/1000, + frame_width=20/1000): + if min_pulse_width >= max_pulse_width: + raise ValueError('min_pulse_width must be less than max_pulse_width') + if max_pulse_width >= frame_width: + raise ValueError('max_pulse_width must be less than frame_width') + self._frame_width = frame_width + self._min_dc = min_pulse_width / frame_width + self._dc_range = (max_pulse_width - min_pulse_width) / frame_width + self._min_value = -1 + self._value_range = 2 + super(Servo, self).__init__( + pwm_device=PWMOutputDevice(pin, frequency=int(1 / frame_width))) + try: + self.value = initial_value + except: + self.close() + raise + + @property + def frame_width(self): + """ + The time between control pulses, measured in seconds. + """ + return self._frame_width + + @property + def min_pulse_width(self): + """ + The control pulse width corresponding to the servo's minimum position, + measured in seconds. + """ + return self._min_dc * self.frame_width + + @property + def max_pulse_width(self): + """ + The control pulse width corresponding to the servo's maximum position, + measured in seconds. + """ + return (self._dc_range * self.frame_width) + self.min_pulse_width + + @property + def pulse_width(self): + """ + Returns the current pulse width controlling the servo. + """ + if self.pwm_device.pin.frequency is None: + return None + else: + return self.pwm_device.pin.state * self.frame_width + + def min(self): + """ + Set the servo to its minimum position. + """ + self.value = -1 + + def mid(self): + """ + Set the servo to its mid-point position. + """ + self.value = 0 + + def max(self): + """ + Set the servo to its maximum position. + """ + self.value = 1 + + def detach(self): + """ + Temporarily disable control of the servo. This is equivalent to + setting :attr:`value` to ``None``. + """ + self.value = None + + def _get_value(self): + if self.pwm_device.pin.frequency is None: + return None + else: + return ( + ((self.pwm_device.pin.state - self._min_dc) / self._dc_range) * + self._value_range + self._min_value) + + @property + def value(self): + """ + Represents the position of the servo as a value between -1 (the minimum + position) and +1 (the maximum position). This can also be the special + value ``None`` indicating that the servo is currently "uncontrolled", + i.e. that no control signal is being sent. Typically this means the + servo's position remains unchanged, but that it can be moved by hand. + """ + result = self._get_value() + if result is None: + return result + else: + # NOTE: This round() only exists to ensure we don't confuse people + # by returning 2.220446049250313e-16 as the default initial value + # instead of 0. The reason _get_value and _set_value are split + # out is for descendents that require the un-rounded values for + # accuracy + return round(result, 14) + + @value.setter + def value(self, value): + if value is None: + self.pwm_device.pin.frequency = None + elif -1 <= value <= 1: + self.pwm_device.pin.frequency = int(1 / self.frame_width) + self.pwm_device.pin.state = ( + self._min_dc + self._dc_range * + ((value - self._min_value) / self._value_range) + ) + else: + raise OutputDeviceBadValue( + "Servo value must be between -1 and 1, or None") + + @property + def is_active(self): + return self.value is not None + + +class AngularServo(Servo): + """ + Extends :class:`Servo` and represents a rotational PWM-controlled servo + motor which can be set to particular angles (assuming valid minimum and + maximum angles are provided to the constructor). + + Connect a power source (e.g. a battery pack or the 5V pin) to the power + cable of the servo (this is typically colored red); connect the ground + cable of the servo (typically colored black or brown) to the negative of + your battery pack, or a GND pin; connect the final cable (typically colored + white or orange) to the GPIO pin you wish to use for controlling the servo. + + Next, calibrate the angles that the servo can rotate to. In an interactive + Python session, construct a :class:`Servo` instance. The servo should move + to its mid-point by default. Set the servo to its minimum value, and + measure the angle from the mid-point. Set the servo to its maximum value, + and again measure the angle:: + + >>> from gpiozero import Servo + >>> s = Servo(17) + >>> s.min() # measure the angle + >>> s.max() # measure the angle + + You should now be able to construct an :class:`AngularServo` instance + with the correct bounds:: + + >>> from gpiozero import AngularServo + >>> s = AngularServo(17, min_angle=-42, max_angle=44) + >>> s.angle = 0.0 + >>> s.angle + 0.0 + >>> s.angle = 15 + >>> s.angle + 15.0 + + .. note:: + + You can set *min_angle* greater than *max_angle* if you wish to reverse + the sense of the angles (e.g. ``min_angle=45, max_angle=-45``). This + can be useful with servos that rotate in the opposite direction to your + expectations of minimum and maximum. + + :param int pin: + The GPIO pin which the device is attached to. See :ref:`pin_numbering` + for valid pin numbers. + + :param float initial_angle: + Sets the servo's initial angle to the specified value. The default is + 0. The value specified must be between *min_angle* and *max_angle* + inclusive. ``None`` means to start the servo un-controlled (see + :attr:`value`). + + :param float min_angle: + Sets the minimum angle that the servo can rotate to. This defaults to + -90, but should be set to whatever you measure from your servo during + calibration. + + :param float max_angle: + Sets the maximum angle that the servo can rotate to. This defaults to + 90, but should be set to whatever you measure from your servo during + calibration. + + :param float min_pulse_width: + The pulse width corresponding to the servo's minimum position. This + defaults to 1ms. + + :param float max_pulse_width: + The pulse width corresponding to the servo's maximum position. This + defaults to 2ms. + + :param float frame_width: + The length of time between servo control pulses measured in seconds. + This defaults to 20ms which is a common value for servos. + """ + def __init__( + self, pin=None, initial_angle=0.0, + min_angle=-90, max_angle=90, + min_pulse_width=1/1000, max_pulse_width=2/1000, + frame_width=20/1000): + self._min_angle = min_angle + self._angular_range = max_angle - min_angle + initial_value = 2 * ((initial_angle - min_angle) / self._angular_range) - 1 + super(AngularServo, self).__init__( + pin, initial_value, min_pulse_width, max_pulse_width, frame_width) + + @property + def min_angle(self): + """ + The minimum angle that the servo will rotate to when :meth:`min` is + called. + """ + return self._min_angle + + @property + def max_angle(self): + """ + The maximum angle that the servo will rotate to when :meth:`max` is + called. + """ + return self._min_angle + self._angular_range + + @property + def angle(self): + """ + The position of the servo as an angle measured in degrees. This will + only be accurate if *min_angle* and *max_angle* have been set + appropriately in the constructor. + + This can also be the special value ``None`` indicating that the servo + is currently "uncontrolled", i.e. that no control signal is being sent. + Typically this means the servo's position remains unchanged, but that + it can be moved by hand. + """ + result = self._get_value() + if result is None: + return None + else: + # NOTE: Why round(n, 12) here instead of 14? Angle ranges can be + # much larger than -1..1 so we need a little more rounding to + # smooth off the rough corners! + return round( + self._angular_range * + ((result - self._min_value) / self._value_range) + + self._min_angle, 12) + + @angle.setter + def angle(self, value): + if value is None: + self.value = None + else: + self.value = ( + self._value_range * + ((value - self._min_angle) / self._angular_range) + + self._min_value) + diff --git a/gpiozero/pins/__init__.py b/gpiozero/pins/__init__.py index e753076..3503145 100644 --- a/gpiozero/pins/__init__.py +++ b/gpiozero/pins/__init__.py @@ -6,6 +6,9 @@ from __future__ import ( ) str = type('') +import io + +from .data import pi_info from ..exc import ( PinInvalidFunction, PinSetInput, @@ -47,6 +50,7 @@ class Pin(object): * :meth:`_set_edges` * :meth:`_get_when_changed` * :meth:`_set_when_changed` + * :meth:`pi_info` * :meth:`output_with_state` * :meth:`input_with_pull` @@ -243,3 +247,48 @@ class Pin(object): property will raise :exc:`PinEdgeDetectUnsupported`. """) + @classmethod + def pi_info(cls): + """ + Returns a :class:`PiBoardInfo` instance representing the Pi that + instances of this pin class will be attached to. + + If the pins represented by this class are not *directly* attached to a + Pi (e.g. the pin is attached to a board attached to the Pi, or the pins + are not on a Pi at all), this may return ``None``. + """ + return None + + +class LocalPin(Pin): + """ + Abstract base class representing pins attached locally to a Pi. This forms + the base class for local-only pin interfaces (:class:`RPiGPIOPin`, + :class:`RPIOPin`, and :class:`NativePin`). + """ + _PI_REVISION = None + + @classmethod + def pi_info(cls): + """ + Returns a :class:`PiBoardInfo` instance representing the local Pi. + The Pi's revision is determined by reading :file:`/proc/cpuinfo`. If + no valid revision is found, returns ``None``. + """ + # Cache the result as we can reasonably assume it won't change during + # runtime (this is LocalPin after all; descendents that deal with + # remote Pis should inherit from Pin instead) + if cls._PI_REVISION is None: + with io.open('/proc/cpuinfo', 'r') as f: + for line in f: + if line.startswith('Revision'): + revision = line.split(':')[1].strip().lower() + overvolted = revision.startswith('100') + if overvolted: + revision = revision[-4:] + cls._PI_REVISION = revision + break + if cls._PI_REVISION is None: + return None # something weird going on + return pi_info(cls._PI_REVISION) + diff --git a/gpiozero/pins/data.py b/gpiozero/pins/data.py index b824d7f..2d39fe1 100644 --- a/gpiozero/pins/data.py +++ b/gpiozero/pins/data.py @@ -243,29 +243,29 @@ CM_SODIMM = { PI_REVISIONS = { # rev model pcb_rev released soc manufacturer ram storage usb eth wifi bt csi dsi headers - 'beta': ('B', '?', '2012Q1', 'BCM2835', '?', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV1_P1}, ), - '0002': ('B', '1.0', '2012Q1', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV1_P1}, ), - '0003': ('B', '1.0', '2012Q3', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV1_P1}, ), - '0004': ('B', '2.0', '2012Q3', 'BCM2835', 'Sony', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), - '0005': ('B', '2.0', '2012Q4', 'BCM2835', 'Qisda', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), - '0006': ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), - '0007': ('A', '2.0', '2013Q1', 'BCM2835', 'Egoman', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), - '0008': ('A', '2.0', '2013Q1', 'BCM2835', 'Sony', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), - '0009': ('A', '2.0', '2013Q1', 'BCM2835', 'Qisda', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), - '000d': ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), - '000e': ('B', '2.0', '2012Q4', 'BCM2835', 'Sony', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), - '000f': ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), - '0010': ('B+', '1.2', '2014Q3', 'BCM2835', 'Sony', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'P1': PLUS_P1}, ), - '0011': ('CM', '1.2', '2014Q2', 'BCM2835', 'Sony', 512, 'eMMC', 0, 0, False, False, 2, 2, {'SODIMM': CM_SODIMM}, ), - '0012': ('A+', '1.2', '2014Q4', 'BCM2835', 'Sony', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'P1': PLUS_P1}, ), - '0013': ('B+', '1.2', '2015Q1', 'BCM2835', 'Egoman', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'P1': PLUS_P1}, ), - '0014': ('CM', '1.1', '2014Q2', 'BCM2835', 'Sony', 512, 'eMMC', 0, 0, False, False, 2, 2, {'SODIMM': CM_SODIMM}, ), - '0015': ('A+', '1.1', '2014Q4', 'BCM2835', 'Sony', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'P1': PLUS_P1}, ), - 'a01041': ('2B', '1.1', '2015Q1', 'BCM2836', 'Sony', 1024, 'MicroSD', 4, 1, False, False, 1, 1, {'P1': PLUS_P1}, ), - 'a21041': ('2B', '1.1', '2015Q1', 'BCM2836', 'Embest', 1024, 'MicroSD', 4, 1, False, False, 1, 1, {'P1': PLUS_P1}, ), - '900092': ('Zero', '1.2', '2015Q4', 'BCM2835', 'Sony', 512, 'MicroSD', 1, 0, False, False, 0, 0, {'P1': PLUS_P1}, ), - 'a02082': ('3B', '1.2', '2016Q1', 'BCM2837', 'Sony', 1024, 'MicroSD', 4, 1, True, True, 1, 1, {'P1': PLUS_P1}, ), - 'a22082': ('3B', '1.2', '2016Q1', 'BCM2837', 'Embest', 1024, 'MicroSD', 4, 1, True, True, 1, 1, {'P1': PLUS_P1}, ), + 0x2: ('B', '1.0', '2012Q1', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV1_P1}, ), + 0x3: ('B', '1.0', '2012Q3', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV1_P1}, ), + 0x4: ('B', '2.0', '2012Q3', 'BCM2835', 'Sony', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), + 0x5: ('B', '2.0', '2012Q4', 'BCM2835', 'Qisda', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), + 0x6: ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), + 0x7: ('A', '2.0', '2013Q1', 'BCM2835', 'Egoman', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), + 0x8: ('A', '2.0', '2013Q1', 'BCM2835', 'Sony', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), + 0x9: ('A', '2.0', '2013Q1', 'BCM2835', 'Qisda', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), + 0xd: ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), + 0xe: ('B', '2.0', '2012Q4', 'BCM2835', 'Sony', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), + 0xf: ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5},), + 0x10: ('B+', '1.2', '2014Q3', 'BCM2835', 'Sony', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'P1': PLUS_P1}, ), + 0x11: ('CM', '1.2', '2014Q2', 'BCM2835', 'Sony', 512, 'eMMC', 1, 0, False, False, 2, 2, {'SODIMM': CM_SODIMM}, ), + 0x12: ('A+', '1.2', '2014Q4', 'BCM2835', 'Sony', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'P1': PLUS_P1}, ), + 0x13: ('B+', '1.2', '2015Q1', 'BCM2835', 'Egoman', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'P1': PLUS_P1}, ), + 0x14: ('CM', '1.1', '2014Q2', 'BCM2835', 'Embest', 512, 'eMMC', 1, 0, False, False, 2, 2, {'SODIMM': CM_SODIMM}, ), + 0x15: ('A+', '1.1', '2014Q4', 'BCM2835', 'Sony', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'P1': PLUS_P1}, ), + 0xa01041: ('2B', '1.1', '2015Q1', 'BCM2836', 'Sony', 1024, 'MicroSD', 4, 1, False, False, 1, 1, {'P1': PLUS_P1}, ), + 0xa21041: ('2B', '1.1', '2015Q1', 'BCM2836', 'Embest', 1024, 'MicroSD', 4, 1, False, False, 1, 1, {'P1': PLUS_P1}, ), + 0x900092: ('Zero', '1.2', '2015Q4', 'BCM2835', 'Sony', 512, 'MicroSD', 1, 0, False, False, 0, 0, {'P1': PLUS_P1}, ), + 0xa02082: ('3B', '1.2', '2016Q1', 'BCM2837', 'Sony', 1024, 'MicroSD', 4, 1, True, True, 1, 1, {'P1': PLUS_P1}, ), + 0xa22082: ('3B', '1.2', '2016Q1', 'BCM2837', 'Embest', 1024, 'MicroSD', 4, 1, True, True, 1, 1, {'P1': PLUS_P1}, ), + 0x900093: ('Zero', '1.3', '2016Q2', 'BCM2835', 'Sony', 512, 'MicroSD', 1, 0, False, False, 1, 0, {'P1': PLUS_P1}, ), } @@ -324,6 +324,12 @@ class PiBoardInfo(namedtuple('PiBoardInfo', ( a tuple, it is strongly recommended that you use the following named attributes to access the data contained within. + .. automethod:: physical_pin + + .. automethod:: physical_pins + + .. automethod:: pulled_up + .. attribute:: revision A string indicating the revision of the Pi. This is unique to each @@ -389,10 +395,6 @@ class PiBoardInfo(namedtuple('PiBoardInfo', ( .. note:: This does *not* include the micro-USB port used to power the Pi. - On the Compute Module this is listed as 0 as the compute module - itself doesn't have any physical USB headers, despite providing one - on the I/O development board and having the pins for one on the - module itself. .. attribute:: ethernet @@ -430,7 +432,7 @@ class PiBoardInfo(namedtuple('PiBoardInfo', ( def physical_pins(self, function): """ - Return the physical pins supporting the specified *function* as a tuple + Return the physical pins supporting the specified *function* as tuples of ``(header, pin_number)`` where *header* is a string specifying the header containing the *pin_number*. Note that the return value is a :class:`set` which is not indexable. Use :func:`physical_pin` if you @@ -487,19 +489,6 @@ class PiBoardInfo(namedtuple('PiBoardInfo', ( return self.headers[header][number].pull_up -_PI_REVISION = None -def _get_pi_revision(): - with io.open('/proc/cpuinfo', 'r') as f: - for line in f: - if line.startswith('Revision'): - revision = line.split(':')[1].strip().lower() - overvolted = revision.startswith('1000') - if overvolted: - revision = revision[4:] - return revision - raise IOError('unable to locate Pi revision in /proc/cpuinfo') - - def _parse_pi_revision(revision): # For new-style revisions the value's bit pattern is as follows: # @@ -512,10 +501,9 @@ def _parse_pi_revision(revision): # CCCC - Manufacturer (0=Sony, 1=Egoman, 2=Embest) # PPPP - Processor (0=2835, 1=2836, 2=2837) # TTTTTTTT - Type (0=A, 1=B, 2=A+, 3=B+, 4=2B, 5=Alpha (??), 6=CM, 8=3B, 9=Zero) - # RRR - Revision (0, 1, or 2) - i = int(revision, base=16) - if not (i & 0x800000): - raise ValueError('cannot parse "%s"; this is not a new-style revision' % revision) + # RRRR - Revision (0, 1, or 2) + if not (revision & 0x800000): + raise PinUnknownPi('cannot parse "%x"; this is not a new-style revision' % revision) try: model = { 0: 'A', @@ -526,15 +514,15 @@ def _parse_pi_revision(revision): 6: 'CM', 8: '3B', 9: 'Zero', - }[(i & 0xff0) >> 4] + }[(revision & 0xff0) >> 4] if model in ('A', 'B'): pcb_revision = { 0: '1.0', # is this right? 1: '1.0', 2: '2.0', - }[i & 0x0f] + }[revision & 0x0f] else: - pcb_revision = '1.%d' % (i & 0x0f) + pcb_revision = '1.%d' % (revision & 0x0f) released = { 'A': '2013Q1', 'B': '2012Q1' if pcb_revision == '1.0' else '2012Q4', @@ -543,23 +531,23 @@ def _parse_pi_revision(revision): '2B': '2015Q1', 'CM': '2014Q2', '3B': '2016Q1', - 'Zero': '2015Q4', + 'Zero': '2015Q4' if pcb_revision == '1.0' else '2016Q2', }[model] soc = { 0: 'BCM2835', 1: 'BCM2836', 2: 'BCM2837', - }[(i & 0xf000) >> 12] + }[(revision & 0xf000) >> 12] manufacturer = { 0: 'Sony', 1: 'Egoman', 2: 'Embest', - }[(i & 0xf0000) >> 16] + }[(revision & 0xf0000) >> 16] memory = { 0: 256, 1: 512, 2: 1024, - }[(i & 0x700000) >> 20] + }[(revision & 0x700000) >> 20] storage = { 'A': 'SD', 'B': 'SD', @@ -585,17 +573,19 @@ def _parse_pi_revision(revision): '3B': True, }.get(model, False) csi = { - 'Zero': 0, + 'Zero': 0 if pcb_revision == '1.0' else 1, 'CM': 2, }.get(model, 1) - dsi = csi + dsi = { + 'Zero': 0, + }.get(model, csi) headers = { 'A': {'P1': REV2_P1, 'P5': REV2_P5}, 'B': {'P1': REV2_P1, 'P5': REV2_P5} if pcb_revision == '2.0' else {'P1': REV1_P1}, 'CM': {'SODIMM': CM_SODIMM}, }.get(model, {'P1': PLUS_P1}) except KeyError: - raise ValueError('unable to parse new-style revision "%s"' % revision) + raise PinUnknownPi('unable to parse new-style revision "%x"' % revision) else: return ( model, @@ -625,16 +615,26 @@ def pi_info(revision=None): or ``None`` (the default), then the library will attempt to determine the model of Pi it is running on and return information about that. """ - # cache the result as we can reasonably assume the revision of the Pi isn't - # going to change at runtime... if revision is None: - global _PI_REVISION - if _PI_REVISION is None: - try: - _PI_REVISION = _get_pi_revision() - except IOError: - _PI_REVISION = 'unknown' - revision = _PI_REVISION + # NOTE: This import is declared locally for two reasons. Firstly it + # avoids a circular dependency (devices->pins->pins.data->devices). + # Secondly, pin_factory is one global which might potentially be + # re-written by a user's script at runtime hence we should re-import + # here in case it's changed since initialization + from ..devices import pin_factory + result = pin_factory.pi_info() + if result is None: + raise PinUnknownPi('The default pin_factory is not attached to a Pi') + else: + return result + else: + if isinstance(revision, bytes): + revision = revision.decode('ascii') + if isinstance(revision, str): + revision = int(revision, base=16) + else: + # be nice to people passing an int (or something numeric anyway) + revision = int(revision) try: ( model, @@ -653,25 +653,22 @@ def pi_info(revision=None): headers, ) = PI_REVISIONS[revision] except KeyError: - try: - ( - model, - pcb_revision, - released, - soc, - manufacturer, - memory, - storage, - usb, - ethernet, - wifi, - bluetooth, - csi, - dsi, - headers, - ) = _parse_pi_revision(revision) - except ValueError: - raise PinUnknownPi('unknown RPi revision "%s"' % revision) + ( + model, + pcb_revision, + released, + soc, + manufacturer, + memory, + storage, + usb, + ethernet, + wifi, + bluetooth, + csi, + dsi, + headers, + ) = _parse_pi_revision(revision) headers = { header: { number: PinInfo(number, function, pull_up) @@ -680,7 +677,7 @@ def pi_info(revision=None): for header, header_data in headers.items() } return PiBoardInfo( - revision, + '%04x' % revision, model, pcb_revision, released, diff --git a/gpiozero/pins/mock.py b/gpiozero/pins/mock.py index 101ae7d..f865c30 100644 --- a/gpiozero/pins/mock.py +++ b/gpiozero/pins/mock.py @@ -16,6 +16,7 @@ except ImportError: from ..compat import isclose from . import Pin +from .data import pi_info from ..exc import PinSetInput, PinPWMUnsupported, PinFixedPull @@ -32,6 +33,10 @@ class MockPin(Pin): def clear_pins(cls): cls._PINS.clear() + @classmethod + def pi_info(cls): + return pi_info('a21041') # Pretend we're a Pi 2B + def __new__(cls, number): if not (0 <= number < 54): raise ValueError('invalid pin %d specified (must be 0..53)' % number) diff --git a/gpiozero/pins/native.py b/gpiozero/pins/native.py index 8244bac..290ea9a 100644 --- a/gpiozero/pins/native.py +++ b/gpiozero/pins/native.py @@ -17,7 +17,7 @@ from time import sleep from threading import Thread, Event, Lock from collections import Counter -from . import Pin, PINS_CLEANUP +from . import LocalPin, PINS_CLEANUP from .data import pi_info from ..exc import ( PinInvalidPull, @@ -149,7 +149,7 @@ class GPIOFS(object): f.write(str(pin).encode('ascii')) -class NativePin(Pin): +class NativePin(LocalPin): """ Uses a built-in pure Python implementation to interface to the Pi's GPIO pins. This is the default pin implementation if no third-party libraries diff --git a/gpiozero/pins/pigpiod.py b/gpiozero/pins/pigpiod.py index 3dafefc..537eb48 100644 --- a/gpiozero/pins/pigpiod.py +++ b/gpiozero/pins/pigpiod.py @@ -8,6 +8,7 @@ str = type('') import warnings import pigpio +import os from . import Pin from .data import pi_info @@ -68,7 +69,7 @@ class PiGPIOPin(Pin): .. _pigpio: http://abyz.co.uk/rpi/pigpio/ """ - _CONNECTIONS = {} + _CONNECTIONS = {} # maps (host, port) to (connection, pi_info) _PINS = {} GPIO_FUNCTIONS = { @@ -98,25 +99,17 @@ class PiGPIOPin(Pin): GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()} GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()} - PI_INFO = None - - def __new__(cls, number, host='localhost', port=8888): - # XXX What about remote pins? This should probably be instance - # specific rather than class specific for pigpio. Need to check how - # to query remote info though... - if cls.PI_INFO is None: - cls.PI_INFO = pi_info() + def __new__( + cls, number, host=os.getenv('PIGPIO_ADDR', 'localhost'), + port=int(os.getenv('PIGPIO_PORT', 8888))): try: return cls._PINS[(host, port, number)] except KeyError: self = super(PiGPIOPin, cls).__new__(cls) + cls.pi_info(host, port) # implicitly creates connection + self._connection, self._pi_info = cls._CONNECTIONS[(host, port)] try: - self._connection = cls._CONNECTIONS[(host, port)] - except KeyError: - self._connection = pigpio.pi(host, port) - cls._CONNECTIONS[(host, port)] = self._connection - try: - cls.PI_INFO.physical_pin('GPIO%d' % number) + self._pi_info.physical_pin('GPIO%d' % number) except PinNoPins: warnings.warn( PinNonPhysical( @@ -124,7 +117,7 @@ class PiGPIOPin(Pin): self._host = host self._port = port self._number = number - self._pull = 'up' if cls.PI_INFO.pulled_up('GPIO%d' % number) else 'floating' + self._pull = 'up' if self._pi_info.pulled_up('GPIO%d' % number) else 'floating' self._pwm = False self._bounce = None self._when_changed = None @@ -136,7 +129,6 @@ class PiGPIOPin(Pin): raise ValueError(e) self._connection.set_pull_up_down(self._number, self.GPIO_PULL_UPS[self._pull]) self._connection.set_glitch_filter(self._number, 0) - self._connection.set_PWM_range(self._number, 255) cls._PINS[(host, port, number)] = self return self @@ -167,7 +159,7 @@ class PiGPIOPin(Pin): self.frequency = None self.when_changed = None self.function = 'input' - self.pull = 'up' if self.PI_INFO.pulled_up('GPIO%d' % self.number) else 'floating' + self.pull = 'up' if self._pi_info.pulled_up('GPIO%d' % self.number) else 'floating' def _get_function(self): return self.GPIO_FUNCTION_NAMES[self._connection.get_mode(self._number)] @@ -182,14 +174,19 @@ class PiGPIOPin(Pin): def _get_state(self): if self._pwm: - return self._connection.get_PWM_dutycycle(self._number) / 255 + return ( + self._connection.get_PWM_dutycycle(self._number) / + self._connection.get_PWM_range(self._number) + ) else: return bool(self._connection.read(self._number)) def _set_state(self, value): if self._pwm: try: - self._connection.set_PWM_dutycycle(self._number, int(value * 255)) + value = int(value * self._connection.get_PWM_range(self._number)) + if value != self._connection.get_PWM_dutycycle(self._number): + self._connection.set_PWM_dutycycle(self._number, value) except pigpio.error: raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) elif self.function == 'input': @@ -204,7 +201,7 @@ class PiGPIOPin(Pin): def _set_pull(self, value): if self.function != 'input': raise PinFixedPull('cannot set pull on non-input pin %r' % self) - if value != 'up' and self.PI_INFO.pulled_up('GPIO%d' % self._number): + if value != 'up' and self._pi_info.pulled_up('GPIO%d' % self._number): raise PinFixedPull('%r has a physical pull-up resistor' % self) try: self._connection.set_pull_up_down(self._number, self.GPIO_PULL_UPS[value]) @@ -220,12 +217,15 @@ class PiGPIOPin(Pin): def _set_frequency(self, value): if not self._pwm and value is not None: self._connection.set_PWM_frequency(self._number, value) + self._connection.set_PWM_range(self._number, 10000) self._connection.set_PWM_dutycycle(self._number, 0) self._pwm = True elif self._pwm and value is not None: - self._connection.set_PWM_frequency(self._number, value) + if value != self._connection.get_PWM_frequency(self._number): + self._connection.set_PWM_frequency(self._number, value) + self._connection.set_PWM_range(self._number, 10000) elif self._pwm and value is None: - self._connection.set_PWM_dutycycle(self._number, 0) + self._connection.write(self._number, 0) self._pwm = False def _get_bounce(self): @@ -263,3 +263,16 @@ class PiGPIOPin(Pin): self._number, self._edges, lambda gpio, level, tick: value()) + @classmethod + def pi_info( + cls, host=os.getenv('PIGPIO_ADDR', 'localhost'), + port=int(os.getenv('PIGPIO_PORT', 8888))): + try: + connection, info = cls._CONNECTIONS[(host, port)] + except KeyError: + connection = pigpio.pi(host, port) + revision = '%04x' % connection.get_hardware_revision() + info = pi_info(revision) + cls._CONNECTIONS[(host, port)] = (connection, info) + return info + diff --git a/gpiozero/pins/rpigpio.py b/gpiozero/pins/rpigpio.py index 9fea142..1597fb0 100644 --- a/gpiozero/pins/rpigpio.py +++ b/gpiozero/pins/rpigpio.py @@ -9,7 +9,7 @@ str = type('') import warnings from RPi import GPIO -from . import Pin +from . import LocalPin from .data import pi_info from ..exc import ( PinInvalidFunction, @@ -24,7 +24,7 @@ from ..exc import ( ) -class RPiGPIOPin(Pin): +class RPiGPIOPin(LocalPin): """ Uses the `RPi.GPIO`_ library to interface to the Pi's GPIO pins. This is the default pin implementation if the RPi.GPIO library is installed. diff --git a/gpiozero/pins/rpio.py b/gpiozero/pins/rpio.py index 5118150..58d5893 100644 --- a/gpiozero/pins/rpio.py +++ b/gpiozero/pins/rpio.py @@ -12,7 +12,7 @@ import RPIO import RPIO.PWM from RPIO.Exceptions import InvalidChannelException -from . import Pin, PINS_CLEANUP +from . import LocalPin, PINS_CLEANUP from .data import pi_info from ..exc import ( PinInvalidFunction, @@ -27,7 +27,7 @@ from ..exc import ( ) -class RPIOPin(Pin): +class RPIOPin(LocalPin): """ Uses the `RPIO`_ library to interface to the Pi's GPIO pins. This is the default pin implementation if the RPi.GPIO library is not installed, diff --git a/gpiozero/spi.py b/gpiozero/spi.py index 9a4e712..7a816fe 100644 --- a/gpiozero/spi.py +++ b/gpiozero/spi.py @@ -163,7 +163,7 @@ class SPISoftwareBus(SharedMixin, Device): return self.lock is None @classmethod - def _shared_key(self, clock_pin, mosi_pin, miso_pin): + def _shared_key(cls, clock_pin, mosi_pin, miso_pin): return (clock_pin, mosi_pin, miso_pin) def read(self, n): @@ -396,24 +396,30 @@ def SPI(**spi_args): raise SPIBadArgs( 'unrecognized keyword argument %s' % kwargs.popitem()[0]) if all(( - SpiDev is not None, spi_args['clock_pin'] == 11, spi_args['mosi_pin'] == 10, spi_args['miso_pin'] == 9, spi_args['select_pin'] in (7, 8), )): - try: - if shared: - return SharedSPIHardwareInterface( - port=0, device={8: 0, 7: 1}[spi_args['select_pin']]) - else: - return SPIHardwareInterface( - port=0, device={8: 0, 7: 1}[spi_args['select_pin']]) - except Exception as e: + if SpiDev is None: warnings.warn( SPISoftwareFallback( - 'failed to initialize hardware SPI, falling back to ' - 'software (error was: %s)' % str(e))) + 'failed to import spidev, falling back to software SPI')) + else: + try: + hardware_spi_args = { + port: 0, + device: {8: 0, 7: 1}[spi_args['select_pin']], + } + if shared: + return SharedSPIHardwareInterface(**hardware_spi_args) + else: + return SPIHardwareInterface(**hardware_spi_args) + except Exception as e: + warnings.warn( + SPISoftwareFallback( + 'failed to initialize hardware SPI, falling back to ' + 'software (error was: %s)' % str(e))) if shared: return SharedSPISoftwareInterface(**spi_args) else: diff --git a/gpiozero/spi_devices.py b/gpiozero/spi_devices.py index 84cc8cc..78c2a5d 100644 --- a/gpiozero/spi_devices.py +++ b/gpiozero/spi_devices.py @@ -112,7 +112,6 @@ class MCP3xxx(AnalogInputDevice): def __init__(self, channel=0, bits=10, differential=False, **spi_args): self._channel = channel - self._bits = bits self._differential = bool(differential) super(MCP3xxx, self).__init__(bits, **spi_args) diff --git a/gpiozero/tools.py b/gpiozero/tools.py index 11a079a..9ae8346 100644 --- a/gpiozero/tools.py +++ b/gpiozero/tools.py @@ -41,10 +41,13 @@ def negated(values): yield not v -def inverted(values): +def inverted(values, input_min=0, input_max=1): """ - Returns the inversion of the supplied values (1 becomes 0, 0 becomes 1, - 0.1 becomes 0.9, etc.). For example:: + Returns the inversion of the supplied values (*input_min* becomes + *input_max*, *input_max* becomes *input_min*, `input_min + 0.1` becomes + `input_max - 0.1`, etc.). All items in *values* are assumed to be between + *input_min* and *input_max* (which default to 0 and 1 respectively), and + the output will be in the same range. For example:: from gpiozero import MCP3008, PWMLED from gpiozero.tools import inverted @@ -55,8 +58,10 @@ def inverted(values): led.source = inverted(pot.values) pause() """ + if input_min >= input_max: + raise ValueError('input_min must be smaller than input_max') for v in values: - yield 1 - v + yield input_min + input_max - v def scaled(values, output_min, output_max, input_min=0, input_max=1): @@ -82,6 +87,8 @@ def scaled(values, output_min, output_max, input_min=0, input_max=1): *input_max* (inclusive) then the function will not produce values that lie within *output_min* to *output_max* (inclusive). """ + if input_min >= input_max: + raise ValueError('input_min must be smaller than input_max') input_size = input_max - input_min output_size = output_max - output_min for v in values: @@ -104,6 +111,8 @@ def clamped(values, output_min=0, output_max=1): led.source = clamped(pot.values, 0.5, 1.0) pause() """ + if output_min >= output_max: + raise ValueError('output_min must be smaller than output_max') for v in values: yield min(max(v, output_min), output_max) @@ -128,11 +137,11 @@ def absoluted(values): yield abs(v) -def quantized(values, steps, output_min=0, output_max=1): +def quantized(values, steps, input_min=0, input_max=1): """ Returns *values* quantized to *steps* increments. All items in *values* are - assumed to be between *output_min* and *output_max* (use :func:`scaled` to - ensure this if necessary). + assumed to be between *input_min* and *input_max* (which default to 0 and + 1 respectively), and the output will be in the same range. For example, to quantize values between 0 and 1 to 5 "steps" (0.0, 0.25, 0.5, 0.75, 1.0):: @@ -146,9 +155,72 @@ def quantized(values, steps, output_min=0, output_max=1): led.source = quantized(pot.values, 4) pause() """ - output_size = output_max - output_min - for v in scaled(values, 0, 1, output_min, output_max): - yield ((int(v * steps) / steps) * output_size) + output_min + if steps < 1: + raise ValueError("steps must be 1 or larger") + if input_min >= input_max: + raise ValueError('input_min must be smaller than input_max') + input_size = input_max - input_min + for v in scaled(values, 0, 1, input_min, input_max): + yield ((int(v * steps) / steps) * input_size) + input_min + + +def booleanized(values, min_value, max_value, hysteresis=0): + """ + Returns True for each item in *values* between *min_value* and + *max_value*, and False otherwise. *hysteresis* can optionally be used to + add `hysteresis`_ which prevents the output value rapidly flipping when + the input value is fluctuating near the *min_value* or *max_value* + thresholds. For example, to light an LED only when a potentiometer is + between 1/4 and 3/4 of its full range:: + + from gpiozero import LED, MCP3008 + from gpiozero.tools import booleanized + from signal import pause + + led = LED(4) + pot = MCP3008(channel=0) + led.source = booleanized(pot.values, 0.25, 0.75) + pause() + + .. _hysteresis: https://en.wikipedia.org/wiki/Hysteresis + """ + if min_value >= max_value: + raise ValueError('min_value must be smaller than max_value') + min_value = float(min_value) + max_value = float(max_value) + if hysteresis < 0: + raise ValueError("hysteresis must be 0 or larger") + else: + hysteresis = float(hysteresis) + if (max_value - min_value) <= hysteresis: + raise ValueError('The gap between min_value and max_value must be larger than hysteresis') + last_state = None + for v in values: + if v < min_value: + new_state = 'below' + elif v > max_value: + new_state = 'above' + else: + new_state = 'in' + switch = False + if last_state == None or not hysteresis: + switch = True + elif new_state == last_state: + pass + else: # new_state != last_state + if last_state == 'below' and new_state == 'in': + switch = v >= min_value + hysteresis + elif last_state == 'in' and new_state == 'below': + switch = v < min_value - hysteresis + elif last_state == 'in' and new_state == 'above': + switch = v > max_value + hysteresis + elif last_state == 'above' and new_state == 'in': + switch = v <= max_value - hysteresis + else: # above->below or below->above + switch = True + if switch: + last_state = new_state + yield last_state == 'in' def all_values(*values): @@ -218,6 +290,54 @@ def averaged(*values): yield mean(v) +def summed(*values): + """ + Returns the sum of all supplied values. One or more *values* can be + specified. For example, to light a :class:`PWMLED` as the (scaled) sum of + several potentiometers connected to an :class:`MCP3008` ADC:: + + from gpiozero import MCP3008, PWMLED + from gpiozero.tools import summed, scaled + from signal import pause + + pot1 = MCP3008(channel=0) + pot2 = MCP3008(channel=1) + pot3 = MCP3008(channel=2) + led = PWMLED(4) + led.source = scaled(summed(pot1.values, pot2.values, pot3.values), 0, 1, 0, 3) + pause() + """ + for v in zip(*values): + yield sum(v) + + +def multiplied(*values): + """ + Returns the product of all supplied values. One or more *values* can be + specified. For example, to light a :class:`PWMLED` as the product (i.e. + multiplication) of several potentiometers connected to an :class:`MCP3008` + ADC:: + + from gpiozero import MCP3008, PWMLED + from gpiozero.tools import multiplied + from signal import pause + + pot1 = MCP3008(channel=0) + pot2 = MCP3008(channel=1) + pot3 = MCP3008(channel=2) + led = PWMLED(4) + led.source = multiplied(pot1.values, pot2.values, pot3.values) + pause() + """ + def _product(it): + p = 1 + for n in it: + p *= n + return p + for v in zip(*values): + yield _product(v) + + def queued(values, qsize): """ Queues up readings from *values* (the number of readings queued is @@ -236,6 +356,8 @@ def queued(values, qsize): leds[4].source = btn.values pause() """ + if qsize < 1: + raise ValueError("qsize must be 1 or larger") q = [] it = iter(values) for i in range(qsize): @@ -248,10 +370,41 @@ def queued(values, qsize): break +def smoothed(values, qsize, average=mean): + """ + Queues up readings from *values* (the number of readings queued is + determined by *qsize*) and begins yielding the *average* of the last + *qsize* values when the queue is full. The larger the *qsize*, the more the + values are smoothed. For example, to smooth the analog values read from an + ADC:: + + from gpiozero import MCP3008 + from gpiozero.tools import smoothed + + with MCP3008(channel=0) as adc: + for value in smoothed(adc.values, 5): + print value + """ + if qsize < 1: + raise ValueError("qsize must be 1 or larger") + q = [] + it = iter(values) + for i in range(qsize): + q.append(next(it)) + for i in cycle(range(qsize)): + yield average(q) + try: + q[i] = next(it) + except StopIteration: + break + + def pre_delayed(values, delay): """ Waits for *delay* seconds before returning each item from *values*. """ + if delay < 0: + raise ValueError("delay must be 0 or larger") for v in values: sleep(delay) yield v @@ -261,11 +414,78 @@ def post_delayed(values, delay): """ Waits for *delay* seconds after returning each item from *values*. """ + if delay < 0: + raise ValueError("delay must be 0 or larger") for v in values: yield v sleep(delay) +def pre_periodic_filtered(values, block, repeat_after): + """ + Blocks the first *block* items from *values*, repeating the block after + every *repeat_after* items, if *repeat_after* is non-zero. For example, to + discard the first 50 values read from an ADC:: + + from gpiozero import MCP3008 + from gpiozero.tools import pre_periodic_filtered + + with MCP3008(channel=0) as adc: + for value in pre_periodic_filtered(adc.values, 50, 0): + print value + + Or to only display every even item read from an ADC:: + + from gpiozero import MCP3008 + from gpiozero.tools import pre_periodic_filtered + + with MCP3008(channel=0) as adc: + for value in pre_periodic_filtered(adc.values, 1, 1): + print value + """ + if block < 1: + raise ValueError("block must be 1 or larger") + if repeat_after < 0: + raise ValueError("repeat_after must be 0 or larger") + it = iter(values) + if repeat_after == 0: + for _ in range(block): + next(it) + while True: + yield next(it) + else: + while True: + for _ in range(block): + next(it) + for _ in range(repeat_after): + yield next(it) + + +def post_periodic_filtered(values, repeat_after, block): + """ + After every *repeat_after* items, blocks the next *block* items from + *values*. Note that unlike :func:`pre_periodic_filtered`, *repeat_after* + can't be 0. For example, to block every tenth item read from an ADC:: + + from gpiozero import MCP3008 + from gpiozero.tools import post_periodic_filtered + + with MCP3008(channel=0) as adc: + for value in post_periodic_filtered(adc.values, 9, 1): + print value + """ + if repeat_after < 1: + raise ValueError("repeat_after must be 1 or larger") + if block < 1: + raise ValueError("block must be 1 or larger") + it = iter(values) + while True: + for _ in range(repeat_after): + yield next(it) + for _ in range(block): + next(it) + + def random_values(): """ Provides an infinite source of random values between 0 and 1. For example, @@ -298,7 +518,7 @@ def sin_values(period=360): red = PWMLED(2) blue = PWMLED(3) red.source_delay = 0.01 - blue.source_delay = 0.01 + blue.source_delay = red.source_delay red.source = scaled(sin_values(100), 0, 1, -1, 1) blue.source = inverted(red.values) pause() @@ -323,7 +543,7 @@ def cos_values(period=360): red = PWMLED(2) blue = PWMLED(3) red.source_delay = 0.01 - blue.source_delay = 0.01 + blue.source_delay = red.source_delay red.source = scaled(cos_values(100), 0, 1, -1, 1) blue.source = inverted(red.values) pause() diff --git a/setup.py b/setup.py index 396aa34..1256aba 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,14 @@ if sys.version_info[:2] == (3, 2): __extra_requires__['test'][1] = 'coverage<4.0dev' __entry_points__ = { + 'gpiozero_pin_factories': [ + 'PiGPIOPin = gpiozero.pins.pigpiod:PiGPIOPin', + 'RPiGPIOPin = gpiozero.pins.rpigpio:RPiGPIOPin', + 'RPIOPin = gpiozero.pins.rpio:RPIOPin', + 'NativePin = gpiozero.pins.native:NativePin', + 'MockPin = gpiozero.pins.mock:MockPin', + 'MockPWMPin = gpiozero.pins.mock:MockPWMPin', + ], } diff --git a/tests/test_boards.py b/tests/test_boards.py index 304e128..d1accde 100644 --- a/tests/test_boards.py +++ b/tests/test_boards.py @@ -18,10 +18,10 @@ from gpiozero import * def setup_function(function): import gpiozero.devices # dirty, but it does the job - if function.__name__ in ('test_robot', 'test_ryanteck_robot', 'test_camjam_kit_robot'): - gpiozero.devices.DefaultPin = MockPWMPin + if function.__name__ in ('test_robot', 'test_ryanteck_robot', 'test_camjam_kit_robot', 'test_led_borg', 'test_snow_pi_initial_value_pwm'): + gpiozero.devices.pin_factory = MockPWMPin else: - gpiozero.devices.DefaultPin = MockPin + gpiozero.devices.pin_factory = MockPin def teardown_function(function): MockPin.clear_pins() @@ -71,6 +71,10 @@ def test_led_board_on_off(): assert isinstance(board[0], LED) assert isinstance(board[1], LED) assert isinstance(board[2], LED) + assert board.active_high + assert board[0].active_high + assert board[1].active_high + assert board[2].active_high board.on() assert all((pin1.state, pin2.state, pin3.state)) board.off() @@ -85,6 +89,121 @@ def test_led_board_on_off(): assert not pin1.state assert pin2.state assert pin3.state + board.toggle(0,1) + assert board.value == (1, 0, 1) + assert pin1.state + assert not pin2.state + assert pin3.state + board.off(2) + assert board.value == (1, 0, 0) + assert pin1.state + assert not pin2.state + assert not pin3.state + board.on(1) + assert board.value == (1, 1, 0) + assert pin1.state + assert pin2.state + assert not pin3.state + board.off(0,1) + assert board.value == (0, 0, 0) + assert not pin1.state + assert not pin2.state + assert not pin3.state + board.on(1,2) + assert board.value == (0, 1, 1) + assert not pin1.state + assert pin2.state + assert pin3.state + board.toggle(0) + assert board.value == (1, 1, 1) + assert pin1.state + assert pin2.state + assert pin3.state + +def test_led_board_active_low(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + with LEDBoard(pin1, pin2, foo=pin3, active_high=False) as board: + assert not board.active_high + assert not board[0].active_high + assert not board[1].active_high + assert not board[2].active_high + board.on() + assert not any ((pin1.state, pin2.state, pin3.state)) + board.off() + assert all((pin1.state, pin2.state, pin3.state)) + board[0].on() + assert board.value == (1, 0, 0) + assert not pin1.state + assert pin2.state + assert pin3.state + board.toggle() + assert board.value == (0, 1, 1) + assert pin1.state + assert not pin2.state + assert not pin3.state + +def test_led_board_value(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + with LEDBoard(pin1, pin2, foo=pin3) as board: + assert board.value == (0, 0, 0) + board.value = (0, 1, 0) + assert board.value == (0, 1, 0) + board.value = (1, 0, 1) + assert board.value == (1, 0, 1) + +def test_led_board_pwm_value(): + pin1 = MockPWMPin(2) + pin2 = MockPWMPin(3) + pin3 = MockPWMPin(4) + with LEDBoard(pin1, pin2, foo=pin3, pwm=True) as board: + assert board.value == (0, 0, 0) + board.value = (0, 1, 0) + assert board.value == (0, 1, 0) + board.value = (0.5, 0, 0.75) + assert board.value == (0.5, 0, 0.75) + +def test_led_board_pwm_bad_value(): + pin1 = MockPWMPin(2) + pin2 = MockPWMPin(3) + pin3 = MockPWMPin(4) + with LEDBoard(pin1, pin2, foo=pin3, pwm=True) as board: + with pytest.raises(ValueError): + board.value = (-1, 0, 0) + with pytest.raises(ValueError): + board.value = (0, 2, 0) + +def test_led_board_initial_value(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + with LEDBoard(pin1, pin2, foo=pin3, initial_value=0) as board: + assert board.value == (0, 0, 0) + with LEDBoard(pin1, pin2, foo=pin3, initial_value=1) as board: + assert board.value == (1, 1, 1) + +def test_led_board_pwm_initial_value(): + pin1 = MockPWMPin(2) + pin2 = MockPWMPin(3) + pin3 = MockPWMPin(4) + with LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=0) as board: + assert board.value == (0, 0, 0) + with LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=1) as board: + assert board.value == (1, 1, 1) + with LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=0.5) as board: + assert board.value == (0.5, 0.5, 0.5) + +def test_led_board_pwm_bad_initial_value(): + pin1 = MockPWMPin(2) + pin2 = MockPWMPin(3) + pin3 = MockPWMPin(4) + with pytest.raises(ValueError): + LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=-1) + with pytest.raises(ValueError): + LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=2) def test_led_board_nested(): pin1 = MockPin(2) @@ -159,7 +278,7 @@ def test_led_board_blink_control(): board.blink(0.1, 0.1, n=2) # make sure the blink thread's started while not board._blink_leds: - sleep(0.00001) + sleep(0.00001) # pragma: no cover board[1][0].off() # immediately take over the second LED board._blink_thread.join() # naughty, but ensures no arbitrary waits in the test test = [ @@ -206,7 +325,7 @@ def test_led_board_blink_control_all(): board.blink(0.1, 0.1, n=2) # make sure the blink thread's started while not board._blink_leds: - sleep(0.00001) + sleep(0.00001) # pragma: no cover board[0].off() # immediately take over all LEDs board[1][0].off() board[1][1].off() @@ -285,13 +404,24 @@ def test_led_bar_graph_value(): pin2 = MockPin(3) pin3 = MockPin(4) with LEDBarGraph(pin1, pin2, pin3) as graph: + assert isinstance(graph[0], LED) + assert isinstance(graph[1], LED) + assert isinstance(graph[2], LED) + assert graph.active_high + assert graph[0].active_high + assert graph[1].active_high + assert graph[2].active_high graph.value = 0 + assert graph.value == 0 assert not any((pin1.state, pin2.state, pin3.state)) graph.value = 1 + assert graph.value == 1 assert all((pin1.state, pin2.state, pin3.state)) graph.value = 1/3 + assert graph.value == 1/3 assert pin1.state and not (pin2.state or pin3.state) graph.value = -1/3 + assert graph.value == -1/3 assert pin3.state and not (pin1.state or pin2.state) pin1.state = True pin2.state = True @@ -302,31 +432,102 @@ def test_led_bar_graph_value(): pin1.state = False assert graph.value == -2/3 +def test_led_bar_graph_active_low(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + with LEDBarGraph(pin1, pin2, pin3, active_high=False) as graph: + assert not graph.active_high + assert not graph[0].active_high + assert not graph[1].active_high + assert not graph[2].active_high + graph.value = 0 + assert graph.value == 0 + assert all((pin1.state, pin2.state, pin3.state)) + graph.value = 1 + assert graph.value == 1 + assert not any((pin1.state, pin2.state, pin3.state)) + graph.value = 1/3 + assert graph.value == 1/3 + assert not pin1.state and pin2.state and pin3.state + graph.value = -1/3 + assert graph.value == -1/3 + assert not pin3.state and pin1.state and pin2.state + def test_led_bar_graph_pwm_value(): pin1 = MockPWMPin(2) pin2 = MockPWMPin(3) pin3 = MockPWMPin(4) with LEDBarGraph(pin1, pin2, pin3, pwm=True) as graph: + assert isinstance(graph[0], PWMLED) + assert isinstance(graph[1], PWMLED) + assert isinstance(graph[2], PWMLED) graph.value = 0 + assert graph.value == 0 assert not any((pin1.state, pin2.state, pin3.state)) graph.value = 1 + assert graph.value == 1 assert all((pin1.state, pin2.state, pin3.state)) graph.value = 1/3 + assert graph.value == 1/3 assert pin1.state and not (pin2.state or pin3.state) graph.value = -1/3 + assert graph.value == -1/3 assert pin3.state and not (pin1.state or pin2.state) graph.value = 1/2 + assert graph.value == 1/2 assert (pin1.state, pin2.state, pin3.state) == (1, 0.5, 0) pin1.state = 0 pin3.state = 1 assert graph.value == -1/2 +def test_led_bar_graph_bad_value(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + with LEDBarGraph(pin1, pin2, pin3) as graph: + with pytest.raises(ValueError): + graph.value = -2 + with pytest.raises(ValueError): + graph.value = 2 + def test_led_bar_graph_bad_init(): pin1 = MockPin(2) pin2 = MockPin(3) pin3 = MockPin(4) with pytest.raises(TypeError): LEDBarGraph(pin1, pin2, foo=pin3) + with pytest.raises(ValueError): + LEDBarGraph(pin1, pin2, pin3, initial_value=-2) + with pytest.raises(ValueError): + LEDBarGraph(pin1, pin2, pin3, initial_value=2) + +def test_led_bar_graph_initial_value(): + pin1 = MockPin(2) + pin2 = MockPin(3) + pin3 = MockPin(4) + with LEDBarGraph(pin1, pin2, pin3, initial_value=1/3) as graph: + assert graph.value == 1/3 + assert pin1.state and not (pin2.state or pin3.state) + with LEDBarGraph(pin1, pin2, pin3, initial_value=-1/3) as graph: + assert graph.value == -1/3 + assert pin3.state and not (pin1.state or pin2.state) + +def test_led_bar_graph_pwm_initial_value(): + pin1 = MockPWMPin(2) + pin2 = MockPWMPin(3) + pin3 = MockPWMPin(4) + with LEDBarGraph(pin1, pin2, pin3, pwm=True, initial_value=0.5) as graph: + assert graph.value == 0.5 + assert (pin1.state, pin2.state, pin3.state) == (1, 0.5, 0) + with LEDBarGraph(pin1, pin2, pin3, pwm=True, initial_value=-0.5) as graph: + assert graph.value == -0.5 + assert (pin1.state, pin2.state, pin3.state) == (0, 0.5, 1) + +def test_led_borg(): + pins = [MockPWMPin(n) for n in (17, 27, 22)] + with LedBorg() as board: + assert [device.pin for device in board._leds] == pins def test_pi_liter(): pins = [MockPin(n) for n in (4, 17, 27, 18, 22, 23, 24, 25)] @@ -347,13 +548,32 @@ def test_traffic_lights(): green_pin = MockPin(4) with TrafficLights(red_pin, amber_pin, green_pin) as board: board.red.on() + assert board.red.value + assert not board.amber.value + assert not board.yellow.value + assert not board.green.value assert red_pin.state assert not amber_pin.state assert not green_pin.state + with TrafficLights(red=red_pin, yellow=amber_pin, green=green_pin) as board: + board.yellow.on() + assert not board.red.value + assert board.amber.value + assert board.yellow.value + assert not board.green.value + assert not red_pin.state + assert amber_pin.state + assert not green_pin.state def test_traffic_lights_bad_init(): with pytest.raises(ValueError): TrafficLights() + red_pin = MockPin(2) + amber_pin = MockPin(3) + green_pin = MockPin(4) + yellow_pin = MockPin(5) + with pytest.raises(ValueError): + TrafficLights(red=red_pin, amber=amber_pin, yellow=yellow_pin, green=green_pin) def test_pi_traffic(): pins = [MockPin(n) for n in (9, 10, 11)] @@ -365,6 +585,22 @@ def test_snow_pi(): with SnowPi() as board: assert [device.pin for device in board.leds] == pins +def test_snow_pi_initial_value(): + with SnowPi() as board: + assert all(device.pin.state == False for device in board.leds) + with SnowPi(initial_value=False) as board: + assert all(device.pin.state == False for device in board.leds) + with SnowPi(initial_value=True) as board: + assert all(device.pin.state == True for device in board.leds) + with SnowPi(initial_value=0.5) as board: + assert all(device.pin.state == True for device in board.leds) + +def test_snow_pi_initial_value_pwm(): + pins = [MockPWMPin(n) for n in (23, 24, 25, 17, 18, 22, 7, 8, 9)] + with SnowPi(pwm=True, initial_value=0.5) as board: + assert [device.pin for device in board.leds] == pins + assert all(device.pin.state == 0.5 for device in board.leds) + def test_traffic_lights_buzzer(): red_pin = MockPin(2) amber_pin = MockPin(3) @@ -400,20 +636,34 @@ def test_robot(): assert ( [device.pin for device in robot.left_motor] + [device.pin for device in robot.right_motor]) == pins + assert robot.value == (0, 0) robot.forward() assert [pin.state for pin in pins] == [1, 0, 1, 0] + assert robot.value == (1, 1) robot.backward() assert [pin.state for pin in pins] == [0, 1, 0, 1] + assert robot.value == (-1, -1) robot.forward(0.5) assert [pin.state for pin in pins] == [0.5, 0, 0.5, 0] + assert robot.value == (0.5, 0.5) robot.left() assert [pin.state for pin in pins] == [0, 1, 1, 0] + assert robot.value == (-1, 1) robot.right() assert [pin.state for pin in pins] == [1, 0, 0, 1] + assert robot.value == (1, -1) robot.reverse() assert [pin.state for pin in pins] == [0, 1, 1, 0] + assert robot.value == (-1, 1) robot.stop() assert [pin.state for pin in pins] == [0, 0, 0, 0] + assert robot.value == (0, 0) + robot.value = (-1, -1) + assert robot.value == (-1, -1) + robot.value = (0.5, 1) + assert robot.value == (0.5, 1) + robot.value = (0, -0.5) + assert robot.value == (0, -0.5) def test_ryanteck_robot(): pins = [MockPWMPin(n) for n in (17, 18, 22, 23)] @@ -430,11 +680,15 @@ def test_energenie_bad_init(): Energenie() with pytest.raises(ValueError): Energenie(0) + with pytest.raises(ValueError): + Energenie(5) def test_energenie(): pins = [MockPin(n) for n in (17, 22, 23, 27, 24, 25)] with Energenie(1, initial_value=True) as device1, \ Energenie(2, initial_value=False) as device2: + assert repr(device1) == '' + assert repr(device2) == '' assert device1.value assert not device2.value [pin.clear_states() for pin in pins] @@ -455,4 +709,5 @@ def test_energenie(): pins[3].assert_states_and_times([(0.0, True), (0.0, True)]) pins[4].assert_states_and_times([(0.0, False)]) pins[5].assert_states_and_times([(0.0, False), (0.1, True), (0.25, False)]) - + device1.close() + assert repr(device1) == '' diff --git a/tests/test_compat.py b/tests/test_compat.py index a3e6e28..c58fe7c 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -120,6 +120,7 @@ def test_mean(): values = list(values) random.shuffle(values) assert mean(values) == result + assert mean(iter(values)) == result def test_mean_big_data(): c = 1e9 diff --git a/tests/test_inputs.py b/tests/test_inputs.py index ee1d62a..41a8e56 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -34,13 +34,25 @@ def test_input_initial_values(): assert pin.pull == 'down' assert not device.pull_up -def test_input_is_active(): +def test_input_is_active_low(): pin = MockPin(2) with InputDevice(pin, pull_up=True) as device: pin.drive_high() assert not device.is_active + assert repr(device) == '' pin.drive_low() assert device.is_active + assert repr(device) == '' + +def test_input_is_active_high(): + pin = MockPin(2) + with InputDevice(pin, pull_up=False) as device: + pin.drive_high() + assert device.is_active + assert repr(device) == '' + pin.drive_low() + assert not device.is_active + assert repr(device) == '' def test_input_pulled_up(): pin = MockPulledUpPin(2) @@ -52,20 +64,20 @@ def test_input_event_activated(): pin = MockPin(2) with DigitalInputDevice(pin) as device: device.when_activated = lambda: event.set() - assert not event.wait(0) + assert not event.is_set() pin.drive_high() - assert event.wait(0) + assert event.is_set() def test_input_event_deactivated(): event = Event() pin = MockPin(2) with DigitalInputDevice(pin) as device: device.when_deactivated = lambda: event.set() - assert not event.wait(0) + assert not event.is_set() pin.drive_high() - assert not event.wait(0) + assert not event.is_set() pin.drive_low() - assert event.wait(0) + assert event.is_set() def test_input_wait_active(): pin = MockPin(2) @@ -83,11 +95,14 @@ def test_input_wait_inactive(): def test_input_smoothed_attrib(): pin = MockPin(2) with SmoothedInputDevice(pin, threshold=0.5, queue_len=5, partial=False) as device: + assert repr(device) == '' assert device.threshold == 0.5 assert device.queue_len == 5 assert not device.partial device._queue.start() assert not device.is_active + with pytest.raises(InputDeviceError): + device.threshold = 1 def test_input_smoothed_values(): pin = MockPin(2) diff --git a/tests/test_mock_pin.py b/tests/test_mock_pin.py index 92e1d3d..c8bdb95 100644 --- a/tests/test_mock_pin.py +++ b/tests/test_mock_pin.py @@ -160,15 +160,15 @@ def test_mock_pin_edges(): pin.when_changed = changed pin.drive_high() assert pin.state - assert fired.wait(0) + assert fired.is_set() fired.clear() pin.edges = 'falling' pin.drive_low() assert not pin.state - assert fired.wait(0) + assert fired.is_set() fired.clear() pin.drive_high() assert pin.state - assert not fired.wait(0) + assert not fired.is_set() assert pin.edges == 'falling' diff --git a/tests/test_outputs.py b/tests/test_outputs.py index 27bc97d..eabc414 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -8,7 +8,7 @@ str = type('') import sys -from time import sleep +from time import sleep, time try: from math import isclose except ImportError: @@ -53,6 +53,9 @@ def test_output_write_active_low(): def test_output_write_closed(): with OutputDevice(MockPin(2)) as device: device.close() + assert device.closed + device.close() + assert device.closed with pytest.raises(GPIODeviceClosed): device.on() @@ -92,8 +95,11 @@ def test_output_digital_toggle(): def test_output_blink_background(): pin = MockPin(2) with DigitalOutputDevice(pin) as device: + start = time() device.blink(0.1, 0.1, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) device._blink_thread.join() # naughty, but ensures no arbitrary waits in the test + assert isclose(time() - start, 0.4, abs_tol=0.05) pin.assert_states_and_times([ (0.0, False), (0.0, True), @@ -107,7 +113,9 @@ def test_output_blink_background(): def test_output_blink_foreground(): pin = MockPin(2) with DigitalOutputDevice(pin) as device: + start = time() device.blink(0.1, 0.1, n=2, background=False) + assert isclose(time() - start, 0.4, abs_tol=0.05) pin.assert_states_and_times([ (0.0, False), (0.0, True), @@ -209,8 +217,11 @@ def test_output_pwm_write_silly(): def test_output_pwm_blink_background(): pin = MockPWMPin(2) with PWMOutputDevice(pin) as device: + start = time() device.blink(0.1, 0.1, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) device._blink_thread.join() + assert isclose(time() - start, 0.4, abs_tol=0.05) pin.assert_states_and_times([ (0.0, 0), (0.0, 1), @@ -224,7 +235,9 @@ def test_output_pwm_blink_background(): def test_output_pwm_blink_foreground(): pin = MockPWMPin(2) with PWMOutputDevice(pin) as device: + start = time() device.blink(0.1, 0.1, n=2, background=False) + assert isclose(time() - start, 0.4, abs_tol=0.05) pin.assert_states_and_times([ (0.0, 0), (0.0, 1), @@ -238,8 +251,11 @@ def test_output_pwm_blink_foreground(): def test_output_pwm_fade_background(): pin = MockPWMPin(2) with PWMOutputDevice(pin) as device: + start = time() device.blink(0, 0, 0.2, 0.2, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) device._blink_thread.join() + assert isclose(time() - start, 0.8, abs_tol=0.05) pin.assert_states_and_times([ (0.0, 0), (0.04, 0.2), @@ -269,7 +285,75 @@ def test_output_pwm_fade_background(): def test_output_pwm_fade_foreground(): pin = MockPWMPin(2) with PWMOutputDevice(pin) as device: + start = time() device.blink(0, 0, 0.2, 0.2, n=2, background=False) + assert isclose(time() - start, 0.8, abs_tol=0.05) + pin.assert_states_and_times([ + (0.0, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + ]) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_output_pwm_pulse_background(): + pin = MockPWMPin(2) + with PWMOutputDevice(pin) as device: + start = time() + device.pulse(0.2, 0.2, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) + device._blink_thread.join() + assert isclose(time() - start, 0.8, abs_tol=0.05) + pin.assert_states_and_times([ + (0.0, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + ]) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_output_pwm_pulse_foreground(): + pin = MockPWMPin(2) + with PWMOutputDevice(pin) as device: + start = time() + device.pulse(0.2, 0.2, n=2, background=False) + assert isclose(time() - start, 0.8, abs_tol=0.05) pin.assert_states_and_times([ (0.0, 0), (0.04, 0.2), @@ -316,9 +400,47 @@ def test_rgbled_initial_value(): assert isclose(g.state, 0.2) assert isclose(b.state, 0.0) +def test_rgbled_initial_value_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False, initial_value=(0, 1, 1)) as device: + assert r.state == 0 + assert g.state == 1 + assert b.state == 1 + +def test_rgbled_initial_bad_value(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + with pytest.raises(ValueError): + RGBLED(r, g, b, initial_value=(0.1, 0.2, 1.2)) + +def test_rgbled_initial_bad_value_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with pytest.raises(ValueError): + RGBLED(r, g, b, pwm=False, initial_value=(0.1, 0.2, 0)) + def test_rgbled_value(): r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: + assert isinstance(device._leds[0], PWMLED) + assert isinstance(device._leds[1], PWMLED) + assert isinstance(device._leds[2], PWMLED) + assert not device.is_active + assert device.value == (0, 0, 0) + device.on() + assert device.is_active + assert device.value == (1, 1, 1) + device.off() + assert not device.is_active + assert device.value == (0, 0, 0) + device.value = (0.5, 0.5, 0.5) + assert device.is_active + assert device.value == (0.5, 0.5, 0.5) + +def test_rgbled_value_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + assert isinstance(device._leds[0], LED) + assert isinstance(device._leds[1], LED) + assert isinstance(device._leds[2], LED) assert not device.is_active assert device.value == (0, 0, 0) device.on() @@ -328,6 +450,33 @@ def test_rgbled_value(): assert not device.is_active assert device.value == (0, 0, 0) +def test_rgbled_bad_value(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b) as device: + with pytest.raises(ValueError): + device.value = (2, 0, 0) + with RGBLED(r, g, b) as device: + with pytest.raises(ValueError): + device.value = (0, -1, 0) + +def test_rgbled_bad_value_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + with pytest.raises(ValueError): + device.value = (2, 0, 0) + with RGBLED(r, g, b, pwm=False) as device: + with pytest.raises(ValueError): + device.value = (0, -1, 0) + with RGBLED(r, g, b, pwm=False) as device: + with pytest.raises(ValueError): + device.value = (0.5, 0, 0) + with RGBLED(r, g, b, pwm=False) as device: + with pytest.raises(ValueError): + device.value = (0, 0.5, 0) + with RGBLED(r, g, b, pwm=False) as device: + with pytest.raises(ValueError): + device.value = (0, 0, 0.5) + def test_rgbled_toggle(): r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: @@ -340,13 +489,49 @@ def test_rgbled_toggle(): assert not device.is_active assert device.value == (0, 0, 0) +def test_rgbled_toggle_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + assert not device.is_active + assert device.value == (0, 0, 0) + device.toggle() + assert device.is_active + assert device.value == (1, 1, 1) + device.toggle() + assert not device.is_active + assert device.value == (0, 0, 0) + @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_blink_background(): r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: + start = time() device.blink(0.1, 0.1, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) device._blink_thread.join() + assert isclose(time() - start, 0.4, abs_tol=0.05) + expected = [ + (0.0, 0), + (0.0, 1), + (0.1, 0), + (0.1, 1), + (0.1, 0) + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_rgbled_blink_background_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + start = time() + device.blink(0.1, 0.1, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) + device._blink_thread.join() + assert isclose(time() - start, 0.4, abs_tol=0.05) expected = [ (0.0, 0), (0.0, 1), @@ -363,7 +548,28 @@ def test_rgbled_blink_background(): def test_rgbled_blink_foreground(): r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: + start = time() device.blink(0.1, 0.1, n=2, background=False) + assert isclose(time() - start, 0.4, abs_tol=0.05) + expected = [ + (0.0, 0), + (0.0, 1), + (0.1, 0), + (0.1, 1), + (0.1, 0) + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_rgbled_blink_foreground_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + start = time() + device.blink(0.1, 0.1, n=2, background=False) + assert isclose(time() - start, 0.4, abs_tol=0.05) expected = [ (0.0, 0), (0.0, 1), @@ -380,8 +586,11 @@ def test_rgbled_blink_foreground(): def test_rgbled_fade_background(): r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: + start = time() device.blink(0, 0, 0.2, 0.2, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) device._blink_thread.join() + assert isclose(time() - start, 0.8, abs_tol=0.05) expected = [ (0.0, 0), (0.04, 0.2), @@ -409,7 +618,138 @@ def test_rgbled_fade_background(): g.assert_states_and_times(expected) b.assert_states_and_times(expected) -def test_output_rgbled_blink_interrupt(): +def test_rgbled_fade_background_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + with pytest.raises(ValueError): + device.blink(0, 0, 0.2, 0.2, n=2) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_rgbled_fade_foreground(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b) as device: + start = time() + device.blink(0, 0, 0.2, 0.2, n=2, background=False) + assert isclose(time() - start, 0.8, abs_tol=0.05) + expected = [ + (0.0, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +def test_rgbled_fade_foreground_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + with pytest.raises(ValueError): + device.blink(0, 0, 0.2, 0.2, n=2, background=False) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_rgbled_pulse_background(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b) as device: + start = time() + device.pulse(0.2, 0.2, n=2) + assert isclose(time() - start, 0, abs_tol=0.05) + device._blink_thread.join() + assert isclose(time() - start, 0.8, abs_tol=0.05) + expected = [ + (0.0, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +def test_rgbled_pulse_background_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + with pytest.raises(ValueError): + device.pulse(0.2, 0.2, n=2) + +@pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), + reason='timing is too random on pypy') +def test_rgbled_pulse_foreground(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b) as device: + start = time() + device.pulse(0.2, 0.2, n=2, background=False) + assert isclose(time() - start, 0.8, abs_tol=0.05) + expected = [ + (0.0, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + (0.04, 0.2), + (0.04, 0.4), + (0.04, 0.6), + (0.04, 0.8), + (0.04, 1), + (0.04, 0.8), + (0.04, 0.6), + (0.04, 0.4), + (0.04, 0.2), + (0.04, 0), + ] + r.assert_states_and_times(expected) + g.assert_states_and_times(expected) + b.assert_states_and_times(expected) + +def test_rgbled_pulse_foreground_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + with pytest.raises(ValueError): + device.pulse(0.2, 0.2, n=2, background=False) + +def test_rgbled_blink_interrupt(): r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: device.blink(1, 0.1) @@ -419,6 +759,34 @@ def test_output_rgbled_blink_interrupt(): g.assert_states([0, 1, 0]) b.assert_states([0, 1, 0]) +def test_rgbled_blink_interrupt_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + device.blink(1, 0.1) + sleep(0.2) + device.off() # should interrupt while on + r.assert_states([0, 1, 0]) + g.assert_states([0, 1, 0]) + b.assert_states([0, 1, 0]) + +def test_rgbled_close(): + r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b) as device: + assert not device.closed + device.close() + assert device.closed + device.close() + assert device.closed + +def test_rgbled_close_nonpwm(): + r, g, b = (MockPin(i) for i in (1, 2, 3)) + with RGBLED(r, g, b, pwm=False) as device: + assert not device.closed + device.close() + assert device.closed + device.close() + assert device.closed + def test_motor_missing_pins(): with pytest.raises(ValueError): Motor() @@ -428,7 +796,18 @@ def test_motor_pins(): b = MockPWMPin(2) with Motor(f, b) as device: assert device.forward_device.pin is f + assert isinstance(device.forward_device, PWMOutputDevice) assert device.backward_device.pin is b + assert isinstance(device.backward_device, PWMOutputDevice) + +def test_motor_pins_nonpwm(): + f = MockPin(1) + b = MockPin(2) + with Motor(f, b, pwm=False) as device: + assert device.forward_device.pin is f + assert isinstance(device.forward_device, DigitalOutputDevice) + assert device.backward_device.pin is b + assert isinstance(device.backward_device, DigitalOutputDevice) def test_motor_close(): f = MockPWMPin(1) @@ -438,6 +817,17 @@ def test_motor_close(): assert device.closed assert device.forward_device.pin is None assert device.backward_device.pin is None + device.close() + assert device.closed + +def test_motor_close_nonpwm(): + f = MockPin(1) + b = MockPin(2) + with Motor(f, b, pwm=False) as device: + device.close() + assert device.closed + assert device.forward_device.pin is None + assert device.backward_device.pin is None def test_motor_value(): f = MockPWMPin(1) @@ -455,6 +845,27 @@ def test_motor_value(): assert device.is_active assert device.value == 0.5 assert b.state == 0 and f.state == 0.5 + device.value = -0.5 + assert device.is_active + assert device.value == -0.5 + assert b.state == 0.5 and f.state == 0 + device.value = 0 + assert not device.is_active + assert not device.value + assert b.state == 0 and f.state == 0 + +def test_motor_value_nonpwm(): + f = MockPin(1) + b = MockPin(2) + with Motor(f, b, pwm=False) as device: + device.value = -1 + assert device.is_active + assert device.value == -1 + assert b.state == 1 and f.state == 0 + device.value = 1 + assert device.is_active + assert device.value == 1 + assert b.state == 0 and f.state == 1 device.value = 0 assert not device.is_active assert not device.value @@ -464,9 +875,24 @@ def test_motor_bad_value(): f = MockPWMPin(1) b = MockPWMPin(2) with Motor(f, b) as device: + with pytest.raises(ValueError): + device.value = -2 with pytest.raises(ValueError): device.value = 2 +def test_motor_bad_value_nonpwm(): + f = MockPin(1) + b = MockPin(2) + with Motor(f, b, pwm=False) as device: + with pytest.raises(ValueError): + device.value = -2 + with pytest.raises(ValueError): + device.value = 2 + with pytest.raises(ValueError): + device.value = 0.5 + with pytest.raises(ValueError): + device.value = -0.5 + def test_motor_reverse(): f = MockPWMPin(1) b = MockPWMPin(2) @@ -477,4 +903,132 @@ def test_motor_reverse(): device.reverse() assert device.value == -1 assert b.state == 1 and f.state == 0 + device.backward(0.5) + assert device.value == -0.5 + assert b.state == 0.5 and f.state == 0 + device.reverse() + assert device.value == 0.5 + assert b.state == 0 and f.state == 0.5 + +def test_motor_reverse_nonpwm(): + f = MockPin(1) + b = MockPin(2) + with Motor(f, b, pwm=False) as device: + device.forward() + assert device.value == 1 + assert b.state == 0 and f.state == 1 + device.reverse() + assert device.value == -1 + assert b.state == 1 and f.state == 0 + +def test_servo_pins(): + p = MockPWMPin(1) + with Servo(p) as device: + assert device.pwm_device.pin is p + assert isinstance(device.pwm_device, PWMOutputDevice) + +def test_servo_bad_value(): + p = MockPWMPin(1) + with pytest.raises(ValueError): + Servo(p, initial_value=2) + with pytest.raises(ValueError): + Servo(p, min_pulse_width=30/1000) + with pytest.raises(ValueError): + Servo(p, max_pulse_width=30/1000) + +def test_servo_pins_nonpwm(): + p = MockPin(2) + with pytest.raises(PinPWMUnsupported): + Servo(p) + +def test_servo_close(): + p = MockPWMPin(2) + with Servo(p) as device: + device.close() + assert device.closed + assert device.pwm_device.pin is None + device.close() + assert device.closed + +def test_servo_pulse_width(): + p = MockPWMPin(2) + with Servo(p, min_pulse_width=5/10000, max_pulse_width=25/10000) as device: + assert isclose(device.min_pulse_width, 5/10000) + assert isclose(device.max_pulse_width, 25/10000) + assert isclose(device.frame_width, 20/1000) + assert isclose(device.pulse_width, 15/10000) + device.value = -1 + assert isclose(device.pulse_width, 5/10000) + device.value = 1 + assert isclose(device.pulse_width, 25/10000) + device.value = None + assert device.pulse_width is None + +def test_servo_values(): + p = MockPWMPin(1) + with Servo(p) as device: + device.min() + assert device.is_active + assert device.value == -1 + assert isclose(p.state, 0.05) + device.max() + assert device.is_active + assert device.value == 1 + assert isclose(p.state, 0.1) + device.mid() + assert device.is_active + assert device.value == 0.0 + assert isclose(p.state, 0.075) + device.value = 0.5 + assert device.is_active + assert device.value == 0.5 + assert isclose(p.state, 0.0875) + device.detach() + assert not device.is_active + assert device.value is None + device.value = 0 + assert device.value == 0 + device.value = None + assert device.value is None + +def test_angular_servo_range(): + p = MockPWMPin(1) + with AngularServo(p, initial_angle=15, min_angle=0, max_angle=90) as device: + assert device.min_angle == 0 + assert device.max_angle == 90 + +def test_angular_servo_angles(): + p = MockPWMPin(1) + with AngularServo(p) as device: + device.angle = 0 + assert device.angle == 0 + assert isclose(device.value, 0) + device.max() + assert device.angle == 90 + assert isclose(device.value, 1) + device.min() + assert device.angle == -90 + assert isclose(device.value, -1) + device.detach() + assert device.angle is None + with AngularServo(p, initial_angle=15, min_angle=0, max_angle=90) as device: + assert device.angle == 15 + assert isclose(device.value, -2/3) + device.angle = 0 + assert device.angle == 0 + assert isclose(device.value, -1) + device.angle = 90 + assert device.angle == 90 + assert isclose(device.value, 1) + device.angle = None + assert device.angle is None + with AngularServo(p, min_angle=45, max_angle=-45) as device: + assert device.angle == 0 + assert isclose(device.value, 0) + device.angle = -45 + assert device.angle == -45 + assert isclose(device.value, 1) + device.angle = -15 + assert device.angle == -15 + assert isclose(device.value, 1/3) diff --git a/tests/test_pins_data.py b/tests/test_pins_data.py new file mode 100644 index 0000000..27395d4 --- /dev/null +++ b/tests/test_pins_data.py @@ -0,0 +1,90 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +import pytest +from mock import patch, MagicMock + +import gpiozero.devices +import gpiozero.pins.data +import gpiozero.pins.native +from gpiozero.pins.data import pi_info +from gpiozero import PinMultiplePins, PinNoPins, PinUnknownPi + + +def test_pi_revision(): + save_factory = gpiozero.devices.pin_factory + try: + # Can't use MockPin for this as we want something that'll actually try + # and read /proc/cpuinfo (MockPin simply parrots the 2B's data); + # NativePin is used as we're guaranteed to be able to import it + gpiozero.devices.pin_factory = gpiozero.pins.native.NativePin + with patch('io.open') as m: + m.return_value.__enter__.return_value = ['lots of irrelevant', 'lines', 'followed by', 'Revision: 0002', 'Serial: xxxxxxxxxxx'] + assert pi_info().revision == '0002' + # LocalPin caches the revision (because realistically it isn't going to + # change at runtime); we need to wipe it here though + gpiozero.pins.native.NativePin._PI_REVISION = None + m.return_value.__enter__.return_value = ['Revision: a21042'] + assert pi_info().revision == 'a21042' + # Check over-volting result (some argument over whether this is 7 or + # 8 character result; make sure both work) + gpiozero.pins.native.NativePin._PI_REVISION = None + m.return_value.__enter__.return_value = ['Revision: 1000003'] + assert pi_info().revision == '0003' + gpiozero.pins.native.NativePin._PI_REVISION = None + m.return_value.__enter__.return_value = ['Revision: 100003'] + assert pi_info().revision == '0003' + with pytest.raises(PinUnknownPi): + m.return_value.__enter__.return_value = ['nothing', 'relevant', 'at all'] + gpiozero.pins.native.NativePin._PI_REVISION = None + pi_info() + with pytest.raises(PinUnknownPi): + pi_info('0fff') + finally: + gpiozero.devices.pin_factory = save_factory + +def test_pi_info(): + r = pi_info('900011') + assert r.model == 'B' + assert r.pcb_revision == '1.0' + assert r.memory == 512 + assert r.manufacturer == 'Sony' + assert r.storage == 'SD' + assert r.usb == 2 + assert not r.wifi + assert not r.bluetooth + assert r.csi == 1 + assert r.dsi == 1 + with pytest.raises(PinUnknownPi): + pi_info('9000f1') + +def test_pi_info_other_types(): + with pytest.raises(PinUnknownPi): + pi_info(b'9000f1') + with pytest.raises(PinUnknownPi): + pi_info(0x9000f1) + +def test_physical_pins(): + # Assert physical pins for some well-known Pi's; a21041 is a Pi2B + assert pi_info('a21041').physical_pins('3V3') == {('P1', 1), ('P1', 17)} + assert pi_info('a21041').physical_pins('GPIO2') == {('P1', 3)} + assert pi_info('a21041').physical_pins('GPIO47') == set() + +def test_physical_pin(): + with pytest.raises(PinMultiplePins): + assert pi_info('a21041').physical_pin('GND') + assert pi_info('a21041').physical_pin('GPIO3') == ('P1', 5) + with pytest.raises(PinNoPins): + assert pi_info('a21041').physical_pin('GPIO47') + +def test_pulled_up(): + assert pi_info('a21041').pulled_up('GPIO2') + assert not pi_info('a21041').pulled_up('GPIO4') + assert not pi_info('a21041').pulled_up('GPIO47') + diff --git a/tests/test_tools.py b/tests/test_tools.py index 9749b80..a06109e 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -16,6 +16,14 @@ try: from math import isclose except ImportError: from gpiozero.compat import isclose +try: + from statistics import mean +except ImportError: + from gpiozero.compat import mean +try: + from statistics import median +except ImportError: + from gpiozero.compat import median def test_negated(): @@ -23,24 +31,108 @@ def test_negated(): assert list(negated((True, True, False, False))) == [False, False, True, True] def test_inverted(): + with pytest.raises(ValueError): + list(inverted((), 0, 0)) + with pytest.raises(ValueError): + list(inverted((), 1, 1)) + with pytest.raises(ValueError): + list(inverted((), 1, 0)) assert list(inverted(())) == [] assert list(inverted((1, 0, 0.1, 0.5))) == [0, 1, 0.9, 0.5] + assert list(inverted((1, 0, 0.1, 0.5), 0, 1)) == [0, 1, 0.9, 0.5] + assert list(inverted((-1, 0, -0.1, -0.5), -1, 0)) == [0, -1, -0.9, -0.5] + assert list(inverted((1, 0, 0.1, 0.5, -1, -0.1, -0.5), -1, 1)) == [-1, 0, -0.1, -0.5, 1, 0.1, 0.5] + assert list(inverted((2, 1, 1.1, 1.5), 1, 2)) == [1, 2, 1.9, 1.5] def test_scaled(): + with pytest.raises(ValueError): + list(scaled((), 0, 1, 0, 0)) + with pytest.raises(ValueError): + list(scaled((), 0, 1, 1, 1)) + with pytest.raises(ValueError): + list(scaled((), 0, 1, 1, 0)) + assert list(scaled((), 0, 1)) == [] + # no scale assert list(scaled((0, 1, 0.5, 0.1), 0, 1)) == [0, 1, 0.5, 0.1] + assert list(scaled((0, 1, 0.5, 0.1), 0, 1, 0, 1)) == [0, 1, 0.5, 0.1] + # multiply by 2 + assert list(scaled((0, 1, 0.5, 0.1), 0, 2, 0, 1)) == [0, 2, 1, 0.2] + # add 1 + assert list(scaled((0, 1, 0.5, 0.1), 1, 2, 0, 1)) == [1, 2, 1.5, 1.1] + # multiply by 2 then add 1 + assert list(scaled((0, 1, 0.5, 0.1), 1, 3, 0, 1)) == [1, 3, 2, 1.2] + # add 1 then multiply by 2 + assert list(scaled((0, 1, 0.5, 0.1), 2, 4, 0, 1)) == [2, 4, 3, 2.2] + # invert + assert list(scaled((0, 1, 0.5, 0.1), 1, 0, 0, 1)) == [1, 0, 0.5, 0.9] + # multiply by -1 then subtract 1 + assert list(scaled((0, 1, 0.5, 0.1), -1, -2, 0, 1)) == [-1, -2, -1.5, -1.1] + # scale 0->1 to -1->+1 assert list(scaled((0, 1, 0.5, 0.1), -1, 1)) == [-1, 1, 0.0, -0.8] + assert list(scaled((0, 1, 0.5, 0.1), -1, 1, 0, 1)) == [-1, 1, 0.0, -0.8] + # scale -1->+1 to 0->1 + assert list(scaled((-1, 1, 0.0, -0.5), 0, 1, -1, 1)) == [0, 1, 0.5, 0.25] def test_clamped(): - assert list(clamped((-1, 0, 1, 2))) == [0, 0, 1, 1] + with pytest.raises(ValueError): + list(clamped((), 0, 0)) + with pytest.raises(ValueError): + list(clamped((), 1, 1)) + with pytest.raises(ValueError): + list(clamped((), 1, 0)) + assert list(clamped(())) == [] + assert list(clamped((-2, -1, -0.5, 0, 0.5, 1, 2))) == [0, 0, 0, 0, 0.5, 1, 1] + assert list(clamped((-2, -1, -0.5, 0, 0.5, 1, 2), 0, 1)) == [0, 0, 0, 0, 0.5, 1, 1] + assert list(clamped((-2, -1, -0.5, 0, 0.5, 1, 2), -1, 1)) == [-1, -1, -0.5, 0, 0.5, 1, 1] + assert list(clamped((-2, -1, -0.5, 0, 0.5, 1, 2), -2, 2)) == [-2, -1, -0.5, 0, 0.5, 1, 2] def test_absoluted(): + assert list(absoluted(())) == [] assert list(absoluted((-2, -1, 0, 1, 2))) == [2, 1, 0, 1, 2] def test_quantized(): + with pytest.raises(ValueError): + list(quantized((), 0)) + with pytest.raises(ValueError): + list(quantized((), 4, 0, 0)) + with pytest.raises(ValueError): + list(quantized((), 4, 1, 1)) + with pytest.raises(ValueError): + list(quantized((), 4, 1, 0)) + assert list(quantized((), 4)) == [] assert list(quantized((0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1), 4)) == [ 0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.5, 0.75, 0.75, 1.0] + assert list(quantized((0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1), 4, 0, 1)) == [ + 0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.5, 0.75, 0.75, 1.0] assert list(quantized((0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1), 5)) == [ 0.0, 0.0, 0.2, 0.2, 0.4, 0.4, 0.6, 0.6, 0.8, 0.8, 1.0] + assert list(quantized((0, 0.25, 0.5, 0.75, 1.0, 1.5, 1.75, 2.0), 2, 0, 2)) == [ + 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0] + assert list(quantized((1, 1.25, 1.5, 1.75, 2.0, 2.5, 2.75, 3.0), 2, 1, 3)) == [ + 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 3.0] + +def test_booleanized(): + with pytest.raises(ValueError): + list(booleanized((), 0, 0)) + with pytest.raises(ValueError): + list(booleanized((), 1, 1)) + with pytest.raises(ValueError): + list(booleanized((), 1, 0)) + with pytest.raises(ValueError): + list(booleanized((), 0, 0.5, -0.2)) + with pytest.raises(ValueError): + list(booleanized((), 0, 0.5, 0.5)) + assert list(booleanized((), 0, 0.5)) == [] + assert list(booleanized((0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1), 0, 0.5)) == [ + True, True, True, True, True, True, False, False, False, False, False] + assert list(booleanized((0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 0), 0.25, 0.75)) == [ + False, False, False, True, True, True, True, True, False, False, False, False] + assert list(booleanized((0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 0), 0.25, 0.75, 0.2)) == [ + False, False, False, False, False, True, True, True, True, True, False, False] + assert list(booleanized((1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0, 1), 0.25, 0.75)) == [ + False, False, False, True, True, True, True, True, False, False, False, False] + assert list(booleanized((1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0, 1), 0.25, 0.75, 0.2)) == [ + False, False, False, False, False, True, True, True, True, True, False, False] def test_all_values(): assert list(all_values(())) == [] @@ -54,38 +146,127 @@ def test_any_values(): def test_averaged(): assert list(averaged(())) == [] + assert list(averaged((0, 0.5, 1))) == [0, 0.5, 1] assert list(averaged((0, 0.5, 1), (1, 1, 1))) == [0.5, 0.75, 1] +def test_summed(): + assert list(summed(())) == [] + assert list(summed((0, 0.5, 0.5, 1))) == [0, 0.5, 0.5, 1] + assert list(summed((0, 0.5, 0.5, 1), (1, 0.5, 1, 1))) == [1, 1, 1.5, 2] + +def test_multiplied(): + assert list(multiplied(())) == [] + assert list(multiplied((0, 0.5, 0.5, 1))) == [0, 0.5, 0.5, 1] + assert list(multiplied((0, 0.5, 0.5, 1), (1, 0.5, 1, 1))) == [0, 0.25, 0.5, 1] + def test_queued(): + with pytest.raises(ValueError): + list(queued((), 0)) + assert list(queued((), 5)) == [] assert list(queued((1, 2, 3, 4, 5), 5)) == [1] assert list(queued((1, 2, 3, 4, 5, 6), 5)) == [1, 2] +def test_smoothed(): + with pytest.raises(ValueError): + list(smoothed((), 0)) + assert list(smoothed((), 5)) == [] + assert list(smoothed((1, 2, 3, 4, 5), 5)) == [3.0] + assert list(smoothed((1, 2, 3, 4, 5, 6), 5)) == [3.0, 4.0] + assert list(smoothed((1, 2, 3, 4, 5, 6), 5, average=mean)) == [3.0, 4.0] + assert list(smoothed((1, 1, 1, 4, 5, 5), 5, average=mean)) == [2.4, 3.2] + assert list(smoothed((1, 1, 1, 4, 5, 5), 5, average=median)) == [1, 4] + def test_pre_delayed(): + with pytest.raises(ValueError): + list(pre_delayed((), -1)) + assert list(pre_delayed((), 0.01)) == [] + count = 0 start = time() for v in pre_delayed((0, 0, 0), 0.01): + count += 1 assert v == 0 assert time() - start >= 0.01 start = time() + assert count == 3 def test_post_delayed(): + with pytest.raises(ValueError): + list(post_delayed((), -1)) + assert list(post_delayed((), 0.01)) == [] + count = 0 start = time() for v in post_delayed((1, 2, 2), 0.01): + count += 1 if v == 1: assert time() - start < 0.01 else: + assert v == 2 assert time() - start >= 0.01 start = time() assert time() - start >= 0.01 + assert count == 3 + +def test_pre_periodic_filtered(): + with pytest.raises(ValueError): + list(pre_periodic_filtered((), 2, -1)) + with pytest.raises(ValueError): + list(pre_periodic_filtered((), 0, 0)) + assert list(pre_periodic_filtered((), 2, 0)) == [] + assert list(pre_periodic_filtered((1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 2, 0)) == [3, 4, 5, 6, 7, 8, 9, 10] + assert list(pre_periodic_filtered((1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 1, 1)) == [2, 4, 6, 8, 10] + assert list(pre_periodic_filtered((1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 1, 2)) == [2, 3, 5, 6, 8, 9] + assert list(pre_periodic_filtered((1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 2, 1)) == [3, 6, 9] + +def test_post_periodic_filtered(): + with pytest.raises(ValueError): + list(post_periodic_filtered((), 1, 0)) + with pytest.raises(ValueError): + list(post_periodic_filtered((), 0, 1)) + assert list(pre_periodic_filtered((), 1, 1)) == [] + assert list(post_periodic_filtered((1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 1, 1)) == [1, 3, 5, 7, 9] + assert list(post_periodic_filtered((1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 1, 2)) == [1, 4, 7, 10] + assert list(post_periodic_filtered((1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 2, 1)) == [1, 2, 4, 5, 7, 8, 10] def test_random_values(): - for i, v in zip(range(1000), random_values()): + for _, v in zip(range(1000), random_values()): assert 0 <= v <= 1 def test_sin_values(): + firstval = None for i, v in zip(range(1000), sin_values()): + assert -1 <= v <= 1 assert isclose(v, sin(radians(i)), abs_tol=1e-9) + if i == 0: + firstval = v + else: + if i % 360 == 0: + assert v == firstval + for period in (360, 100): + firstval = None + for i, v in zip(range(1000), sin_values(period)): + assert -1 <= v <= 1 + if i == 0: + firstval = v + else: + if i % period == 0: + assert v == firstval def test_cos_values(): + firstval = None for i, v in zip(range(1000), cos_values()): + assert -1 <= v <= 1 assert isclose(v, cos(radians(i)), abs_tol=1e-9) - + if i == 0: + firstval = v + else: + if i % 360 == 0: + assert v == firstval + for period in (360, 100): + firstval = None + for i, v in zip(range(1000), cos_values(period)): + assert -1 <= v <= 1 + if i == 0: + firstval = v + else: + if i % period == 0: + assert v == firstval