2928 lines
105 KiB
Python
2928 lines
105 KiB
Python
# vim: set fileencoding=utf-8:
|
||
#
|
||
# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
|
||
#
|
||
# Copyright (c) 2015-2021 Dave Jones <dave@waveform.org.uk>
|
||
# Copyright (c) 2015-2021 Ben Nuttall <ben@bennuttall.com>
|
||
# Copyright (c) 2020 Ryan Walmsley <ryanteck@gmail.com>
|
||
# Copyright (c) 2020 Jack Wearden <jack@jackwearden.co.uk>
|
||
# Copyright (c) 2019 tuftii <3215045+tuftii@users.noreply.github.com>
|
||
# Copyright (c) 2019 ForToffee <ForToffee@users.noreply.github.com>
|
||
# Copyright (c) 2016-2019 Andrew Scheller <github@loowis.durge.org>
|
||
# Copyright (c) 2018 SteveAmor <steveamor@users.noreply.github.com>
|
||
# Copyright (c) 2018 Rick Ansell <rick@nbinvincible.org.uk>
|
||
# Copyright (c) 2018 Claire Pollard <claire.r.pollard@gmail.com>
|
||
# Copyright (c) 2016 Ian Harcombe <ian.harcombe@gmail.com>
|
||
# Copyright (c) 2016 Andrew Scheller <lurch@durge.org>
|
||
#
|
||
# SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
from __future__ import (
|
||
unicode_literals,
|
||
print_function,
|
||
absolute_import,
|
||
division,
|
||
)
|
||
try:
|
||
from itertools import izip as zip
|
||
except ImportError:
|
||
pass
|
||
|
||
from time import sleep
|
||
from itertools import repeat, cycle, chain, tee
|
||
from threading import Lock
|
||
from collections import OrderedDict, Counter, namedtuple
|
||
try:
|
||
from collections.abc import MutableMapping
|
||
except ImportError:
|
||
from collections import MutableMapping
|
||
|
||
from .exc import (
|
||
DeviceClosed,
|
||
PinInvalidPin,
|
||
GPIOPinMissing,
|
||
EnergenieSocketMissing,
|
||
EnergenieBadSocket,
|
||
OutputDeviceBadValue,
|
||
CompositeDeviceBadDevice,
|
||
BadWaitTime,
|
||
)
|
||
from .input_devices import Button
|
||
from .output_devices import (
|
||
OutputDevice,
|
||
LED,
|
||
PWMLED,
|
||
RGBLED,
|
||
Buzzer,
|
||
Motor,
|
||
PhaseEnableMotor,
|
||
TonalBuzzer,
|
||
)
|
||
from .threads import GPIOThread
|
||
from .devices import Device, CompositeDevice
|
||
from .mixins import SharedMixin, SourceMixin, HoldMixin
|
||
from .fonts import load_font_7seg, load_font_14seg
|
||
|
||
|
||
def pairwise(it):
|
||
a, b = tee(it)
|
||
next(b, None)
|
||
return zip(a, b)
|
||
|
||
|
||
class CompositeOutputDevice(SourceMixin, CompositeDevice):
|
||
"""
|
||
Extends :class:`CompositeDevice` with :meth:`on`, :meth:`off`, and
|
||
:meth:`toggle` methods for controlling subordinate output devices. Also
|
||
extends :attr:`value` to be writeable.
|
||
|
||
:param Device \\*args:
|
||
The un-named devices that belong to the composite device. The
|
||
:attr:`~Device.value` attributes of these devices will be represented
|
||
within the composite device's tuple :attr:`value` in the order
|
||
specified here.
|
||
|
||
:type _order: list or None
|
||
:param _order:
|
||
If specified, this is the order of named items specified by keyword
|
||
arguments (to ensure that the :attr:`value` tuple is constructed with a
|
||
specific order). All keyword arguments *must* be included in the
|
||
collection. If omitted, an alphabetically sorted order will be selected
|
||
for keyword arguments.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
:param Device \\*\\*kwargs:
|
||
The named devices that belong to the composite device. These devices
|
||
will be accessible as named attributes on the resulting device, and
|
||
their :attr:`value` attributes will be accessible as named elements of
|
||
the composite device's tuple :attr:`value`.
|
||
"""
|
||
|
||
def on(self):
|
||
"""
|
||
Turn all the output devices on.
|
||
"""
|
||
for device in self:
|
||
if isinstance(device, (OutputDevice, CompositeOutputDevice)):
|
||
device.on()
|
||
|
||
def off(self):
|
||
"""
|
||
Turn all the output devices off.
|
||
"""
|
||
for device in self:
|
||
if isinstance(device, (OutputDevice, CompositeOutputDevice)):
|
||
device.off()
|
||
|
||
def toggle(self):
|
||
"""
|
||
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:
|
||
if isinstance(device, (OutputDevice, CompositeOutputDevice)):
|
||
device.toggle()
|
||
|
||
@property
|
||
def value(self):
|
||
"""
|
||
A tuple containing a value for each subordinate device. This property
|
||
can also be set to update the state of all subordinate output devices.
|
||
"""
|
||
return super(CompositeOutputDevice, self).value
|
||
|
||
@value.setter
|
||
def value(self, 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. The :attr:`value` of the button board is a tuple
|
||
of all the buttons states. This can be used to control all the LEDs in a
|
||
:class:`LEDBoard` with a :class:`ButtonBoard`::
|
||
|
||
from gpiozero import LEDBoard, ButtonBoard
|
||
from signal import pause
|
||
|
||
leds = LEDBoard(2, 3, 4, 5)
|
||
btns = ButtonBoard(6, 7, 8, 9)
|
||
leds.source = btns
|
||
|
||
pause()
|
||
|
||
Alternatively you could represent the number of pressed buttons with an
|
||
:class:`LEDBarGraph`::
|
||
|
||
from gpiozero import LEDBarGraph, ButtonBoard
|
||
from statistics import mean
|
||
from signal import pause
|
||
|
||
graph = LEDBarGraph(2, 3, 4, 5)
|
||
bb = ButtonBoard(6, 7, 8, 9)
|
||
graph.source = (mean(values) for values in bb.values)
|
||
|
||
pause()
|
||
|
||
:type pins: int or str
|
||
:param \\*pins:
|
||
Specify the GPIO pins that the buttons of the board are attached to.
|
||
See :ref:`pin-numbering` for valid pin numbers. You can designate as
|
||
many pins as necessary.
|
||
|
||
:type pull_up: bool or None
|
||
:param pull_up:
|
||
If :data:`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 :data:`False`, the GPIO pins will be pulled low by default.
|
||
In this case, connect the other side of the buttons to 3V3. If
|
||
:data:`None`, the pin will be floating, so it must be externally pulled
|
||
up or down and the ``active_state`` parameter must be set accordingly.
|
||
|
||
:type active_state: bool or None
|
||
:param active_state:
|
||
See description under :class:`InputDevice` for more information.
|
||
|
||
:param float bounce_time:
|
||
If :data:`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.
|
||
|
||
: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``.
|
||
|
||
:param bool hold_repeat:
|
||
If :data:`True`, the :attr:`when_held` handler will be repeatedly
|
||
executed as long as any buttons remain held, every *hold_time* seconds.
|
||
If :data:`False` (the default) the :attr:`when_held` handler will be
|
||
only be executed once per hold.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
:type named_pins: int or str
|
||
: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)
|
||
active_state = kwargs.pop('active_state', None)
|
||
bounce_time = kwargs.pop('bounce_time', None)
|
||
hold_time = kwargs.pop('hold_time', 1)
|
||
hold_repeat = kwargs.pop('hold_repeat', False)
|
||
pin_factory = kwargs.pop('pin_factory', None)
|
||
order = kwargs.pop('_order', None)
|
||
super(ButtonBoard, self).__init__(
|
||
*(
|
||
Button(pin, pull_up=pull_up, active_state=active_state,
|
||
bounce_time=bounce_time, hold_time=hold_time,
|
||
hold_repeat=hold_repeat)
|
||
for pin in args
|
||
),
|
||
_order=order,
|
||
pin_factory=pin_factory,
|
||
**{
|
||
name: Button(pin, pull_up=pull_up, active_state=active_state,
|
||
bounce_time=bounce_time, hold_time=hold_time,
|
||
hold_repeat=hold_repeat)
|
||
for name, pin in kwargs.items()
|
||
}
|
||
)
|
||
if len(self) == 0:
|
||
raise GPIOPinMissing('No pins given')
|
||
def get_new_handler(device):
|
||
def fire_both_events(ticks, state):
|
||
device._fire_events(ticks, device._state_to_value(state))
|
||
self._fire_events(ticks, self.is_active)
|
||
return fire_both_events
|
||
# _handlers only exists to ensure that we keep a reference to the
|
||
# generated fire_both_events handler for each Button (remember that
|
||
# pin.when_changed only keeps a weak reference to handlers)
|
||
self._handlers = tuple(get_new_handler(device) for device in self)
|
||
for button, handler in zip(self, self._handlers):
|
||
button.pin.when_changed = handler
|
||
self._when_changed = None
|
||
self._last_value = None
|
||
# Call _fire_events once to set initial state of events
|
||
self._fire_events(self.pin_factory.ticks(), self.is_active)
|
||
self.hold_time = hold_time
|
||
self.hold_repeat = hold_repeat
|
||
|
||
@property
|
||
def pull_up(self):
|
||
"""
|
||
If :data:`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, ticks, new_value):
|
||
super(ButtonBoard, self)._fire_events(ticks, new_value)
|
||
old_value, self._last_value = self._last_value, new_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):
|
||
pwm = kwargs.pop('pwm', False)
|
||
active_high = kwargs.pop('active_high', True)
|
||
initial_value = kwargs.pop('initial_value', False)
|
||
pin_factory = kwargs.pop('pin_factory', None)
|
||
order = kwargs.pop('_order', None)
|
||
LEDClass = PWMLED if pwm else LED
|
||
super(LEDCollection, self).__init__(
|
||
*(
|
||
pin_or_collection
|
||
if isinstance(pin_or_collection, LEDCollection) else
|
||
LEDClass(
|
||
pin_or_collection, active_high, initial_value,
|
||
pin_factory=pin_factory
|
||
)
|
||
for pin_or_collection in args
|
||
),
|
||
_order=order,
|
||
pin_factory=pin_factory,
|
||
**{
|
||
name: pin_or_collection
|
||
if isinstance(pin_or_collection, LEDCollection) else
|
||
LEDClass(
|
||
pin_or_collection, active_high, initial_value,
|
||
pin_factory=pin_factory
|
||
)
|
||
for name, pin_or_collection in kwargs.items()
|
||
}
|
||
)
|
||
if len(self) == 0:
|
||
raise GPIOPinMissing('No pins given')
|
||
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 tuple of all LEDs contained in this collection (and all
|
||
sub-collections).
|
||
"""
|
||
return self._leds
|
||
|
||
@property
|
||
def active_high(self):
|
||
return self[0].active_high
|
||
|
||
|
||
LEDCollection.is_lit = LEDCollection.is_active
|
||
|
||
|
||
class LEDBoard(LEDCollection):
|
||
"""
|
||
Extends :class:`LEDCollection` and represents a generic LED board or
|
||
collection of LEDs.
|
||
|
||
The following example turns on all the LEDs on a board containing 5 LEDs
|
||
attached to GPIO pins 2 through 6::
|
||
|
||
from gpiozero import LEDBoard
|
||
|
||
leds = LEDBoard(2, 3, 4, 5, 6)
|
||
leds.on()
|
||
|
||
:type pins: int or str or LEDCollection
|
||
:param \\*pins:
|
||
Specify the GPIO pins that the LEDs of the board are attached to. See
|
||
:ref:`pin-numbering` for valid pin numbers. You can designate as many
|
||
pins as necessary. You can also specify :class:`LEDBoard` instances to
|
||
create trees of LEDs.
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances for each pin. If
|
||
:data:`False` (the default), construct regular :class:`LED` instances.
|
||
|
||
:param bool active_high:
|
||
If :data:`True` (the default), the :meth:`on` method will set all the
|
||
associated pins to HIGH. If :data:`False`, the :meth:`on` method will
|
||
set all pins to LOW (the :meth:`off` method always does the opposite).
|
||
|
||
:type initial_value: bool or None
|
||
:param initial_value:
|
||
If :data:`False` (the default), all LEDs will be off initially. If
|
||
:data:`None`, each device will be left in whatever state the pin is
|
||
found in when configured for output (warning: this can be on). If
|
||
:data:`True`, the device will be switched on initially.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
:type named_pins: int or str
|
||
:param \\*\\*named_pins:
|
||
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 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_thread = None
|
||
self._blink_leds = []
|
||
self._blink_lock = Lock()
|
||
super(LEDBoard, self).__init__(*args, **kwargs)
|
||
|
||
def close(self):
|
||
try:
|
||
self._stop_blink()
|
||
except AttributeError:
|
||
pass
|
||
super(LEDBoard, self).close()
|
||
|
||
def on(self, *args):
|
||
"""
|
||
If no arguments are specified, turn all the LEDs on. If arguments are
|
||
specified, they must be the indexes of the LEDs you wish to turn on.
|
||
For example::
|
||
|
||
from gpiozero import LEDBoard
|
||
|
||
leds = LEDBoard(2, 3, 4, 5)
|
||
leds.on(0) # turn on the first LED (pin 2)
|
||
leds.on(-1) # turn on the last LED (pin 5)
|
||
leds.on(1, 2) # turn on the middle LEDs (pins 3 and 4)
|
||
leds.off() # turn off all LEDs
|
||
leds.on() # turn on all LEDs
|
||
|
||
If :meth:`blink` is currently active, it will be stopped first.
|
||
|
||
:param int args:
|
||
The index(es) of the LED(s) to turn on. If no indexes are specified
|
||
turn on all LEDs.
|
||
"""
|
||
self._stop_blink()
|
||
if args:
|
||
for index in args:
|
||
self[index].on()
|
||
else:
|
||
super(LEDBoard, self).on()
|
||
|
||
def off(self, *args):
|
||
"""
|
||
If no arguments are specified, turn all the LEDs off. If arguments are
|
||
specified, they must be the indexes of the LEDs you wish to turn off.
|
||
For example::
|
||
|
||
from gpiozero import LEDBoard
|
||
|
||
leds = LEDBoard(2, 3, 4, 5)
|
||
leds.on() # turn on all LEDs
|
||
leds.off(0) # turn off the first LED (pin 2)
|
||
leds.off(-1) # turn off the last LED (pin 5)
|
||
leds.off(1, 2) # turn off the middle LEDs (pins 3 and 4)
|
||
leds.on() # turn on all LEDs
|
||
|
||
If :meth:`blink` is currently active, it will be stopped first.
|
||
|
||
:param int args:
|
||
The index(es) of the LED(s) to turn off. If no indexes are
|
||
specified turn off all LEDs.
|
||
"""
|
||
self._stop_blink()
|
||
if args:
|
||
for index in args:
|
||
self[index].off()
|
||
else:
|
||
super(LEDBoard, self).off()
|
||
|
||
def toggle(self, *args):
|
||
"""
|
||
If no arguments are specified, toggle the state of all LEDs. If
|
||
arguments are specified, they must be the indexes of the LEDs you wish
|
||
to toggle. For example::
|
||
|
||
from gpiozero import LEDBoard
|
||
|
||
leds = LEDBoard(2, 3, 4, 5)
|
||
leds.toggle(0) # turn on the first LED (pin 2)
|
||
leds.toggle(-1) # turn on the last LED (pin 5)
|
||
leds.toggle() # turn the first and last LED off, and the
|
||
# middle pair on
|
||
|
||
If :meth:`blink` is currently active, it will be stopped first.
|
||
|
||
:param int args:
|
||
The index(es) of the LED(s) to toggle. If no indexes are specified
|
||
toggle the state of all LEDs.
|
||
"""
|
||
self._stop_blink()
|
||
if args:
|
||
for index in args:
|
||
self[index].toggle()
|
||
else:
|
||
super(LEDBoard, self).toggle()
|
||
|
||
def blink(
|
||
self, on_time=1, off_time=1, fade_in_time=0, fade_out_time=0,
|
||
n=None, background=True):
|
||
"""
|
||
Make all the LEDs turn on and off repeatedly.
|
||
|
||
:param float on_time:
|
||
Number of seconds on. Defaults to 1 second.
|
||
|
||
:param float off_time:
|
||
Number of seconds off. Defaults to 1 second.
|
||
|
||
:param float fade_in_time:
|
||
Number of seconds to spend fading in. Defaults to 0. Must be 0 if
|
||
``pwm`` was :data:`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. Must be 0 if
|
||
``pwm`` was :data:`False` when the class was constructed
|
||
(:exc:`ValueError` will be raised if not).
|
||
|
||
:type n: int or None
|
||
:param n:
|
||
Number of times to blink; :data:`None` (the default) means forever.
|
||
|
||
:param bool background:
|
||
If :data:`True`, start a background thread to continue blinking and
|
||
return immediately. If :data:`False`, only return when the blink is
|
||
finished (warning: the default value of *n* will result in this
|
||
method never returning).
|
||
"""
|
||
for led in self.leds:
|
||
if isinstance(led, LED):
|
||
if fade_in_time:
|
||
raise ValueError('fade_in_time must be 0 with non-PWM LEDs')
|
||
if fade_out_time:
|
||
raise ValueError('fade_out_time must be 0 with non-PWM LEDs')
|
||
self._stop_blink()
|
||
self._blink_thread = GPIOThread(
|
||
self._blink_device,
|
||
(on_time, off_time, fade_in_time, fade_out_time, n))
|
||
self._blink_thread.start()
|
||
if not background:
|
||
self._blink_thread.join()
|
||
self._blink_thread = None
|
||
|
||
def _stop_blink(self, led=None):
|
||
if led is None:
|
||
if self._blink_thread:
|
||
self._blink_thread.stop()
|
||
self._blink_thread = None
|
||
else:
|
||
with self._blink_lock:
|
||
self._blink_leds.remove(led)
|
||
|
||
def pulse(self, fade_in_time=1, fade_out_time=1, n=None, background=True):
|
||
"""
|
||
Make all LEDs fade in and out repeatedly. Note that this method will
|
||
only work if the *pwm* parameter was :data:`True` at construction time.
|
||
|
||
: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.
|
||
|
||
:type n: int or None
|
||
:param n:
|
||
Number of times to blink; :data:`None` (the default) means forever.
|
||
|
||
:param bool background:
|
||
If :data:`True` (the default), start a background thread to
|
||
continue blinking and return immediately. If :data:`False`, only
|
||
return when the blink 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, n, background)
|
||
|
||
def _blink_device(
|
||
self, on_time, off_time, fade_in_time, fade_out_time, n, fps=25):
|
||
sequence = []
|
||
if fade_in_time > 0:
|
||
sequence += [
|
||
(i * (1 / fps) / fade_in_time, 1 / fps)
|
||
for i in range(int(fps * fade_in_time))
|
||
]
|
||
sequence.append((1, on_time))
|
||
if fade_out_time > 0:
|
||
sequence += [
|
||
(1 - (i * (1 / fps) / fade_out_time), 1 / fps)
|
||
for i in range(int(fps * fade_out_time))
|
||
]
|
||
sequence.append((0, off_time))
|
||
if n is None:
|
||
sequence = cycle(sequence)
|
||
else:
|
||
sequence = chain.from_iterable(repeat(sequence, n))
|
||
with self._blink_lock:
|
||
self._blink_leds = list(self.leds)
|
||
for led in self._blink_leds:
|
||
if led._controller not in (None, self):
|
||
led._controller._stop_blink(led)
|
||
led._controller = self
|
||
for value, delay in sequence:
|
||
with self._blink_lock:
|
||
if not self._blink_leds:
|
||
break
|
||
for led in self._blink_leds:
|
||
led._write(value)
|
||
if self._blink_thread.stopping.wait(delay):
|
||
break
|
||
|
||
|
||
class LEDBarGraph(LEDCollection):
|
||
"""
|
||
Extends :class:`LEDCollection` to control a line of LEDs representing a
|
||
bar graph. Positive values (0 to 1) light the LEDs from first to last.
|
||
Negative values (-1 to 0) light the LEDs from last to first.
|
||
|
||
The following example demonstrates turning on the first two and last two
|
||
LEDs in a board containing five LEDs attached to GPIOs 2 through 6::
|
||
|
||
from gpiozero import LEDBarGraph
|
||
from time import sleep
|
||
|
||
graph = LEDBarGraph(2, 3, 4, 5, 6)
|
||
graph.value = 2/5 # Light the first two LEDs only
|
||
sleep(1)
|
||
graph.value = -2/5 # Light the last two LEDs only
|
||
sleep(1)
|
||
graph.off()
|
||
|
||
As with all other output devices, :attr:`source` and :attr:`values` are
|
||
supported::
|
||
|
||
from gpiozero import LEDBarGraph, MCP3008
|
||
from signal import pause
|
||
|
||
graph = LEDBarGraph(2, 3, 4, 5, 6, pwm=True)
|
||
pot = MCP3008(channel=0)
|
||
|
||
graph.source = pot
|
||
|
||
pause()
|
||
|
||
:type pins: int or str
|
||
:param \\*pins:
|
||
Specify the GPIO pins that the LEDs of the bar graph are attached to.
|
||
See :ref:`pin-numbering` for valid pin numbers. You can designate as
|
||
many pins as necessary.
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances for each pin. If
|
||
:data:`False` (the default), construct regular :class:`LED` instances.
|
||
This parameter can only be specified as a keyword parameter.
|
||
|
||
:param bool active_high:
|
||
If :data:`True` (the default), the :meth:`on` method will set all the
|
||
associated pins to HIGH. If :data:`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.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
"""
|
||
def __init__(self, *pins, **kwargs):
|
||
# Don't allow graphs to contain collections
|
||
for pin in pins:
|
||
if isinstance(pin, Device):
|
||
raise CompositeDeviceBadDevice(
|
||
'Only pins may be specified for LEDBarGraph')
|
||
pwm = kwargs.pop('pwm', False)
|
||
active_high = kwargs.pop('active_high', True)
|
||
initial_value = kwargs.pop('initial_value', 0.0)
|
||
pin_factory = kwargs.pop('pin_factory', None)
|
||
if kwargs:
|
||
raise TypeError(
|
||
'unexpected keyword argument: %s' % kwargs.popitem()[0])
|
||
super(LEDBarGraph, self).__init__(
|
||
*pins, pwm=pwm, active_high=active_high, pin_factory=pin_factory)
|
||
try:
|
||
self.value = initial_value
|
||
except:
|
||
self.close()
|
||
raise
|
||
|
||
@property
|
||
def value(self):
|
||
"""
|
||
The value of the LED bar graph. When no LEDs are lit, the value is 0.
|
||
When all LEDs are lit, the value is 1. Values between 0 and 1
|
||
light LEDs linearly from first to last. Values between 0 and -1
|
||
light LEDs linearly from last to first.
|
||
|
||
To light a particular number of LEDs, simply divide that number by
|
||
the number of LEDs. For example, if your graph contains 3 LEDs, the
|
||
following will light the first::
|
||
|
||
from gpiozero import LEDBarGraph
|
||
|
||
graph = LEDBarGraph(12, 16, 19)
|
||
graph.value = 1/3
|
||
|
||
.. note::
|
||
|
||
Setting value to -1 will light all LEDs. However, querying it
|
||
subsequently will return 1 as both representations are the same in
|
||
hardware. The readable range of :attr:`value` is effectively
|
||
-1 < value <= 1.
|
||
"""
|
||
result = sum(led.value for led in self)
|
||
if self[0].value < self[-1].value:
|
||
result = -result
|
||
return result / len(self)
|
||
|
||
@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:
|
||
leds = reversed(leds)
|
||
value = -value
|
||
if isinstance(self[0], PWMLED):
|
||
calc_value = lambda index: min(1, max(0, count * value - index))
|
||
else:
|
||
calc_value = lambda index: value >= ((index + 1) / count)
|
||
for index, led in enumerate(leds):
|
||
led.value = calc_value(index)
|
||
|
||
@property
|
||
def lit_count(self):
|
||
"""
|
||
The number of LEDs on the bar graph actually lit up. Note that just
|
||
like :attr:`value`, this can be negative if the LEDs are lit from last
|
||
to first.
|
||
"""
|
||
lit_value = self.value * len(self)
|
||
if not isinstance(self[0], PWMLED):
|
||
lit_value = int(lit_value)
|
||
return lit_value
|
||
|
||
@lit_count.setter
|
||
def lit_count(self, value):
|
||
self.value = value / len(self)
|
||
|
||
|
||
class LEDCharFont(MutableMapping):
|
||
"""
|
||
Contains a mapping of values to tuples of LED states.
|
||
|
||
This effectively acts as a "font" for :class:`LEDCharDisplay`, and two
|
||
default fonts (for 7-segment and 14-segment displays) are shipped with GPIO
|
||
Zero by default. You can construct your own font instance from a
|
||
:class:`dict` which maps values (usually single-character strings) to
|
||
a tuple of LED states::
|
||
|
||
from gpiozero import LEDCharDisplay, LEDCharFont
|
||
|
||
my_font = LEDCharFont({
|
||
' ': (0, 0, 0, 0, 0, 0, 0),
|
||
'D': (1, 1, 1, 1, 1, 1, 0),
|
||
'A': (1, 1, 1, 0, 1, 1, 1),
|
||
'd': (0, 1, 1, 1, 1, 0, 1),
|
||
'a': (1, 1, 1, 1, 1, 0, 1),
|
||
})
|
||
display = LEDCharDisplay(26, 13, 12, 22, 17, 19, 6, dp=5, font=my_font)
|
||
display.value = 'D'
|
||
|
||
Font instances are mutable and can be changed while actively in use by
|
||
an instance of :class:`LEDCharDisplay`. However, changing the font will
|
||
*not* change the state of the LEDs in the display (though it may change
|
||
the :attr:`~LEDCharDisplay.value` of the display when next queried).
|
||
|
||
.. note::
|
||
|
||
Your custom mapping should always include a value (typically space)
|
||
which represents all the LEDs off. This will usually be the default
|
||
value for an instance of :class:`LEDCharDisplay`.
|
||
|
||
You may also wish to load fonts from a friendly text-based format. A simple
|
||
parser for such formats (supporting an arbitrary number of segments) is
|
||
provided by :func:`gpiozero.fonts.load_segment_font`.
|
||
"""
|
||
def __init__(self, font):
|
||
super(LEDCharFont, self).__init__()
|
||
self._map = OrderedDict([
|
||
(char, tuple(int(bool(pin)) for pin in pins))
|
||
for char, pins in font.items()
|
||
])
|
||
self._refresh_rmap()
|
||
|
||
def __repr__(self):
|
||
return '{self.__class__.__name__}({{\n{content}\n}})'.format(
|
||
self=self, content='\n'.join(
|
||
' {key!r}: {value!r},'.format(key=key, value=value)
|
||
for key, value in sorted(self.items())
|
||
))
|
||
|
||
def _refresh_rmap(self):
|
||
# The reverse mapping is pre-calculated for speed of lookup. Given that
|
||
# the font mapping can be 1:n, we cannot guarantee the reverse is
|
||
# unique. In case the provided font is an ordered dictionary, we
|
||
# explicitly take only the first definition of each non-unique pin
|
||
# definition so that value lookups are predictable
|
||
rmap = {}
|
||
for char, pins in self._map.items():
|
||
rmap.setdefault(pins, char)
|
||
self._rmap = rmap
|
||
|
||
def __len__(self):
|
||
return len(self._map)
|
||
|
||
def __iter__(self):
|
||
return iter(self._map)
|
||
|
||
def __getitem__(self, char):
|
||
return self._map[char]
|
||
|
||
def __setitem__(self, char, pins):
|
||
try:
|
||
# This is necessary to ensure that _rmap is correct in the case
|
||
# that we're overwriting an existing char->pins mapping
|
||
del self[char]
|
||
except KeyError:
|
||
pass
|
||
pins = tuple(int(bool(pin)) for pin in pins)
|
||
self._map[char] = pins
|
||
self._rmap.setdefault(pins, char)
|
||
|
||
def __delitem__(self, char):
|
||
pins = self._map[char]
|
||
del self._map[char]
|
||
# If the reverse mapping of the char's pins maps to the char we need
|
||
# to find if it now maps to another char (given the n:1 mapping)
|
||
if self._rmap[pins] == char:
|
||
del self._rmap[pins]
|
||
for char, char_pins in self._map.items():
|
||
if pins == char_pins:
|
||
self._rmap[pins] = char
|
||
break
|
||
|
||
|
||
class LEDCharDisplay(LEDCollection):
|
||
"""
|
||
Extends :class:`LEDCollection` for a multi-segment LED display.
|
||
|
||
`Multi-segment LED displays`_ typically have 7 pins (labelled "a" through
|
||
"g") representing 7 LEDs layed out in a figure-of-8 fashion. Frequently, an
|
||
eigth pin labelled "dp" is included for a trailing decimal-point:
|
||
|
||
.. code-block:: text
|
||
|
||
a
|
||
━━━━━
|
||
f ┃ ┃ b
|
||
┃ g ┃
|
||
━━━━━
|
||
e ┃ ┃ c
|
||
┃ ┃
|
||
━━━━━ • dp
|
||
d
|
||
|
||
Other common layouts are 9, 14, and 16 segment displays which include
|
||
additional segments permitting more accurate renditions of alphanumerics.
|
||
For example:
|
||
|
||
.. code-block:: text
|
||
|
||
a
|
||
━━━━━
|
||
f ┃╲i┃j╱┃ b
|
||
┃ ╲┃╱k┃
|
||
g━━ ━━h
|
||
e ┃ ╱┃╲n┃ c
|
||
┃╱l┃m╲┃
|
||
━━━━━ • dp
|
||
d
|
||
|
||
Such displays have either a common anode, or common cathode pin. This class
|
||
defaults to the latter; when using a common anode display *active_high*
|
||
should be set to :data:`False`.
|
||
|
||
Instances of this class can be used to display characters or control
|
||
individual LEDs on the display. For example::
|
||
|
||
from gpiozero import LEDCharDisplay
|
||
|
||
char = LEDCharDisplay(4, 5, 6, 7, 8, 9, 10, active_high=False)
|
||
char.value = 'C'
|
||
|
||
If the class is constructed with 7 or 14 segments, a default :attr:`font`
|
||
will be loaded, mapping some ASCII characters to typical layouts. In other
|
||
cases, the default mapping will simply assign " " (space) to all LEDs off.
|
||
You can assign your own mapping at construction time or after
|
||
instantiation.
|
||
|
||
While the example above shows the display with a :class:`str` value,
|
||
theoretically the *font* can map any value that can be the key in a
|
||
:class:`dict`, so the value of the display can be likewise be any valid
|
||
key value (e.g. you could map integer digits to LED patterns). That said,
|
||
there is one exception to this: when *dp* is specified to enable the
|
||
decimal-point, the :attr:`value` must be a :class:`str` as the presence
|
||
or absence of a "." suffix indicates whether the *dp* LED is lit.
|
||
|
||
:type pins: int or str
|
||
:param \\*pins:
|
||
Specify the GPIO pins that the multi-segment display is attached to.
|
||
Pins should be in the LED segment order A, B, C, D, E, F, G, and will
|
||
be named automatically by the class. If a decimal-point pin is
|
||
present, specify it separately as the *dp* parameter.
|
||
|
||
:type dp: int or str
|
||
:param dp:
|
||
If a decimal-point segment is present, specify it as this named
|
||
parameter.
|
||
|
||
:type font: dict or None
|
||
:param font:
|
||
A mapping of values (typically characters, but may also be numbers) to
|
||
tuples of LED states. A default mapping for ASCII characters is
|
||
provided for 7 and 14 segment displays.
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances for each pin. If
|
||
:data:`False` (the default), construct regular :class:`LED` instances.
|
||
|
||
:param bool active_high:
|
||
If :data:`True` (the default), the :meth:`on` method will set all the
|
||
associated pins to HIGH. If :data:`False`, the :meth:`on` method will
|
||
set all pins to LOW (the :meth:`off` method always does the opposite).
|
||
|
||
:param initial_value:
|
||
The initial value to display. Defaults to space (" ") which typically
|
||
maps to all LEDs being inactive. If :data:`None`, each device will be
|
||
left in whatever state the pin is found in when configured for output
|
||
(warning: this can be on).
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Multi-segment LED displays: https://en.wikipedia.org/wiki/Seven-segment_display
|
||
"""
|
||
def __init__(self, *pins, **kwargs):
|
||
dp = kwargs.pop('dp', None)
|
||
font = kwargs.pop('font', None)
|
||
pwm = kwargs.pop('pwm', False)
|
||
active_high = kwargs.pop('active_high', True)
|
||
initial_value = kwargs.pop('initial_value', " ")
|
||
pin_factory = kwargs.pop('pin_factory', None)
|
||
if kwargs:
|
||
raise TypeError(
|
||
'unexpected keyword argument: %s' % kwargs.popiem()[0])
|
||
if not 1 < len(pins) <= 26:
|
||
raise PinInvalidPin(
|
||
'Must have between 2 and 26 LEDs in LEDCharDisplay')
|
||
for pin in pins:
|
||
if isinstance(pin, LEDCollection):
|
||
raise PinInvalidPin(
|
||
'Cannot use LEDCollection in LEDCharDisplay')
|
||
|
||
if font is None:
|
||
if len(pins) in (7, 14):
|
||
# Only import pkg_resources here as merely importing it is
|
||
# slooooow!
|
||
from pkg_resources import resource_stream
|
||
font = {
|
||
7: lambda: load_font_7seg(
|
||
resource_stream(__name__, 'fonts/7seg.txt')),
|
||
14: lambda: load_font_14seg(
|
||
resource_stream(__name__, 'fonts/14seg.txt')),
|
||
}[len(pins)]()
|
||
else:
|
||
# Construct a default dict containing a definition for " "
|
||
font = {" ": (0,) * len(pins)}
|
||
self._font = LEDCharFont(font)
|
||
|
||
pins = {chr(ord('a') + i): pin for i, pin in enumerate(pins)}
|
||
order = sorted(pins.keys())
|
||
if dp is not None:
|
||
pins['dp'] = dp
|
||
order.append('dp')
|
||
super(LEDCharDisplay, self).__init__(
|
||
pwm=pwm, active_high=active_high, initial_value=None,
|
||
_order=order, pin_factory=pin_factory, **pins)
|
||
if initial_value is not None:
|
||
self.value = initial_value
|
||
|
||
@property
|
||
def font(self):
|
||
"""
|
||
An :class:`LEDCharFont` mapping characters to tuples of LED states.
|
||
The font is mutable after construction. You can assign a tuple of LED
|
||
states to a character to modify the font, delete an existing character
|
||
in the font, or assign a mapping of characters to tuples to replace the
|
||
entire font.
|
||
|
||
Note that modifying the :attr:`font` never alters the underlying LED
|
||
states. Only assignment to :attr:`value`, or calling the inherited
|
||
:class:`LEDCollection` methods (:meth:`on`, :meth:`off`, etc.) modifies
|
||
LED states. However, modifying the font may alter the character
|
||
returned by querying :attr:`value`.
|
||
"""
|
||
return self._font
|
||
|
||
@font.setter
|
||
def font(self, value):
|
||
self._font = LEDCharFont(value)
|
||
|
||
@property
|
||
def value(self):
|
||
"""
|
||
The character the display should show. This is mapped by the current
|
||
:attr:`font` to a tuple of LED states which is applied to the
|
||
underlying LED objects when this attribute is set.
|
||
|
||
When queried, the current LED states are looked up in the font to
|
||
determine the character shown. If the current LED states do not
|
||
correspond to any character in the :attr:`font`, the value is
|
||
:data:`None`.
|
||
|
||
It is possible for multiple characters in the font to map to the same
|
||
LED states (e.g. S and 5). In this case, if the font was constructed
|
||
from an ordered mapping (which is the default), then the first matching
|
||
mapping will always be returned. This also implies that the value
|
||
queried need not match the value set.
|
||
"""
|
||
state = super(LEDCharDisplay, self).value
|
||
if hasattr(self, 'dp'):
|
||
state, dp = state[:-1], state[-1]
|
||
else:
|
||
dp = False
|
||
try:
|
||
result = self._font._rmap[state]
|
||
except KeyError:
|
||
# Raising exceptions on lookup is problematic; in case the LED
|
||
# state is not representable we simply return None (although
|
||
# technically that is a valid item we can map :)
|
||
return None
|
||
else:
|
||
if dp:
|
||
return result + '.'
|
||
else:
|
||
return result
|
||
|
||
@value.setter
|
||
def value(self, value):
|
||
for led, v in zip(self, self._parse_state(value)):
|
||
led.value = v
|
||
|
||
def _parse_state(self, value):
|
||
if hasattr(self, 'dp'):
|
||
if len(value) > 1 and value.endswith('.'):
|
||
value = value[:-1]
|
||
dp = 1
|
||
else:
|
||
dp = 0
|
||
return self._font[value] + (dp,)
|
||
else:
|
||
return self._font[value]
|
||
|
||
|
||
class LEDMultiCharDisplay(CompositeOutputDevice):
|
||
"""
|
||
Wraps :class:`LEDCharDisplay` for multi-character `multiplexed`_ LED
|
||
character displays.
|
||
|
||
The class is constructed with a *char* which is an instance of the
|
||
:class:`LEDCharDisplay` class, capable of controlling the LEDs in one
|
||
character of the display, and an additional set of *pins* that represent
|
||
the common cathode (or anode) of each character.
|
||
|
||
.. warning::
|
||
|
||
You should not attempt to connect the common cathode (or anode) off
|
||
each character directly to a GPIO. Rather, use a set of transistors (or
|
||
some other suitable component capable of handling the current of all
|
||
the segment LEDs simultaneously) to connect the common cathode to
|
||
ground (or the common anode to the supply) and control those
|
||
transistors from the GPIOs specified under *pins*.
|
||
|
||
The *active_high* parameter defaults to :data:`True`. Note that it only
|
||
applies to the specified *pins*, which are assumed to be controlling a set
|
||
of transistors (hence the default). The specified *char* will use its own
|
||
*active_high* parameter. Finally, *initial_value* defaults to a tuple of
|
||
:attr:`~LEDCharDisplay.value` attribute of the specified display multiplied
|
||
by the number of *pins* provided.
|
||
|
||
When the :attr:`value` is set such that one or more characters in the
|
||
display differ in value, a background thread is implicitly started to
|
||
rotate the active character, relying on `persistence of vision`_ to display
|
||
the complete value.
|
||
|
||
.. _multiplexed: https://en.wikipedia.org/wiki/Multiplexed_display
|
||
.. _persistence of vision: https://en.wikipedia.org/wiki/Persistence_of_vision
|
||
"""
|
||
def __init__(self, char, *pins, **kwargs):
|
||
active_high = kwargs.pop('active_high', True)
|
||
initial_value = kwargs.pop('initial_value', None)
|
||
pin_factory = kwargs.pop('pin_factory', None)
|
||
if kwargs:
|
||
raise TypeError(
|
||
'unexpected keyword argument: %s' % kwargs.popiem()[0])
|
||
if not isinstance(char, LEDCharDisplay):
|
||
raise ValueError('char must be an LEDCharDisplay')
|
||
if initial_value is None:
|
||
initial_value = (char.value,) * len(pins)
|
||
if pin_factory is None:
|
||
pin_factory = char.pin_factory
|
||
self._plex_thread = None
|
||
self._plex_delay = 0.005
|
||
plex = CompositeOutputDevice(*(
|
||
OutputDevice(
|
||
pin, active_high=active_high, initial_value=None,
|
||
pin_factory=pin_factory)
|
||
for pin in pins
|
||
))
|
||
super(LEDMultiCharDisplay, self).__init__(
|
||
plex=plex, char=char, pin_factory=pin_factory)
|
||
self.value = initial_value
|
||
|
||
def close(self):
|
||
try:
|
||
self._stop_plex()
|
||
except AttributeError:
|
||
pass
|
||
super(LEDMultiCharDisplay, self).close()
|
||
|
||
def _stop_plex(self):
|
||
if self._plex_thread:
|
||
self._plex_thread.stop()
|
||
self._plex_thread = None
|
||
|
||
@property
|
||
def plex_delay(self):
|
||
"""
|
||
The delay (measured in seconds) in the loop used to switch each
|
||
character in the multiplexed display on. Defaults to 0.005 seconds
|
||
which is generally sufficient to provide a "stable" (non-flickery)
|
||
display.
|
||
"""
|
||
return self._plex_delay
|
||
|
||
@plex_delay.setter
|
||
def plex_delay(self, value):
|
||
if value < 0:
|
||
raise BadWaitTime('plex_delay must be 0 or greater')
|
||
self._plex_delay = float(value)
|
||
|
||
@property
|
||
def value(self):
|
||
"""
|
||
The sequence of values to display.
|
||
|
||
This can be any sequence containing keys from the
|
||
:attr:`~LEDCharDisplay.font` of the associated character display. For
|
||
example, if the value consists only of single-character strings, it's
|
||
valid to assign a string to this property (as a string is simply a
|
||
sequence of individual character keys)::
|
||
|
||
from gpiozero import LEDCharDisplay, LEDMultiCharDisplay
|
||
|
||
c = LEDCharDisplay(4, 5, 6, 7, 8, 9, 10)
|
||
d = LEDMultiCharDisplay(c, 19, 20, 21, 22)
|
||
d.value = 'LEDS'
|
||
|
||
However, things get more complicated if a decimal point is in use as
|
||
then this class needs to know explicitly where to break the value for
|
||
use on each character of the display. This can be handled by simply
|
||
assigning a sequence of strings thus::
|
||
|
||
from gpiozero import LEDCharDisplay, LEDMultiCharDisplay
|
||
|
||
c = LEDCharDisplay(4, 5, 6, 7, 8, 9, 10)
|
||
d = LEDMultiCharDisplay(c, 19, 20, 21, 22)
|
||
d.value = ('L.', 'E', 'D', 'S')
|
||
|
||
This is how the value will always be represented when queried (as a
|
||
tuple of individual values) as it neatly handles dealing with
|
||
heterogeneous types and the aforementioned decimal point issue.
|
||
|
||
.. note::
|
||
|
||
The value also controls whether a background thread is in use to
|
||
multiplex the display. When all positions in the value are equal
|
||
the background thread is disabled and all characters are
|
||
simultaneously enabled.
|
||
"""
|
||
return self._value
|
||
|
||
@value.setter
|
||
def value(self, value):
|
||
if len(value) > len(self.plex):
|
||
raise ValueError(
|
||
'length of value must not exceed the number of characters in '
|
||
'the display')
|
||
elif len(value) < len(self.plex):
|
||
# Right-align the short value on the display
|
||
value = (' ',) * (len(self.plex) - len(value)) + tuple(value)
|
||
else:
|
||
value = tuple(value)
|
||
|
||
# Get the list of tuples of states that the character LEDs will pass
|
||
# through. Prune any entirely blank state (which we can skip by never
|
||
# activating the plex for them) but remember which plex index each
|
||
# (non-blank) state is associated with
|
||
states = {}
|
||
for index, char in enumerate(value):
|
||
state = self.char._parse_state(char)
|
||
if any(state):
|
||
states.setdefault(state, set()).add(index)
|
||
# Calculate the transitions between states for an ordering of chars
|
||
# based on activated LEDs. This a vague attempt at minimizing the
|
||
# number of LEDs that need flipping between chars; to do this
|
||
# "properly" is O(n!) which gets silly quickly so ... fudge it
|
||
order = sorted(states)
|
||
if len(order) > 1:
|
||
transitions = [
|
||
[(self.plex[index], 0) for index in states[old]] +
|
||
[
|
||
(led, new_value)
|
||
for led, old_value, new_value in zip(self.char, old, new)
|
||
if old_value ^ new_value
|
||
] +
|
||
[(self.plex[index], 1) for index in states[new]]
|
||
for old, new in pairwise(order + [order[0]])
|
||
]
|
||
else:
|
||
transitions = []
|
||
|
||
# Stop any current display thread and disable the display
|
||
self._stop_plex()
|
||
self.plex.off()
|
||
|
||
# If there's any characters to display, set the character LEDs to the
|
||
# state of the first character in the display order. If there's
|
||
# transitions to display, activate the plex thread; otherwise, just
|
||
# switch on each plex with a char to display
|
||
if order:
|
||
for led, state in zip(self.char, order[0]):
|
||
led.value = state
|
||
if transitions:
|
||
self._plex_thread = GPIOThread(self._show_chars, (transitions,))
|
||
self._plex_thread.start()
|
||
else:
|
||
for index in states[order[0]]:
|
||
self.plex[index].on()
|
||
self._value = value
|
||
|
||
def _show_chars(self, transitions):
|
||
for transition in cycle(transitions):
|
||
for device, value in transition:
|
||
device.value = value
|
||
if self._plex_thread.stopping.wait(self._plex_delay):
|
||
break
|
||
|
||
|
||
class PiHutXmasTree(LEDBoard):
|
||
"""
|
||
Extends :class:`LEDBoard` for `The Pi Hut's Xmas board`_: a 3D Christmas
|
||
tree board with 24 red LEDs and a white LED as a star on top.
|
||
|
||
The 24 red LEDs can be accessed through the attributes led0, led1, led2,
|
||
and so on. The white star LED is accessed through the :attr:`star`
|
||
attribute. Alternatively, as with all descendents of :class:`LEDBoard`,
|
||
you can treat the instance as a sequence of LEDs (the first element is the
|
||
:attr:`star`).
|
||
|
||
The Xmas Tree board pins are fixed and therefore there's no need to specify
|
||
them when constructing this class. The following example turns all the LEDs
|
||
on one at a time::
|
||
|
||
from gpiozero import PiHutXmasTree
|
||
from time import sleep
|
||
|
||
tree = PiHutXmasTree()
|
||
|
||
for light in tree:
|
||
light.on()
|
||
sleep(1)
|
||
|
||
The following example turns the star LED on and sets all the red LEDs to
|
||
flicker randomly::
|
||
|
||
from gpiozero import PiHutXmasTree
|
||
from gpiozero.tools import random_values
|
||
from signal import pause
|
||
|
||
tree = PiHutXmasTree(pwm=True)
|
||
|
||
tree.star.on()
|
||
|
||
for led in tree[1:]:
|
||
led.source_delay = 0.1
|
||
led.source = random_values()
|
||
|
||
pause()
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances for each pin. If
|
||
:data:`False` (the default), construct regular :class:`LED` instances.
|
||
|
||
:type initial_value: bool or None
|
||
:param initial_value:
|
||
If :data:`False` (the default), all LEDs will be off initially. If
|
||
:data:`None`, each device will be left in whatever state the pin is
|
||
found in when configured for output (warning: this can be on). If
|
||
:data:`True`, the device will be switched on initially.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _The Pi Hut's Xmas board: https://thepihut.com/xmas
|
||
|
||
.. attribute:: star
|
||
|
||
Returns the :class:`LED` or :class:`PWMLED` representing the white
|
||
star on top of the tree.
|
||
|
||
.. attribute:: led0, led1, led2, ...
|
||
|
||
Returns the :class:`LED` or :class:`PWMLED` representing one of the red
|
||
LEDs. There are actually 24 of these properties named led0, led1, and
|
||
so on but for the sake of brevity we represent all 24 under this
|
||
section.
|
||
"""
|
||
def __init__(self, pwm=False, initial_value=False, pin_factory=None):
|
||
pins_dict = OrderedDict(star=2)
|
||
pins = (4, 15, 13, 21, 25, 8, 5, 10, 16, 17, 27, 26,
|
||
24, 9, 12, 6, 20, 19, 14, 18, 11, 7, 23, 22)
|
||
for i, pin in enumerate(pins):
|
||
pins_dict['led%d' % (i+1)] = pin
|
||
super(PiHutXmasTree, self).__init__(
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=pins_dict.keys(),
|
||
pin_factory=pin_factory,
|
||
**pins_dict
|
||
)
|
||
|
||
|
||
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)
|
||
|
||
:type initial_value: ~colorzero.Color or tuple
|
||
:param initial_value:
|
||
The initial color for the LedBorg. Defaults to black ``(0, 0, 0)``.
|
||
|
||
:param bool pwm:
|
||
If :data:`True` (the default), construct :class:`PWMLED` instances for
|
||
each component of the LedBorg. If :data:`False`, construct regular
|
||
:class:`LED` instances, which prevents smooth color graduations.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _PiBorg LedBorg: https://www.piborg.org/ledborg
|
||
"""
|
||
|
||
def __init__(self, initial_value=(0, 0, 0), pwm=True, pin_factory=None):
|
||
super(LedBorg, self).__init__(
|
||
red='BOARD11', green='BOARD13', blue='BOARD15',
|
||
pwm=pwm, initial_value=initial_value, pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class PiLiter(LEDBoard):
|
||
"""
|
||
Extends :class:`LEDBoard` for the `Ciseco Pi-LITEr`_: a strip of 8 very
|
||
bright LEDs.
|
||
|
||
The Pi-LITEr pins are fixed and therefore there's no need to specify them
|
||
when constructing this class. The following example turns on all the LEDs
|
||
of the Pi-LITEr::
|
||
|
||
from gpiozero import PiLiter
|
||
|
||
lite = PiLiter()
|
||
lite.on()
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances for each pin. If
|
||
:data:`False` (the default), construct regular :class:`LED` instances.
|
||
|
||
:type initial_value: bool or None
|
||
:param initial_value:
|
||
If :data:`False` (the default), all LEDs will be off initially. If
|
||
:data:`None`, each LED will be left in whatever state the pin is found
|
||
in when configured for output (warning: this can be on). If
|
||
:data:`True`, the each LED will be switched on initially.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/
|
||
"""
|
||
|
||
def __init__(self, pwm=False, initial_value=False, pin_factory=None):
|
||
pins = ('BOARD7', 'BOARD11', 'BOARD13', 'BOARD12',
|
||
'BOARD15', 'BOARD16', 'BOARD18', 'BOARD22')
|
||
super(PiLiter, self).__init__(
|
||
*pins, pwm=pwm, initial_value=initial_value, pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class PiLiterBarGraph(LEDBarGraph):
|
||
"""
|
||
Extends :class:`LEDBarGraph` to treat the `Ciseco Pi-LITEr`_ as an
|
||
8-segment bar graph.
|
||
|
||
The Pi-LITEr pins are fixed and therefore there's no need to specify them
|
||
when constructing this class. The following example sets the graph value
|
||
to 0.5::
|
||
|
||
from gpiozero import PiLiterBarGraph
|
||
|
||
graph = PiLiterBarGraph()
|
||
graph.value = 0.5
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances for each pin. If
|
||
:data:`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``.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/
|
||
"""
|
||
|
||
def __init__(self, pwm=False, initial_value=0.0, pin_factory=None):
|
||
pins = ('BOARD7', 'BOARD11', 'BOARD13', 'BOARD12',
|
||
'BOARD15', 'BOARD16', 'BOARD18', 'BOARD22')
|
||
super(PiLiterBarGraph, self).__init__(
|
||
*pins, pwm=pwm, initial_value=initial_value, pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class TrafficLights(LEDBoard):
|
||
"""
|
||
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 (yellow) LED attached to GPIO 3::
|
||
|
||
from gpiozero import TrafficLights
|
||
|
||
traffic = TrafficLights(2, 3, 4)
|
||
traffic.amber.on()
|
||
|
||
:type red: int or str
|
||
:param red:
|
||
The GPIO pin that the red LED is attached to. See :ref:`pin-numbering`
|
||
for valid pin numbers.
|
||
|
||
:type amber: int or str or None
|
||
:param amber:
|
||
The GPIO pin that the amber LED is attached to. See
|
||
:ref:`pin-numbering` for valid pin numbers.
|
||
|
||
:type yellow: int or str or None
|
||
:param 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``. See :ref:`pin-numbering` for valid pin numbers.
|
||
|
||
:type green: int or str
|
||
:param green:
|
||
The GPIO pin that the green LED is attached to. See
|
||
:ref:`pin-numbering` for valid pin numbers.
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances to represent each
|
||
LED. If :data:`False` (the default), construct regular :class:`LED`
|
||
instances.
|
||
|
||
:type initial_value: bool or None
|
||
:param initial_value:
|
||
If :data:`False` (the default), all LEDs will be off initially. If
|
||
:data:`None`, each device will be left in whatever state the pin is
|
||
found in when configured for output (warning: this can be on). If
|
||
:data:`True`, the device will be switched on initially.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. attribute:: red
|
||
|
||
The red :class:`LED` or :class:`PWMLED`.
|
||
|
||
.. attribute:: amber
|
||
|
||
The amber :class:`LED` or :class:`PWMLED`. Note that this attribute
|
||
will not be present when the instance is constructed with the
|
||
*yellow* keyword parameter.
|
||
|
||
.. attribute:: yellow
|
||
|
||
The yellow :class:`LED` or :class:`PWMLED`. Note that this attribute
|
||
will only be present when the instance is constructed with the
|
||
*yellow* keyword parameter.
|
||
|
||
.. attribute:: green
|
||
|
||
The green :class:`LED` or :class:`PWMLED`.
|
||
"""
|
||
def __init__(self, red=None, amber=None, green=None,
|
||
pwm=False, initial_value=False, yellow=None,
|
||
pin_factory=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('%s pins must be provided' %
|
||
', '.join(devices.keys()))
|
||
super(TrafficLights, self).__init__(
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=devices.keys(), pin_factory=pin_factory,
|
||
**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)
|
||
|
||
|
||
class PiTraffic(TrafficLights):
|
||
"""
|
||
Extends :class:`TrafficLights` for the `Low Voltage Labs PI-TRAFFIC`_
|
||
vertical traffic lights board when attached to GPIO pins 9, 10, and 11.
|
||
|
||
There's no need to specify the pins if the PI-TRAFFIC is connected to the
|
||
default pins (9, 10, 11). The following example turns on the amber LED on
|
||
the PI-TRAFFIC::
|
||
|
||
from gpiozero import PiTraffic
|
||
|
||
traffic = PiTraffic()
|
||
traffic.amber.on()
|
||
|
||
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 :data:`True`, construct :class:`PWMLED` instances to represent each
|
||
LED. If :data:`False` (the default), construct regular :class:`LED`
|
||
instances.
|
||
|
||
:type initial_value: bool or None
|
||
:param bool initial_value:
|
||
If :data:`False` (the default), all LEDs will be off initially. If
|
||
:data:`None`, each device will be left in whatever state the pin is
|
||
found in when configured for output (warning: this can be on). If
|
||
:data:`True`, the device will be switched on initially.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Low Voltage Labs PI-TRAFFIC: http://lowvoltagelabs.com/products/pi-traffic/
|
||
"""
|
||
def __init__(self, pwm=False, initial_value=False, pin_factory=None):
|
||
super(PiTraffic, self).__init__(
|
||
'BOARD21', 'BOARD19', 'BOARD23',
|
||
pwm=pwm, initial_value=initial_value, pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class PiStop(TrafficLights):
|
||
"""
|
||
Extends :class:`TrafficLights` for the `PiHardware Pi-Stop`_: a vertical
|
||
traffic lights board.
|
||
|
||
The following example turns on the amber LED on a Pi-Stop connected to
|
||
location ``A+``::
|
||
|
||
from gpiozero import PiStop
|
||
|
||
traffic = PiStop('A+')
|
||
traffic.amber.on()
|
||
|
||
:param str location:
|
||
The `location`_ on the GPIO header to which the Pi-Stop is connected.
|
||
Must be one of: ``A``, ``A+``, ``B``, ``B+``, ``C``, ``D``.
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances to represent each
|
||
LED. If :data:`False` (the default), construct regular :class:`LED`
|
||
instances.
|
||
|
||
:type initial_value: bool or None
|
||
:param bool initial_value:
|
||
If :data:`False` (the default), all LEDs will be off initially. If
|
||
:data:`None`, each device will be left in whatever state the pin is
|
||
found in when configured for output (warning: this can be on). If
|
||
:data:`True`, the device will be switched on initially.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _PiHardware Pi-Stop: https://pihw.wordpress.com/meltwaters-pi-hardware-kits/pi-stop/
|
||
.. _location: https://github.com/PiHw/Pi-Stop/blob/master/markdown_source/markdown/Discover-PiStop.md
|
||
"""
|
||
LOCATIONS = {
|
||
'A': ('BOARD26', 'BOARD24', 'BOARD22'),
|
||
'A+': ('BOARD40', 'BOARD38', 'BOARD36'),
|
||
'B': ('BOARD19', 'BOARD21', 'BOARD23'),
|
||
'B+': ('BOARD33', 'BOARD35', 'BOARD37'),
|
||
'C': ('BOARD12', 'BOARD10', 'BOARD8'),
|
||
'D': ('BOARD3', 'BOARD5', 'BOARD7'),
|
||
}
|
||
|
||
def __init__(
|
||
self, location=None, pwm=False, initial_value=False,
|
||
pin_factory=None):
|
||
gpios = self.LOCATIONS.get(location, None)
|
||
if gpios is None:
|
||
raise ValueError('location must be one of: %s' %
|
||
', '.join(sorted(self.LOCATIONS.keys())))
|
||
super(PiStop, self).__init__(
|
||
*gpios, pwm=pwm, initial_value=initial_value,
|
||
pin_factory=pin_factory)
|
||
|
||
|
||
class StatusZero(LEDBoard):
|
||
"""
|
||
Extends :class:`LEDBoard` for The Pi Hut's `STATUS Zero`_: a Pi Zero sized
|
||
add-on board with three sets of red/green LEDs to provide a status
|
||
indicator.
|
||
|
||
The following example designates the first strip the label "wifi" and the
|
||
second "raining", and turns them green and red respectfully::
|
||
|
||
from gpiozero import StatusZero
|
||
|
||
status = StatusZero('wifi', 'raining')
|
||
status.wifi.green.on()
|
||
status.raining.red.on()
|
||
|
||
Each designated label will contain two :class:`LED` objects named "red"
|
||
and "green".
|
||
|
||
:param str \\*labels:
|
||
Specify the names of the labels you wish to designate the strips to.
|
||
You can list up to three labels. If no labels are given, three strips
|
||
will be initialised with names 'one', 'two', and 'three'. If some, but
|
||
not all strips are given labels, any remaining strips will not be
|
||
initialised.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _STATUS Zero: https://thepihut.com/statuszero
|
||
|
||
.. attribute:: your-label-here, your-label-here, ...
|
||
|
||
This entry represents one of the three labelled attributes supported on
|
||
the STATUS Zero board. It is an :class:`LEDBoard` which contains:
|
||
|
||
.. attribute:: red
|
||
|
||
The :class:`LED` or :class:`PWMLED` representing the red LED
|
||
next to the label.
|
||
|
||
.. attribute:: green
|
||
|
||
The :class:`LED` or :class:`PWMLED` representing the green LED
|
||
next to the label.
|
||
"""
|
||
default_labels = ('one', 'two', 'three')
|
||
|
||
def __init__(self, *labels, **kwargs):
|
||
pins = (
|
||
('BOARD11', 'BOARD7'),
|
||
('BOARD15', 'BOARD13'),
|
||
('BOARD21', 'BOARD19'),
|
||
)
|
||
pin_factory = kwargs.pop('pin_factory', None)
|
||
if len(labels) == 0:
|
||
labels = self.default_labels
|
||
elif len(labels) > len(pins):
|
||
raise ValueError("StatusZero doesn't support more than three labels")
|
||
dup, count = Counter(labels).most_common(1)[0]
|
||
if count > 1:
|
||
raise ValueError("Duplicate label %s" % dup)
|
||
super(StatusZero, self).__init__(
|
||
_order=labels, pin_factory=pin_factory, **{
|
||
label: LEDBoard(
|
||
red=red, green=green, _order=('red', 'green'),
|
||
pin_factory=pin_factory, **kwargs
|
||
)
|
||
for (green, red), label in zip(pins, labels)
|
||
}
|
||
)
|
||
|
||
|
||
class StatusBoard(CompositeOutputDevice):
|
||
"""
|
||
Extends :class:`CompositeOutputDevice` for The Pi Hut's `STATUS`_ board: a
|
||
HAT sized add-on board with five sets of red/green LEDs and buttons to
|
||
provide a status indicator with additional input.
|
||
|
||
The following example designates the first strip the label "wifi" and the
|
||
second "raining", turns the wifi green and then activates the button to
|
||
toggle its lights when pressed::
|
||
|
||
from gpiozero import StatusBoard
|
||
|
||
status = StatusBoard('wifi', 'raining')
|
||
status.wifi.lights.green.on()
|
||
status.wifi.button.when_pressed = status.wifi.lights.toggle
|
||
|
||
Each designated label will contain a "lights" :class:`LEDBoard` containing
|
||
two :class:`LED` objects named "red" and "green", and a :class:`Button`
|
||
object named "button".
|
||
|
||
:param str \\*labels:
|
||
Specify the names of the labels you wish to designate the strips to.
|
||
You can list up to five labels. If no labels are given, five strips
|
||
will be initialised with names 'one' to 'five'. If some, but not all
|
||
strips are given labels, any remaining strips will not be initialised.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _STATUS: https://thepihut.com/status
|
||
|
||
.. attribute:: your-label-here, your-label-here, ...
|
||
|
||
This entry represents one of the five labelled attributes supported on
|
||
the STATUS board. It is an :class:`CompositeOutputDevice` which
|
||
contains:
|
||
|
||
.. attribute:: lights
|
||
|
||
A :class:`LEDBoard` representing the lights next to the label. It
|
||
contains:
|
||
|
||
.. attribute:: red
|
||
|
||
The :class:`LED` or :class:`PWMLED` representing the red LED
|
||
next to the label.
|
||
|
||
.. attribute:: green
|
||
|
||
The :class:`LED` or :class:`PWMLED` representing the green LED
|
||
next to the label.
|
||
|
||
.. attribute:: button
|
||
|
||
A :class:`Button` representing the button next to the label.
|
||
"""
|
||
default_labels = ('one', 'two', 'three', 'four', 'five')
|
||
|
||
def __init__(self, *labels, **kwargs):
|
||
pins = (
|
||
('BOARD11', 'BOARD7', 'BOARD8'),
|
||
('BOARD15', 'BOARD13', 'BOARD35'),
|
||
('BOARD21', 'BOARD19', 'BOARD10'),
|
||
('BOARD29', 'BOARD23', 'BOARD37'),
|
||
('BOARD33', 'BOARD31', 'BOARD12'),
|
||
)
|
||
pin_factory = kwargs.pop('pin_factory', None)
|
||
if len(labels) == 0:
|
||
labels = self.default_labels
|
||
elif len(labels) > len(pins):
|
||
raise ValueError("StatusBoard doesn't support more than five labels")
|
||
dup, count = Counter(labels).most_common(1)[0]
|
||
if count > 1:
|
||
raise ValueError("Duplicate label %s" % dup)
|
||
super(StatusBoard, self).__init__(
|
||
_order=labels, pin_factory=pin_factory, **{
|
||
label: CompositeOutputDevice(
|
||
button=Button(button, pin_factory=pin_factory),
|
||
lights=LEDBoard(
|
||
red=red, green=green, _order=('red', 'green'),
|
||
pin_factory=pin_factory, **kwargs
|
||
), _order=('button', 'lights'), pin_factory=pin_factory
|
||
)
|
||
for (green, red, button), label in zip(pins, labels)
|
||
}
|
||
)
|
||
|
||
|
||
class SnowPi(LEDBoard):
|
||
"""
|
||
Extends :class:`LEDBoard` for the `Ryanteck SnowPi`_ board.
|
||
|
||
The SnowPi pins are fixed and therefore there's no need to specify them
|
||
when constructing this class. The following example turns on the eyes, sets
|
||
the nose pulsing, and the arms blinking::
|
||
|
||
from gpiozero import SnowPi
|
||
|
||
snowman = SnowPi(pwm=True)
|
||
snowman.eyes.on()
|
||
snowman.nose.pulse()
|
||
snowman.arms.blink()
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances to represent each
|
||
LED. If :data:`False` (the default), construct regular :class:`LED`
|
||
instances.
|
||
|
||
:type initial_value: bool or None
|
||
:param bool initial_value:
|
||
If :data:`False` (the default), all LEDs will be off initially. If
|
||
:data:`None`, each device will be left in whatever state the pin is
|
||
found in when configured for output (warning: this can be on). If
|
||
:data:`True`, the device will be switched on initially.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Ryanteck SnowPi: https://ryanteck.uk/raspberry-pi/114-snowpi-the-gpio-snowman-for-raspberry-pi-0635648608303.html
|
||
|
||
.. attribute:: arms
|
||
|
||
A :class:`LEDBoard` representing the arms of the snow man. It contains
|
||
the following attributes:
|
||
|
||
.. attribute:: left, right
|
||
|
||
Two :class:`LEDBoard` objects representing the left and right arms
|
||
of the snow-man. They contain:
|
||
|
||
.. attribute:: top, middle, bottom
|
||
|
||
The :class:`LED` or :class:`PWMLED` down the snow-man's arms.
|
||
|
||
.. attribute:: eyes
|
||
|
||
A :class:`LEDBoard` representing the eyes of the snow-man. It contains:
|
||
|
||
.. attribute:: left, right
|
||
|
||
The :class:`LED` or :class:`PWMLED` for the snow-man's eyes.
|
||
|
||
.. attribute:: nose
|
||
|
||
The :class:`LED` or :class:`PWMLED` for the snow-man's nose.
|
||
"""
|
||
def __init__(self, pwm=False, initial_value=False, pin_factory=None):
|
||
super(SnowPi, self).__init__(
|
||
arms=LEDBoard(
|
||
left=LEDBoard(
|
||
top='BOARD11', middle='BOARD12', bottom='BOARD15',
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=('top', 'middle', 'bottom'),
|
||
pin_factory=pin_factory),
|
||
right=LEDBoard(
|
||
top='BOARD26', middle='BOARD24', bottom='BOARD21',
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=('top', 'middle', 'bottom'),
|
||
pin_factory=pin_factory),
|
||
_order=('left', 'right'),
|
||
pin_factory=pin_factory
|
||
),
|
||
eyes=LEDBoard(
|
||
left='BOARD16', right='BOARD18',
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=('left', 'right'),
|
||
pin_factory=pin_factory
|
||
),
|
||
nose='BOARD22',
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=('eyes', 'nose', 'arms'),
|
||
pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class TrafficLightsBuzzer(CompositeOutputDevice):
|
||
"""
|
||
Extends :class:`CompositeOutputDevice` and is a generic class for HATs with
|
||
traffic lights, a button and a buzzer.
|
||
|
||
:param TrafficLights lights:
|
||
An instance of :class:`TrafficLights` representing the traffic lights
|
||
of the HAT.
|
||
|
||
:param Buzzer buzzer:
|
||
An instance of :class:`Buzzer` representing the buzzer on the HAT.
|
||
|
||
:param Button button:
|
||
An instance of :class:`Button` representing the button on the HAT.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. attribute:: lights
|
||
|
||
The :class:`TrafficLights` instance passed as the *lights* parameter.
|
||
|
||
.. attribute:: buzzer
|
||
|
||
The :class:`Buzzer` instance passed as the *buzzer* parameter.
|
||
|
||
.. attribute:: button
|
||
|
||
The :class:`Button` instance passed as the *button* parameter.
|
||
"""
|
||
def __init__(self, lights, buzzer, button, pin_factory=None):
|
||
super(TrafficLightsBuzzer, self).__init__(
|
||
lights=lights, buzzer=buzzer, button=button,
|
||
_order=('lights', 'buzzer', 'button'),
|
||
pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class FishDish(CompositeOutputDevice):
|
||
"""
|
||
Extends :class:`CompositeOutputDevice` 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
|
||
when constructing this class. The following example waits for the button
|
||
to be pressed on the FishDish, then turns on all the LEDs::
|
||
|
||
from gpiozero import FishDish
|
||
|
||
fish = FishDish()
|
||
fish.button.wait_for_press()
|
||
fish.lights.on()
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances to represent each
|
||
LED. If :data:`False` (the default), construct regular :class:`LED`
|
||
instances.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Pi Supply FishDish: https://www.pi-supply.com/product/fish-dish-raspberry-pi-led-buzzer-board/
|
||
"""
|
||
def __init__(self, pwm=False, pin_factory=None):
|
||
super(FishDish, self).__init__(
|
||
lights=TrafficLights(
|
||
'BOARD21', 'BOARD15', 'BOARD7', pwm=pwm, pin_factory=pin_factory
|
||
),
|
||
buzzer=Buzzer('BOARD24', pin_factory=pin_factory),
|
||
button=Button('BOARD26', pull_up=False, pin_factory=pin_factory),
|
||
_order=('lights', 'buzzer', 'button'),
|
||
pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class TrafficHat(CompositeOutputDevice):
|
||
"""
|
||
Extends :class:`CompositeOutputDevice` for the `Pi Supply Traffic HAT`_: a
|
||
board with traffic light LEDs, a button and a buzzer.
|
||
|
||
The Traffic HAT pins are fixed and therefore there's no need to specify
|
||
them when constructing this class. The following example waits for the
|
||
button to be pressed on the Traffic HAT, then turns on all the LEDs::
|
||
|
||
from gpiozero import TrafficHat
|
||
|
||
hat = TrafficHat()
|
||
hat.button.wait_for_press()
|
||
hat.lights.on()
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances to represent each
|
||
LED. If :data:`False` (the default), construct regular :class:`LED`
|
||
instances.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Pi Supply Traffic HAT: https://uk.pi-supply.com/products/traffic-hat-for-raspberry-pi
|
||
"""
|
||
def __init__(self, pwm=False, pin_factory=None):
|
||
super(TrafficHat, self).__init__(
|
||
lights=TrafficLights(
|
||
'BOARD18', 'BOARD16', 'BOARD15',
|
||
pwm=pwm, pin_factory=pin_factory
|
||
),
|
||
buzzer=Buzzer('BOARD29', pin_factory=pin_factory),
|
||
button=Button('BOARD22', pin_factory=pin_factory),
|
||
_order=('lights', 'buzzer', 'button'),
|
||
pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class TrafficpHat(TrafficLights):
|
||
"""
|
||
Extends :class:`TrafficLights` for the `Pi Supply Traffic pHAT`_: a small
|
||
board with traffic light LEDs.
|
||
|
||
The Traffic pHAT pins are fixed and therefore there's no need to specify
|
||
them when constructing this class. The following example then turns on all
|
||
the LEDs::
|
||
|
||
from gpiozero import TrafficpHat
|
||
phat = TrafficpHat()
|
||
phat.red.on()
|
||
phat.blink()
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances to represent each
|
||
LED. If :data:`False` (the default), construct regular :class:`LED`
|
||
instances.
|
||
|
||
:type initial_value: bool or None
|
||
:param initial_value:
|
||
If :data:`False` (the default), all LEDs will be off initially. If
|
||
:data:`None`, each device will be left in whatever state the pin is
|
||
found in when configured for output (warning: this can be on). If
|
||
:data:`True`, the device will be switched on initially.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Pi Supply Traffic pHAT: http://pisupp.ly/trafficphat
|
||
"""
|
||
def __init__(self, pwm=False, initial_value=False, pin_factory=None):
|
||
super(TrafficpHat, self).__init__(
|
||
red='BOARD22', amber='BOARD18', green='BOARD16',
|
||
pwm=pwm, initial_value=initial_value, pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class Robot(SourceMixin, CompositeDevice):
|
||
"""
|
||
Extends :class:`CompositeDevice` to represent a generic dual-motor robot.
|
||
|
||
This class is constructed with two tuples representing the forward and
|
||
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 drive the robot forward::
|
||
|
||
from gpiozero import Robot
|
||
|
||
robot = Robot(left=(4, 14), right=(17, 18))
|
||
robot.forward()
|
||
|
||
:param tuple left:
|
||
A tuple of two (or three) GPIO pins representing the forward and
|
||
backward inputs of the left motor's controller. Use three pins if your
|
||
motor controller requires an enable pin.
|
||
|
||
:param tuple right:
|
||
A tuple of two (or three) GPIO pins representing the forward and
|
||
backward inputs of the right motor's controller. Use three pins if your
|
||
motor controller requires an enable pin.
|
||
|
||
:param bool pwm:
|
||
If :data:`True` (the default), construct :class:`PWMOutputDevice`
|
||
instances for the motor controller pins, allowing both direction and
|
||
variable speed control. If :data:`False`, construct
|
||
:class:`DigitalOutputDevice` instances, allowing only direction
|
||
control.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. attribute:: left_motor
|
||
|
||
The :class:`Motor` on the left of the robot.
|
||
|
||
.. attribute:: right_motor
|
||
|
||
The :class:`Motor` on the right of the robot.
|
||
"""
|
||
def __init__(self, left=None, right=None, pwm=True, pin_factory=None, *args):
|
||
# *args is a hack to ensure a useful message is shown when pins are
|
||
# supplied as sequential positional arguments e.g. 2, 3, 4, 5
|
||
if not isinstance(left, tuple) or not isinstance(right, tuple):
|
||
raise GPIOPinMissing('left and right motor pins must be given as '
|
||
'tuples')
|
||
super(Robot, self).__init__(
|
||
left_motor=Motor(*left, pwm=pwm, pin_factory=pin_factory),
|
||
right_motor=Motor(*right, pwm=pwm, pin_factory=pin_factory),
|
||
_order=('left_motor', 'right_motor'),
|
||
pin_factory=pin_factory
|
||
)
|
||
|
||
@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, **kwargs):
|
||
"""
|
||
Drive the robot forward by running both motors forward.
|
||
|
||
:param float speed:
|
||
Speed at which to drive the motors, as a value between 0 (stopped)
|
||
and 1 (full speed). The default is 1.
|
||
|
||
:param float curve_left:
|
||
The amount to curve left while moving forwards, by driving the
|
||
left motor at a slower speed. Maximum *curve_left* is 1, the
|
||
default is 0 (no curve). This parameter can only be specified as a
|
||
keyword parameter, and is mutually exclusive with *curve_right*.
|
||
|
||
:param float curve_right:
|
||
The amount to curve right while moving forwards, by driving the
|
||
right motor at a slower speed. Maximum *curve_right* is 1, the
|
||
default is 0 (no curve). This parameter can only be specified as a
|
||
keyword parameter, and is mutually exclusive with *curve_left*.
|
||
"""
|
||
curve_left = kwargs.pop('curve_left', 0)
|
||
curve_right = kwargs.pop('curve_right', 0)
|
||
if kwargs:
|
||
raise TypeError('unexpected argument %s' % kwargs.popitem()[0])
|
||
if not 0 <= curve_left <= 1:
|
||
raise ValueError('curve_left must be between 0 and 1')
|
||
if not 0 <= curve_right <= 1:
|
||
raise ValueError('curve_right must be between 0 and 1')
|
||
if curve_left != 0 and curve_right != 0:
|
||
raise ValueError("curve_left and curve_right can't be used at "
|
||
"the same time")
|
||
self.left_motor.forward(speed * (1 - curve_left))
|
||
self.right_motor.forward(speed * (1 - curve_right))
|
||
|
||
def backward(self, speed=1, **kwargs):
|
||
"""
|
||
Drive the robot backward by running both motors backward.
|
||
|
||
:param float speed:
|
||
Speed at which to drive the motors, as a value between 0 (stopped)
|
||
and 1 (full speed). The default is 1.
|
||
|
||
:param float curve_left:
|
||
The amount to curve left while moving backwards, by driving the
|
||
left motor at a slower speed. Maximum *curve_left* is 1, the
|
||
default is 0 (no curve). This parameter can only be specified as a
|
||
keyword parameter, and is mutually exclusive with *curve_right*.
|
||
|
||
:param float curve_right:
|
||
The amount to curve right while moving backwards, by driving the
|
||
right motor at a slower speed. Maximum *curve_right* is 1, the
|
||
default is 0 (no curve). This parameter can only be specified as a
|
||
keyword parameter, and is mutually exclusive with *curve_left*.
|
||
"""
|
||
curve_left = kwargs.pop('curve_left', 0)
|
||
curve_right = kwargs.pop('curve_right', 0)
|
||
if kwargs:
|
||
raise TypeError('unexpected argument %s' % kwargs.popitem()[0])
|
||
if not 0 <= curve_left <= 1:
|
||
raise ValueError('curve_left must be between 0 and 1')
|
||
if not 0 <= curve_right <= 1:
|
||
raise ValueError('curve_right must be between 0 and 1')
|
||
if curve_left != 0 and curve_right != 0:
|
||
raise ValueError("curve_left and curve_right can't be used at "
|
||
"the same time")
|
||
self.left_motor.backward(speed * (1 - curve_left))
|
||
self.right_motor.backward(speed * (1 - curve_right))
|
||
|
||
def left(self, speed=1):
|
||
"""
|
||
Make the robot turn left by running the right motor forward and left
|
||
motor backward.
|
||
|
||
:param float speed:
|
||
Speed at which to drive the motors, as a value between 0 (stopped)
|
||
and 1 (full speed). The default is 1.
|
||
"""
|
||
self.right_motor.forward(speed)
|
||
self.left_motor.backward(speed)
|
||
|
||
def right(self, speed=1):
|
||
"""
|
||
Make the robot turn right by running the left motor forward and right
|
||
motor backward.
|
||
|
||
:param float speed:
|
||
Speed at which to drive the motors, as a value between 0 (stopped)
|
||
and 1 (full speed). The default is 1.
|
||
"""
|
||
self.left_motor.forward(speed)
|
||
self.right_motor.backward(speed)
|
||
|
||
def reverse(self):
|
||
"""
|
||
Reverse the robot's current motor directions. If the robot is currently
|
||
running full speed forward, it will run full speed backward. If the
|
||
robot is turning left at half-speed, it will turn right at half-speed.
|
||
If the robot is currently stopped it will remain stopped.
|
||
"""
|
||
self.left_motor.reverse()
|
||
self.right_motor.reverse()
|
||
|
||
def stop(self):
|
||
"""
|
||
Stop the robot.
|
||
"""
|
||
self.left_motor.stop()
|
||
self.right_motor.stop()
|
||
|
||
|
||
class RyanteckRobot(Robot):
|
||
"""
|
||
Extends :class:`Robot` for the `Ryanteck motor controller board`_.
|
||
|
||
The Ryanteck MCB pins are fixed and therefore there's no need to specify
|
||
them when constructing this class. The following example drives the robot
|
||
forward::
|
||
|
||
from gpiozero import RyanteckRobot
|
||
|
||
robot = RyanteckRobot()
|
||
robot.forward()
|
||
|
||
:param bool pwm:
|
||
If :data:`True` (the default), construct :class:`PWMOutputDevice`
|
||
instances for the motor controller pins, allowing both direction and
|
||
variable speed control. If :data:`False`, construct
|
||
:class:`DigitalOutputDevice` instances, allowing only direction
|
||
control.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Ryanteck motor controller board: https://uk.pi-supply.com/products/ryanteck-rtk-000-001-motor-controller-board-kit-raspberry-pi
|
||
"""
|
||
|
||
def __init__(self, pwm=True, pin_factory=None):
|
||
super(RyanteckRobot, self).__init__(
|
||
left=('BOARD11', 'BOARD12'), right=('BOARD15', 'BOARD16'),
|
||
pwm=pwm, pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class CamJamKitRobot(Robot):
|
||
"""
|
||
Extends :class:`Robot` for the `CamJam #3 EduKit`_ motor controller board.
|
||
|
||
The CamJam robot controller pins are fixed and therefore there's no need
|
||
to specify them when constructing this class. The following example drives
|
||
the robot forward::
|
||
|
||
from gpiozero import CamJamKitRobot
|
||
|
||
robot = CamJamKitRobot()
|
||
robot.forward()
|
||
|
||
:param bool pwm:
|
||
If :data:`True` (the default), construct :class:`PWMOutputDevice`
|
||
instances for the motor controller pins, allowing both direction and
|
||
variable speed control. If :data:`False`, construct
|
||
:class:`DigitalOutputDevice` instances, allowing only direction
|
||
control.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _CamJam #3 EduKit: http://camjam.me/?page_id=1035
|
||
"""
|
||
def __init__(self, pwm=True, pin_factory=None):
|
||
super(CamJamKitRobot, self).__init__(
|
||
left=('BOARD21', 'BOARD19'), right=('BOARD26', 'BOARD24'),
|
||
pwm=pwm, pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class PhaseEnableRobot(SourceMixin, CompositeDevice):
|
||
"""
|
||
Extends :class:`CompositeDevice` to represent a dual-motor robot based
|
||
around a Phase/Enable motor board.
|
||
|
||
This class is constructed with two tuples representing the phase
|
||
(direction) and enable (speed) pins of the left and right controllers
|
||
respectively. For example, if the left motor's controller is connected to
|
||
GPIOs 12 and 5, while the right motor's controller is connected to GPIOs 13
|
||
and 6 so the following example will drive the robot forward::
|
||
|
||
from gpiozero import PhaseEnableRobot
|
||
|
||
robot = PhaseEnableRobot(left=(5, 12), right=(6, 13))
|
||
robot.forward()
|
||
|
||
:param tuple left:
|
||
A tuple of two GPIO pins representing the phase and enable inputs
|
||
of the left motor's controller.
|
||
|
||
:param tuple right:
|
||
A tuple of two GPIO pins representing the phase and enable inputs
|
||
of the right motor's controller.
|
||
|
||
:param bool pwm:
|
||
If :data:`True` (the default), construct :class:`PWMOutputDevice`
|
||
instances for the motor controller's enable pins, allowing both
|
||
direction and variable speed control. If :data:`False`, construct
|
||
:class:`DigitalOutputDevice` instances, allowing only direction
|
||
control.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. attribute:: left_motor
|
||
|
||
The :class:`PhaseEnableMotor` on the left of the robot.
|
||
|
||
.. attribute:: right_motor
|
||
|
||
The :class:`PhaseEnableMotor` on the right of the robot.
|
||
"""
|
||
def __init__(self, left=None, right=None, pwm=True, pin_factory=None, *args):
|
||
# *args is a hack to ensure a useful message is shown when pins are
|
||
# supplied as sequential positional arguments e.g. 2, 3, 4, 5
|
||
if not isinstance(left, tuple) or not isinstance(right, tuple):
|
||
raise GPIOPinMissing(
|
||
'left and right motor pins must be given as tuples'
|
||
)
|
||
super(PhaseEnableRobot, self).__init__(
|
||
left_motor=PhaseEnableMotor(*left, pwm=pwm, pin_factory=pin_factory),
|
||
right_motor=PhaseEnableMotor(*right, pwm=pwm, pin_factory=pin_factory),
|
||
_order=('left_motor', 'right_motor'),
|
||
pin_factory=pin_factory
|
||
)
|
||
|
||
@property
|
||
def value(self):
|
||
"""
|
||
Returns a tuple of two floating point values (-1 to 1) representing the
|
||
speeds of the robot's two motors (left and right). This property can
|
||
also be set to alter the speed of both motors.
|
||
"""
|
||
return super(PhaseEnableRobot, 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.
|
||
|
||
:param float speed:
|
||
Speed at which to drive the motors, as a value between 0 (stopped)
|
||
and 1 (full speed). The default is 1.
|
||
"""
|
||
self.left_motor.forward(speed)
|
||
self.right_motor.forward(speed)
|
||
|
||
def backward(self, speed=1):
|
||
"""
|
||
Drive the robot backward by running both motors backward.
|
||
|
||
:param float speed:
|
||
Speed at which to drive the motors, as a value between 0 (stopped)
|
||
and 1 (full speed). The default is 1.
|
||
"""
|
||
self.left_motor.backward(speed)
|
||
self.right_motor.backward(speed)
|
||
|
||
def left(self, speed=1):
|
||
"""
|
||
Make the robot turn left by running the right motor forward and left
|
||
motor backward.
|
||
|
||
:param float speed:
|
||
Speed at which to drive the motors, as a value between 0 (stopped)
|
||
and 1 (full speed). The default is 1.
|
||
"""
|
||
self.right_motor.forward(speed)
|
||
self.left_motor.backward(speed)
|
||
|
||
def right(self, speed=1):
|
||
"""
|
||
Make the robot turn right by running the left motor forward and right
|
||
motor backward.
|
||
|
||
:param float speed:
|
||
Speed at which to drive the motors, as a value between 0 (stopped)
|
||
and 1 (full speed). The default is 1.
|
||
"""
|
||
self.left_motor.forward(speed)
|
||
self.right_motor.backward(speed)
|
||
|
||
def reverse(self):
|
||
"""
|
||
Reverse the robot's current motor directions. If the robot is currently
|
||
running full speed forward, it will run full speed backward. If the
|
||
robot is turning left at half-speed, it will turn right at half-speed.
|
||
If the robot is currently stopped it will remain stopped.
|
||
"""
|
||
self.left_motor.value = -self.left_motor.value
|
||
self.right_motor.value = -self.right_motor.value
|
||
|
||
def stop(self):
|
||
"""
|
||
Stop the robot.
|
||
"""
|
||
self.left_motor.stop()
|
||
self.right_motor.stop()
|
||
|
||
|
||
class PololuDRV8835Robot(PhaseEnableRobot):
|
||
"""
|
||
Extends :class:`PhaseEnableRobot` for the `Pololu DRV8835 Dual Motor Driver
|
||
Kit`_.
|
||
|
||
The Pololu DRV8835 pins are fixed and therefore there's no need to specify
|
||
them when constructing this class. The following example drives the robot
|
||
forward::
|
||
|
||
from gpiozero import PololuDRV8835Robot
|
||
|
||
robot = PololuDRV8835Robot()
|
||
robot.forward()
|
||
|
||
:param bool pwm:
|
||
If :data:`True` (the default), construct :class:`PWMOutputDevice`
|
||
instances for the motor controller's enable pins, allowing both
|
||
direction and variable speed control. If :data:`False`, construct
|
||
:class:`DigitalOutputDevice` instances, allowing only direction
|
||
control.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Pololu DRV8835 Dual Motor Driver Kit: https://www.pololu.com/product/2753
|
||
"""
|
||
def __init__(self, pwm=True, pin_factory=None):
|
||
super(PololuDRV8835Robot, self).__init__(
|
||
left=('BOARD29', 'BOARD32'), right=('BOARD31', 'BOARD33'),
|
||
pwm=pwm, pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class _EnergenieMaster(SharedMixin, CompositeOutputDevice):
|
||
def __init__(self, pin_factory=None):
|
||
self._lock = Lock()
|
||
super(_EnergenieMaster, self).__init__(
|
||
*(
|
||
OutputDevice(pin, pin_factory=pin_factory)
|
||
for pin in ('BOARD11', 'BOARD15', 'BOARD16', 'BOARD13')
|
||
),
|
||
mode=OutputDevice('BOARD18', pin_factory=pin_factory),
|
||
enable=OutputDevice('BOARD22', pin_factory=pin_factory),
|
||
_order=('mode', 'enable'), pin_factory=pin_factory
|
||
)
|
||
|
||
def close(self):
|
||
if getattr(self, '_lock', None):
|
||
with self._lock:
|
||
super(_EnergenieMaster, self).close()
|
||
self._lock = None
|
||
|
||
@classmethod
|
||
def _shared_key(cls, pin_factory):
|
||
# There's only one Energenie master
|
||
return None
|
||
|
||
def transmit(self, socket, enable):
|
||
with self._lock:
|
||
try:
|
||
code = (8 * bool(enable)) + (8 - socket)
|
||
for bit in self[:4]:
|
||
bit.value = (code & 1)
|
||
code >>= 1
|
||
sleep(0.1)
|
||
self.enable.on()
|
||
sleep(0.25)
|
||
finally:
|
||
self.enable.off()
|
||
|
||
|
||
class Energenie(SourceMixin, Device):
|
||
"""
|
||
Extends :class:`Device` to represent an `Energenie socket`_ controller.
|
||
|
||
This class is constructed with a socket number and an optional initial
|
||
state (defaults to :data:`False`, meaning off). Instances of this class can
|
||
be used to switch peripherals on and off. For example::
|
||
|
||
from gpiozero import Energenie
|
||
|
||
lamp = Energenie(1)
|
||
lamp.on()
|
||
|
||
:param int socket:
|
||
Which socket this instance should control. This is an integer number
|
||
between 1 and 4.
|
||
|
||
:type initial_value: bool or None
|
||
:param initial_value:
|
||
The initial state of the socket. As Energenie sockets provide no
|
||
means of reading their state, you may provide an initial state for
|
||
the socket, which will be set upon construction. This defaults to
|
||
:data:`False` which will switch the socket off.
|
||
Specifying :data:`None` will not set any initial state nor transmit any
|
||
control signal to the device.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Energenie socket: https://energenie4u.co.uk/index.php/catalogue/product/ENER002-2PI
|
||
"""
|
||
def __init__(self, socket=None, initial_value=False, pin_factory=None):
|
||
if socket is None:
|
||
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__(pin_factory=pin_factory)
|
||
self._socket = socket
|
||
self._master = _EnergenieMaster(pin_factory=pin_factory)
|
||
if initial_value:
|
||
self.on()
|
||
elif initial_value is not None:
|
||
self.off()
|
||
|
||
def close(self):
|
||
if getattr(self, '_master', None):
|
||
self._master.close()
|
||
self._master = None
|
||
|
||
@property
|
||
def closed(self):
|
||
return self._master is None
|
||
|
||
def __repr__(self):
|
||
try:
|
||
self._check_open()
|
||
return "<gpiozero.Energenie object on socket %d>" % self._socket
|
||
except DeviceClosed:
|
||
return "<gpiozero.Energenie object closed>"
|
||
|
||
@property
|
||
def socket(self):
|
||
"""
|
||
Returns the socket number.
|
||
"""
|
||
return self._socket
|
||
|
||
@property
|
||
def value(self):
|
||
"""
|
||
Returns :data:`True` if the socket is on and :data:`False` if the
|
||
socket is off. Setting this property changes the state of the socket.
|
||
Returns :data:`None` only when constructed with :data:`initial_value`
|
||
set to :data:`None` and neither :data:`on()` nor :data:`off()` have
|
||
been called since construction.
|
||
"""
|
||
return self._value
|
||
|
||
@value.setter
|
||
def value(self, value):
|
||
if value is None:
|
||
raise TypeError('value cannot be None')
|
||
value = bool(value)
|
||
self._master.transmit(self._socket, value)
|
||
self._value = value
|
||
|
||
def on(self):
|
||
"""
|
||
Turns the socket on.
|
||
"""
|
||
self.value = True
|
||
|
||
def off(self):
|
||
"""
|
||
Turns the socket off.
|
||
"""
|
||
self.value = False
|
||
|
||
|
||
class PumpkinPi(LEDBoard):
|
||
"""
|
||
Extends :class:`LEDBoard` for the `ModMyPi PumpkinPi`_ board.
|
||
|
||
There are twelve LEDs connected up to individual pins, so for the PumpkinPi
|
||
the pins are fixed. For example::
|
||
|
||
from gpiozero import PumpkinPi
|
||
|
||
pumpkin = PumpkinPi(pwm=True)
|
||
pumpkin.sides.pulse()
|
||
pumpkin.off()
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances to represent each
|
||
LED. If :data:`False` (the default), construct regular :class:`LED`
|
||
instances
|
||
|
||
:type initial_value: bool or None
|
||
:param initial_value:
|
||
If :data:`False` (the default), all LEDs will be off initially. If
|
||
:data:`None`, each device will be left in whatever state the pin is
|
||
found in when configured for output (warning: this can be on). If
|
||
:data:`True`, the device will be switched on initially.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _ModMyPi PumpkinPi: https://www.modmypi.com/halloween-pumpkin-programmable-kit
|
||
|
||
.. attribute:: sides
|
||
|
||
A :class:`LEDBoard` representing the LEDs around the edge of the
|
||
pumpkin. It contains:
|
||
|
||
.. attribute:: left, right
|
||
|
||
Two :class:`LEDBoard` instances representing the LEDs on the left
|
||
and right sides of the pumpkin. They each contain:
|
||
|
||
.. attribute:: top, midtop, middle, midbottom, bottom
|
||
|
||
Each :class:`LED` or :class:`PWMLED` around the specified side
|
||
of the pumpkin.
|
||
|
||
.. attribute:: eyes
|
||
|
||
A :class:`LEDBoard` representing the eyes of the pumpkin. It contains:
|
||
|
||
.. attribute:: left, right
|
||
|
||
The :class:`LED` or :class:`PWMLED` for each of the pumpkin's eyes.
|
||
"""
|
||
def __init__(self, pwm=False, initial_value=False, pin_factory=None):
|
||
super(PumpkinPi, self).__init__(
|
||
sides=LEDBoard(
|
||
left=LEDBoard(
|
||
bottom='BOARD12', midbottom='BOARD11', middle='BOARD36',
|
||
midtop='BOARD33', top='BOARD18',
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=('bottom', 'midbottom', 'middle', 'midtop', 'top'),
|
||
pin_factory=pin_factory),
|
||
right=LEDBoard(
|
||
bottom='BOARD35', midbottom='BOARD38', middle='BOARD40',
|
||
midtop='BOARD15', top='BOARD16',
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=('bottom', 'midbottom', 'middle', 'midtop', 'top'),
|
||
pin_factory=pin_factory),
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=('left', 'right'),
|
||
pin_factory=pin_factory
|
||
),
|
||
eyes=LEDBoard(
|
||
left='BOARD32', right='BOARD31',
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=('left', 'right'),
|
||
pin_factory=pin_factory
|
||
),
|
||
pwm=pwm, initial_value=initial_value,
|
||
_order=('eyes', 'sides'),
|
||
pin_factory=pin_factory
|
||
)
|
||
|
||
|
||
class JamHat(CompositeOutputDevice):
|
||
"""
|
||
Extends :class:`CompositeOutputDevice` for the `ModMyPi JamHat`_ board.
|
||
|
||
There are 6 LEDs, two buttons and a tonal buzzer. The pins are fixed.
|
||
Usage::
|
||
|
||
from gpiozero import JamHat
|
||
|
||
hat = JamHat()
|
||
|
||
hat.button_1.wait_for_press()
|
||
hat.lights_1.on()
|
||
hat.buzzer.play('C4')
|
||
hat.button_2.wait_for_press()
|
||
hat.off()
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances to represent each
|
||
LED on the board. If :data:`False` (the default), construct regular
|
||
:class:`LED` instances.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _ModMyPi JamHat: https://thepihut.com/products/jam-hat
|
||
|
||
.. attribute:: lights_1, lights_2
|
||
|
||
Two :class:`LEDBoard` instances representing the top (lights_1) and
|
||
bottom (lights_2) rows of LEDs on the JamHat.
|
||
|
||
.. attribute:: red, yellow, green
|
||
|
||
:class:`LED` or :class:`PWMLED` instances representing the red,
|
||
yellow, and green LEDs along the top row.
|
||
|
||
.. attribute:: button_1, button_2
|
||
|
||
The left (button_1) and right (button_2) :class:`Button` objects on the
|
||
JamHat.
|
||
|
||
.. attribute:: buzzer
|
||
|
||
The :class:`TonalBuzzer` at the bottom right of the JamHat.
|
||
"""
|
||
def __init__(self, pwm=False, pin_factory=None):
|
||
super(JamHat, self).__init__(
|
||
lights_1=LEDBoard(
|
||
red='BOARD29', yellow='BOARD32', green='BOARD36',
|
||
pwm=pwm, _order=('red', 'yellow', 'green'),
|
||
pin_factory=pin_factory
|
||
),
|
||
lights_2=LEDBoard(
|
||
red='BOARD31', yellow='BOARD33', green='BOARD11',
|
||
pwm=pwm, _order=('red', 'yellow', 'green'),
|
||
pin_factory=pin_factory),
|
||
button_1=Button('BOARD35', pull_up=False, pin_factory=pin_factory),
|
||
button_2=Button('BOARD12', pull_up=False, pin_factory=pin_factory),
|
||
buzzer=TonalBuzzer('BOARD38', pin_factory=pin_factory),
|
||
_order=('lights_1', 'lights_2', 'button_1', 'button_2', 'buzzer'),
|
||
pin_factory=pin_factory
|
||
)
|
||
|
||
def on(self):
|
||
"""
|
||
Turns all the LEDs on and makes the buzzer play its mid tone.
|
||
"""
|
||
self.buzzer.value = 0
|
||
super(JamHat, self).on()
|
||
|
||
def off(self):
|
||
"""
|
||
Turns all the LEDs off and stops the buzzer.
|
||
"""
|
||
self.buzzer.value = None
|
||
super(JamHat, self).off()
|
||
|
||
|
||
class Pibrella(CompositeOutputDevice):
|
||
"""
|
||
Extends :class:`CompositeOutputDevice` for the Cyntech/Pimoroni `Pibrella`_
|
||
board.
|
||
|
||
The Pibrella board comprises 3 LEDs, a button, a tonal buzzer, four general
|
||
purpose input channels, and four general purpose output channels (with LEDs).
|
||
|
||
This class exposes the LEDs, button and buzzer.
|
||
|
||
Usage::
|
||
|
||
from gpiozero import Pibrella
|
||
|
||
pb = Pibrella()
|
||
|
||
pb.button.wait_for_press()
|
||
pb.lights.on()
|
||
pb.buzzer.play('A4')
|
||
pb.off()
|
||
|
||
The four input and output channels are exposed so you can create GPIO Zero
|
||
devices using these pins without looking up their respective pin numbers::
|
||
|
||
from gpiozero import Pibrella, LED, Button
|
||
|
||
pb = Pibrella()
|
||
btn = Button(pb.inputs.a, pull_up=False)
|
||
led = LED(pb.outputs.e)
|
||
|
||
btn.when_pressed = led.on
|
||
|
||
:param bool pwm:
|
||
If :data:`True`, construct :class:`PWMLED` instances to represent each
|
||
LED on the board, otherwise if :data:`False` (the default), construct
|
||
regular :class:`LED` instances.
|
||
|
||
:type pin_factory: Factory or None
|
||
:param pin_factory:
|
||
See :doc:`api_pins` for more information (this is an advanced feature
|
||
which most users can ignore).
|
||
|
||
.. _Pibrella: http://www.pibrella.com/
|
||
|
||
.. attribute:: lights
|
||
|
||
:class:`TrafficLights` instance representing the three LEDs
|
||
|
||
.. attribute:: red, amber, green
|
||
|
||
:class:`LED` or :class:`PWMLED` instances representing the red,
|
||
amber, and green LEDs
|
||
|
||
.. attribute:: button
|
||
|
||
The red :class:`Button` object on the Pibrella
|
||
|
||
.. attribute:: buzzer
|
||
|
||
A :class:`TonalBuzzer` object representing the buzzer
|
||
|
||
.. attribute:: inputs
|
||
|
||
A :func:`~collections.namedtuple` of the input pin numbers
|
||
|
||
.. attribute:: a, b, c, d
|
||
|
||
.. attribute:: outputs
|
||
|
||
A :func:`~collections.namedtuple` of the output pin numbers
|
||
|
||
.. attribute:: e, f, g, h
|
||
"""
|
||
def __init__(self, pwm=False, pin_factory=None):
|
||
super(Pibrella, self).__init__(
|
||
lights=TrafficLights(
|
||
red='BOARD13', amber='BOARD11', green='BOARD7',
|
||
pwm=pwm, pin_factory=pin_factory
|
||
),
|
||
button=Button('BOARD23', pull_up=False, pin_factory=pin_factory),
|
||
buzzer=TonalBuzzer('BOARD12', pin_factory=pin_factory),
|
||
_order=('lights', 'button', 'buzzer'),
|
||
pin_factory=pin_factory
|
||
)
|
||
InputPins = namedtuple('InputPins', ['a', 'b', 'c', 'd'])
|
||
OutputPins = namedtuple('OutputPins', ['e', 'f', 'g', 'h'])
|
||
self.inputs = InputPins(
|
||
a='BOARD21', b='BOARD26', c='BOARD24', d='BOARD19'
|
||
)
|
||
self.outputs = OutputPins(
|
||
e='BOARD15', f='BOARD16', g='BOARD18', h='BOARD22'
|
||
)
|
||
|
||
def on(self):
|
||
"""
|
||
Turns all the LEDs on and makes the buzzer play its mid tone.
|
||
"""
|
||
self.buzzer.value = 0
|
||
super(Pibrella, self).on()
|
||
|
||
def off(self):
|
||
"""
|
||
Turns all the LEDs off and stops the buzzer.
|
||
"""
|
||
self.buzzer.value = None
|
||
super(Pibrella, self).off()
|