This finishes off implementing values and source for all (current)
classes in gpiozero. I'm afraid things get rather complex in this
commit. For starters, we've now got quite a few "aggregate" classes
which necessarily don't descend from GPIODevice. To implement values and
source on these I could either repeat a helluva lot of code or ... turn
to mixin classes. Yeah, it's multiple inheritance time, baby!

Unfortunately multiple inheritance doesn't work with __slots__ but we
really ought to keep functionality that they provide us (raise
AttributeError when an unknown attribute is set). So I've implemented
this with ... erm ... metaclasses. Sorry!
This commit is contained in:
Dave Jones
2015-10-19 13:36:31 +01:00
parent b0b54162cc
commit fa0a1b3cdd
5 changed files with 403 additions and 239 deletions

View File

@@ -1,6 +1,12 @@
from __future__ import absolute_import
from __future__ import (
unicode_literals,
print_function,
absolute_import,
division,
)
from .devices import (
GPIODeviceClosed,
GPIODeviceError,
GPIODevice,
)
@@ -16,6 +22,7 @@ from .input_devices import (
MCP3004,
)
from .output_devices import (
OutputDeviceError,
OutputDevice,
PWMOutputDevice,
PWMLED,

View File

@@ -1,19 +1,48 @@
from .input_devices import Button
from .output_devices import LED, Buzzer, Motor
from .devices import GPIODeviceError
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 collections import namedtuple
from .input_devices import InputDeviceError, Button
from .output_devices import OutputDeviceError, LED, Buzzer, Motor
from .devices import CompositeDevice, SourceMixin
class LEDBoard(object):
class LEDBoard(SourceMixin, CompositeDevice):
"""
A Generic LED Board or collection of LEDs.
"""
def __init__(self, leds):
self._leds = tuple(LED(led) for led in leds)
def __init__(self, *pins):
super(LEDBoard, self).__init__()
self._leds = tuple(LED(pin) for pin in pins)
@property
def value(self):
"""
A tuple containing a boolean value for each LED on the board. This
property can also be set to update the state of all LEDs on the board.
"""
return tuple(led.value for led in self._leds)
@value.setter
def value(self, value):
for l, v in zip(self._leds, value):
l.value = v
@property
def leds(self):
"""
A tuple of all the `LED` objects contained by the instance.
"""
return self._leds
def on(self):
@@ -66,10 +95,11 @@ class PiLiter(LEDBoard):
Ciseco Pi-LITEr: strip of 8 very bright LEDs.
"""
def __init__(self):
leds = (4, 17, 27, 18, 22, 23, 24, 25)
super(PiLiter, self).__init__(leds)
super(PiLiter, self).__init__(4, 17, 27, 18, 22, 23, 24, 25)
TrafficLightTuple = namedtuple('TrafficLightTuple', ('red', 'amber', 'green'))
class TrafficLights(LEDBoard):
"""
Generic Traffic Lights set.
@@ -85,12 +115,37 @@ class TrafficLights(LEDBoard):
"""
def __init__(self, red=None, amber=None, green=None):
if not all([red, amber, green]):
raise GPIODeviceError('Red, Amber and Green pins must be provided')
raise OutputDeviceError('red, amber and green pins must be provided')
super(TrafficLights, self).__init__(red, amber, green)
self.red = LED(red)
self.amber = LED(amber)
self.green = LED(green)
self._leds = (self.red, self.amber, self.green)
@property
def value(self):
return TrafficLightTuple(*super(TrafficLights, self).value)
@value.setter
def value(self, value):
super(TrafficLights, self).value = value
@property
def red(self):
"""
The `LED` object representing the red LED.
"""
return self.leds[0]
@property
def amber(self):
"""
The `LED` object representing the red LED.
"""
return self.leds[1]
@property
def green(self):
"""
The `LED` object representing the green LED.
"""
return self.leds[2]
class PiTraffic(TrafficLights):
@@ -99,25 +154,47 @@ class PiTraffic(TrafficLights):
and 11.
"""
def __init__(self):
red, amber, green = (9, 10, 11)
super(PiTraffic, self).__init__(red, amber, green)
super(PiTraffic, self).__init__(9, 10, 11)
FishDishTuple = namedtuple('FishDishTuple', ('red', 'amber', 'green', 'buzzer'))
class FishDish(TrafficLights):
"""
Pi Supply FishDish: traffic light LEDs, a button and a buzzer.
"""
def __init__(self):
red, amber, green = (9, 22, 4)
super(FishDish, self).__init__(red, amber, green)
super(FishDish, self).__init__(9, 22, 4)
self.buzzer = Buzzer(8)
self.button = Button(pin=7, pull_up=False)
self._all = self._leds + (self.buzzer,)
self.button = Button(7, pull_up=False)
self._all = self.leds + (self.buzzer,)
@property
def all(self):
"""
A tuple containing objects for all the items on the board (several
`LED` objects, a `Buzzer`, and a `Button`).
"""
return self._all
@property
def value(self):
"""
Returns a named-tuple containing values representing the states of
the LEDs, and the buzzer. This property can also be set to a 4-tuple
to update the state of all the board's components.
"""
return FishDishTuple(
self.red.value,
self.amber.value,
self.green.value,
self.buzzer.value)
@value.setter
def value(self, value):
for i, v in zip(self._all, value):
i.value = v
def on(self):
"""
Turn all the board's components on.
@@ -208,97 +285,79 @@ class TrafficHat(FishDish):
Ryanteck Traffic HAT: traffic light LEDs, a button and a buzzer.
"""
def __init__(self):
green, amber, red = (22, 23, 24)
super(FishDish, self).__init__(red, amber, green)
super(FishDish, self).__init__(22, 23, 24)
self.buzzer = Buzzer(5)
self.button = Button(25)
self._all = self._leds + (self.buzzer,)
class Robot(object):
RobotTuple = namedtuple('RobotTuple', ('left', 'right'))
class Robot(SourceMixin, CompositeDevice):
"""
Generic dual-motor Robot.
"""
def __init__(self, left=None, right=None):
if not all([left, right]):
raise GPIODeviceError('left and right motor pins must be provided')
raise OutputDeviceError('left and right motor pins must be provided')
super(Robot, self).__init__()
self._left = Motor(*left)
self._right = Motor(*right)
left_forward, left_back = left
right_forward, right_back = right
@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 RobotTuple(self._left.value, self._right.value)
self._left = Motor(forward=left_forward, back=left_back)
self._right = Motor(forward=right_forward, back=right_back)
self._min_pwm = self._left._min_pwm
self._max_pwm = self._left._max_pwm
@value.setter
def value(self, value):
self._left.value, self._right.value = value
def forward(self, speed=1):
"""
Drive the robot forward.
Drive the robot forward by running both motors forward.
speed: `1`
Speed at which to drive the motors, 0 to 1.
"""
self._left._backward.off()
self._right._backward.off()
self._left._forward.on()
self._right._forward.on()
if speed < 1:
sleep(0.1) # warm up the motors
self._left._forward.value = speed
self._right._forward.value = speed
self._left.forward(speed)
self._right.forward(speed)
def backward(self, speed=1):
"""
Drive the robot backward.
Drive the robot backward by running both motors backward.
speed: `1`
Speed at which to drive the motors, 0 to 1.
"""
self._left._forward.off()
self._right._forward.off()
self._left._backward.on()
self._right._backward.on()
if speed < 1:
sleep(0.1) # warm up the motors
self._left._backward.value = speed
self._right._backward.value = speed
self._left.backward(speed)
self._right.backward(speed)
def left(self, speed=1):
"""
Make the robot turn left.
Make the robot turn left by running the right motor forward and left
motor backward.
speed: `1`
Speed at which to drive the motors, 0 to 1.
"""
self._right._backward.off()
self._left._forward.off()
self._right._forward.on()
self._left._backward.on()
if speed < 1:
sleep(0.1) # warm up the motors
self._right._forward.value = speed
self._left._backward.value = speed
self._right.forward(speed)
self._left.backward(speed)
def right(self, speed=1):
"""
Make the robot turn right.
Make the robot turn right by running the left motor forward and right
motor backward.
speed: `1`
Speed at which to drive the motors, 0 to 1.
"""
self._left._backward.off()
self._right._forward.off()
self._left._forward.on()
self._right._backward.on()
if speed < 1:
sleep(0.1) # warm up the motors
self._left._forward.value = speed
self._right._backward.value = speed
self._left.forward(speed)
self._right.backward(speed)
def stop(self):
"""
@@ -313,6 +372,4 @@ class RyanteckRobot(Robot):
RTK MCB Robot. Generic robot controller with pre-configured pin numbers.
"""
def __init__(self):
left = (17, 18)
right = (22, 23)
super(RyanteckRobot, self).__init__(left=left, right=right)
super(RyanteckRobot, self).__init__((17, 18), (22, 23))

View File

@@ -1,3 +1,10 @@
from __future__ import (
unicode_literals,
print_function,
absolute_import,
division,
)
import atexit
import weakref
from threading import Thread, Event, RLock
@@ -30,11 +37,123 @@ GPIO.setwarnings(False)
class GPIODeviceError(Exception):
pass
class GPIODeviceClosed(GPIODeviceError):
pass
class GPIODevice(object):
class GPIOFixedAttrs(type):
# NOTE Yes, this is a metaclass. Don't be scared - it's a simple one.
def __call__(cls, *args, **kwargs):
# Construct the class as normal and ensure it's a subclass of GPIOBase
# (defined below with a custom __setattrs__)
result = super(GPIOFixedAttrs, cls).__call__(*args, **kwargs)
assert isinstance(result, GPIOBase)
# At this point __new__ and __init__ have all been run. We now fix the
# set of attributes on the class by dir'ing the instance and creating a
# frozenset of the result called __attrs__ (which is queried by
# GPIOBase.__setattr__)
result.__attrs__ = frozenset(dir(result))
return result
class GPIOBase(object):
__metaclass__ = GPIOFixedAttrs
def __setattr__(self, name, value):
# This overridden __setattr__ simply ensures that additional attributes
# cannot be set on the class after construction (it manages this in
# conjunction with the meta-class above). Traditionally, this is
# managed with __slots__; however, this doesn't work with Python's
# multiple inheritance system which we need to use in order to avoid
# repeating the "source" and "values" property code in myriad places
if hasattr(self, '__attrs__') and name not in self.__attrs__:
raise AttributeError(
"'%s' object has no attribute '%s'" % (
self.__class__.__name__, name))
return super(GPIOBase, self).__setattr__(name, value)
def __del__(self):
self.close()
def close(self):
# This is a placeholder which is simply here to ensure close() can be
# safely called from subclasses without worrying whether super-class'
# have it (which in turn is useful in conjunction with the SourceMixin
# class).
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_tb):
self.close()
class ValuesMixin(object):
# NOTE Use this mixin *first* in the parent list
@property
def values(self):
"""
An infinite iterator of values read from `value`.
"""
while True:
try:
yield self.value
except GPIODeviceClosed:
break
class SourceMixin(object):
# NOTE Use this mixin *first* in the parent list
def __init__(self, *args, **kwargs):
self._source = None
self._source_thread = None
super(SourceMixin, self).__init__(*args, **kwargs)
def close(self):
try:
super(SourceMixin, self).close()
except AttributeError:
pass
self.source = None
def _copy_values(self, source):
for v in source:
self.value = v
if self._source_thread.stopping.wait(0):
break
@property
def source(self):
"""
The iterable to use as a source of values for `value`.
"""
return self._source
@source.setter
def source(self, value):
if self._source_thread is not None:
self._source_thread.stop()
self._source_thread = None
self._source = value
if value is not None:
self._source_thread = GPIOThread(target=self._copy_values, args=(value,))
self._source_thread.start()
class CompositeDevice(ValuesMixin, GPIOBase):
"""
Represents a device composed of multiple GPIO devices like simple HATs,
H-bridge motor controllers, robots composed of multiple motors, etc.
"""
pass
class GPIODevice(ValuesMixin, GPIOBase):
"""
Represents a generic GPIO device.
@@ -47,9 +166,6 @@ class GPIODevice(object):
The GPIO pin (in BCM numbering) that the device is connected to. If
this is `None` a `GPIODeviceError` will be raised.
"""
__slots__ = ('_pin', '_active_state', '_inactive_state')
def __init__(self, pin=None):
super(GPIODevice, self).__init__()
# self._pin must be set before any possible exceptions can be raised
@@ -68,9 +184,6 @@ class GPIODevice(object):
self._active_state = GPIO.HIGH
self._inactive_state = GPIO.LOW
def __del__(self):
self.close()
def _read(self):
try:
return GPIO.input(self.pin) == self._active_state
@@ -132,6 +245,7 @@ class GPIODevice(object):
... led.on()
...
"""
super(GPIODevice, self).close()
with _GPIO_PINS_LOCK:
pin = self._pin
self._pin = None
@@ -140,12 +254,6 @@ class GPIODevice(object):
GPIO.remove_event_detect(pin)
GPIO.cleanup(pin)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_tb):
self.close()
@property
def pin(self):
"""
@@ -163,17 +271,6 @@ class GPIODevice(object):
is_active = value
@property
def values(self):
"""
An infinite iterator of values read from `value`.
"""
while True:
try:
yield self.value
except GPIODeviceClosed:
break
def __repr__(self):
try:
return "<gpiozero.%s object on pin=%d, is_active=%s>" % (
@@ -238,3 +335,4 @@ class GPIOQueue(GPIOThread):
except ReferenceError:
# Parent is dead; time to die!
pass

View File

@@ -1,4 +1,9 @@
from __future__ import division
from __future__ import (
unicode_literals,
print_function,
absolute_import,
division,
)
import inspect
import warnings
@@ -10,7 +15,13 @@ from RPi import GPIO
from w1thermsensor import W1ThermSensor
from spidev import SpiDev
from .devices import GPIODeviceError, GPIODeviceClosed, GPIODevice, GPIOQueue
from .devices import (
GPIODeviceError,
GPIODeviceClosed,
GPIODevice,
CompositeDevice,
GPIOQueue,
)
class InputDeviceError(GPIODeviceError):
@@ -35,13 +46,6 @@ class InputDevice(GPIODevice):
If `True`, the pin will be pulled high with an internal resistor. If
`False` (the default), the pin will be pulled low.
"""
__slots__ = (
'_pull_up',
'_active_edge',
'_inactive_edge',
)
def __init__(self, pin=None, pull_up=False):
if pin in (2, 3) and not pull_up:
raise InputDeviceError(
@@ -79,6 +83,10 @@ class InputDevice(GPIODevice):
@property
def pull_up(self):
"""
If `True`, the device uses a pull-up resistor to set the GPIO pin
"high" by default. Defaults to `False`.
"""
return self._pull_up
def __repr__(self):
@@ -102,15 +110,6 @@ class WaitableInputDevice(InputDevice):
Note that this class provides no means of actually firing its events; it's
effectively an abstract base class.
"""
__slots__ = (
'_active_event',
'_inactive_event',
'_when_activated',
'_when_deactivated',
'_last_state',
)
def __init__(self, pin=None, pull_up=False):
super(WaitableInputDevice, self).__init__(pin, pull_up)
self._active_event = Event()
@@ -121,7 +120,7 @@ class WaitableInputDevice(InputDevice):
def wait_for_active(self, timeout=None):
"""
Halt the program until the device is activated, or the timeout is
Pause the script until the device is activated, or the timeout is
reached.
timeout: `None`
@@ -132,7 +131,7 @@ class WaitableInputDevice(InputDevice):
def wait_for_inactive(self, timeout=None):
"""
Halt the program until the device is deactivated, or the timeout is
Pause the script until the device is deactivated, or the timeout is
reached.
timeout: `None`
@@ -248,9 +247,6 @@ class DigitalInputDevice(WaitableInputDevice):
ignore changes in state after an initial change. This defaults to
`None` which indicates that no bounce compensation will be performed.
"""
__slots__ = ()
def __init__(self, pin=None, pull_up=False, bounce_time=None):
super(DigitalInputDevice, self).__init__(pin, pull_up)
try:
@@ -301,9 +297,6 @@ class SmoothedInputDevice(WaitableInputDevice):
If `True`, a value will be returned immediately, but be aware that this
value is likely to fluctuate excessively.
"""
__slots__ = ('_queue', '_threshold', '__weakref__')
def __init__(
self, pin=None, pull_up=False, threshold=0.5,
queue_len=5, sample_wait=0.0, partial=False):
@@ -388,6 +381,9 @@ class SmoothedInputDevice(WaitableInputDevice):
@property
def is_active(self):
"""
Returns `True` if the device is currently active and `False` otherwise.
"""
return self.value > self.threshold
@@ -399,9 +395,6 @@ class Button(DigitalInputDevice):
side of the switch, and ground to the other (the default `pull_up` value
is `True`).
"""
__slots__ = ()
def __init__(self, pin=None, pull_up=True, bouncetime=None):
super(Button, self).__init__(pin, pull_up, bouncetime)
@@ -426,9 +419,6 @@ class MotionSensor(SmoothedInputDevice):
particularly "jittery" you may wish to set this to a higher value (e.g. 5)
to mitigate this.
"""
__slots__ = ()
def __init__(
self, pin=None, queue_len=1, sample_rate=10, threshold=0.5,
partial=False):
@@ -458,12 +448,6 @@ class LightSensor(SmoothedInputDevice):
class repeatedly discharges the capacitor, then times the duration it takes
to charge (which will vary according to the light falling on the LDR).
"""
__slots__ = (
'_charge_time_limit',
'_charged',
)
def __init__(
self, pin=None, queue_len=5, charge_time_limit=0.01,
threshold=0.1, partial=False):
@@ -517,17 +501,11 @@ class TemperatureSensor(W1ThermSensor):
return self.get_temperature()
class AnalogInputDevice(object):
class AnalogInputDevice(CompositeDevice):
"""
Represents an analog input device connected to SPI (serial interface).
"""
__slots__ = (
'_device',
'_bits',
'_spi',
)
def __init__(self, device=0, bits=None):
if bits is None:
raise InputDeviceError('you must specify the bit resolution of the device')
@@ -537,25 +515,32 @@ class AnalogInputDevice(object):
self._bits = bits
self._spi = SpiDev()
self._spi.open(0, self.device)
super(AnalogInputDevice, self).__init__()
def close(self):
"""
Shut down the device and release all associated resources.
"""
if self._spi:
s = self._spi
self._spi = None
s.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_tb):
self.close()
super(AnalogInputDevice, self).close()
@property
def bus(self):
"""
The SPI bus that the device is connected to. As the Pi only has a
single (user accessible) SPI bus, this always returns 0.
"""
return 0
@property
def device(self):
"""
The select pin that the device is connected to. The Pi has two select
pins so this will be 0 or 1.
"""
return self._device
def _read(self):
@@ -563,13 +548,15 @@ class AnalogInputDevice(object):
@property
def value(self):
"""
A value read from the device. This will be a floating point value
between 0 and 1 (scaled according to the number of bits supported by
the device).
"""
return self._read() / (2**self._bits - 1)
class MCP3008(AnalogInputDevice):
__slots__ = ('_channel')
def __init__(self, device=0, channel=0):
if not 0 <= channel < 8:
raise InputDeviceError('channel must be between 0 and 7')
@@ -578,6 +565,10 @@ class MCP3008(AnalogInputDevice):
@property
def channel(self):
"""
The channel to read data from. The MCP3008 has 8 channels (so this will
be between 0 and 7) while the MCP3004 has 4 channels (range 0 to 3).
"""
return self._channel
def _read(self):
@@ -602,11 +593,10 @@ class MCP3008(AnalogInputDevice):
class MCP3004(MCP3008):
__slots__ = ()
def __init__(self, device=0, channel=0):
# MCP3004 protocol is identical to MCP3008 but the top bit of the
# channel number must be 0 (effectively restricting it to 4 channels)
if not 0 <= channel < 4:
raise InputDeviceError('channel must be between 0 and 3')
super(MCP3004, self).__init__(device, channel)

View File

@@ -1,3 +1,10 @@
from __future__ import (
unicode_literals,
print_function,
absolute_import,
division,
)
import warnings
from time import sleep
from threading import Lock
@@ -5,14 +12,21 @@ from itertools import repeat
from RPi import GPIO
from .devices import GPIODeviceError, GPIODevice, GPIOThread
from .devices import (
GPIODeviceError,
GPIODeviceClosed,
GPIODevice,
GPIOThread,
CompositeDevice,
SourceMixin,
)
class OutputDeviceError(GPIODeviceError):
pass
class OutputDevice(GPIODevice):
class OutputDevice(SourceMixin, GPIODevice):
"""
Represents a generic GPIO output device.
@@ -25,13 +39,8 @@ class OutputDevice(GPIODevice):
`False`, the `on` method will set the GPIO to LOW (the `off` method
always does the opposite).
"""
__slots__ = ('_active_high', '_source', '_source_thread')
def __init__(self, pin=None, active_high=True):
self._active_high = active_high
self._source = None
self._source_thread = None
super(OutputDevice, self).__init__(pin)
self._active_state = GPIO.HIGH if active_high else GPIO.LOW
self._inactive_state = GPIO.LOW if active_high else GPIO.HIGH
@@ -52,10 +61,6 @@ class OutputDevice(GPIODevice):
self.close()
raise
def close(self):
self.source = None
super(OutputDevice, self).close()
def _write(self, value):
GPIO.output(self.pin, bool(value))
@@ -79,26 +84,6 @@ class OutputDevice(GPIODevice):
def value(self, value):
self._write(value)
def _copy_values(self, source):
for v in source:
self.value = v
if self._source_thread.stopping.wait(0):
break
@property
def source(self):
return self._source
@source.setter
def source(self, value):
if self._source_thread is not None:
self._source_thread.stop()
self._source_thread = None
self._source = value
if value is not None:
self._source_thread = GPIOThread(target=self._copy_values, args=(value,))
self._source_thread.start()
@property
def active_high(self):
return self._active_high
@@ -120,12 +105,9 @@ class DigitalOutputDevice(OutputDevice):
optional background thread to handle toggling the device state without
further interaction.
"""
__slots__ = ('_blink_thread', '_lock')
def __init__(self, pin=None, active_high=True):
super(DigitalOutputDevice, self).__init__(pin, active_high)
self._blink_thread = None
super(DigitalOutputDevice, self).__init__(pin, active_high)
self._lock = Lock()
def close(self):
@@ -148,8 +130,8 @@ class DigitalOutputDevice(OutputDevice):
def toggle(self):
"""
Reverse the state of the device.
If it's on, turn it off; if it's off, turn it on.
Reverse the state of the device. If it's on, turn it off; if it's off,
turn it on.
"""
with self._lock:
if self.is_active:
@@ -161,20 +143,20 @@ class DigitalOutputDevice(OutputDevice):
"""
Make the device turn on and off repeatedly.
on_time: 1
on_time: `1`
Number of seconds on
off_time: 1
off_time: `1`
Number of seconds off
n: None
Number of times to blink; None means forever
n: `None`
Number of times to blink; `None` means forever
background: True
If True, start a background thread to continue blinking and return
immediately. If False, only return when the blink is finished
(warning: the default value of n will result in this method never
returning).
background: `True`
If `True`, start a background thread to continue blinking and
return immediately. If `False`, only return when the blink is
finished (warning: the default value of n will result in this
method never returning).
"""
self._stop_blink()
self._blink_thread = GPIOThread(
@@ -209,7 +191,7 @@ class LED(DigitalOutputDevice):
anode (long leg) of the LED, and the cathode (short leg) to ground, with
an optional resistor to prevent the LED from burning out.
"""
__slots__ = ()
pass
LED.is_lit = LED.is_active
@@ -221,16 +203,13 @@ class Buzzer(DigitalOutputDevice):
A typical configuration of such a device is to connect a GPIO pin to the
anode (long leg) of the buzzer, and the cathode (short leg) to ground.
"""
__slots__ = ()
pass
class PWMOutputDevice(DigitalOutputDevice):
"""
Generic Output device configured for PWM (Pulse-Width Modulation).
"""
__slots__ = ('_pwm', '_frequency', '_value')
def __init__(self, pin=None, frequency=100):
self._pwm = None
super(PWMOutputDevice, self).__init__(pin)
@@ -278,6 +257,9 @@ class PWMOutputDevice(DigitalOutputDevice):
@property
def is_active(self):
"""
Returns `True` if the device is currently active and `False` otherwise.
"""
return self.value > 0.0
@property
@@ -295,8 +277,17 @@ class PWMOutputDevice(DigitalOutputDevice):
class PWMLED(PWMOutputDevice):
"""
An LED (Light Emmitting Diode) component with variable brightness.
A typical configuration of such a device is to connect a GPIO pin to the
anode (long leg) of the LED, and the cathode (short leg) to ground, with
an optional resistor to prevent the LED from burning out.
"""
pass
PWMLED.is_lit = PWMLED.is_active
def _led_property(index, doc=None):
return property(
@@ -306,7 +297,7 @@ def _led_property(index, doc=None):
)
class RGBLED(object):
class RGBLED(SourceMixin, CompositeDevice):
"""
Single LED with individually controllable red, green and blue components.
@@ -319,10 +310,10 @@ class RGBLED(object):
blue: `None`
The GPIO pin that controls the blue component of the RGB LED.
"""
__slots__ = ('_leds')
def __init__(self, red=None, green=None, blue=None):
if not all([red, green, blue]):
raise OutputDeviceError('red, green, and blue pins must be provided')
super(RGBLED, self).__init__()
self._leds = tuple(PWMOutputDevice(pin) for pin in (red, green, blue))
red = _led_property(0)
@@ -330,78 +321,98 @@ class RGBLED(object):
blue = _led_property(2)
@property
def color(self):
def value(self):
"""
Set the color of the LED from an RGB 3-tuple of `(red, green, blue)`
where each value between 0 and 1.
Represents the color of the LED as an RGB 3-tuple of `(red, green,
blue)` where each value is between 0 and 1.
For example, purple would be `(1, 0, 1)` and yellow would be `(1, 1,
0)`, while orange would be `(1, 0.5, 0)`.
"""
return (self.red, self.green, self.blue)
@color.setter
def color(self, value):
@value.setter
def value(self, value):
self.red, self.green, self.blue = value
@property
def is_active(self):
"""
Returns `True` if the LED is currently active and `False` otherwise.
"""
return self.value != (0, 0, 0)
color = value
def on(self):
"""
Turn the device on. This equivalent to setting the device color to
white `(1, 1, 1)`.
"""
self.color = (1, 1, 1)
self.value = (1, 1, 1)
def off(self):
"""
Turn the device off. This is equivalent to setting the device color
to black `(0, 0, 0)`.
"""
self.color = (0, 0, 0)
self.value = (0, 0, 0)
def close(self):
for led in self._leds:
led.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_tb):
self.close()
class Motor(object):
class Motor(SourceMixin, CompositeDevice):
"""
Generic bi-directional motor.
"""
__slots__ = ('_forward', '_backward')
def __init__(self, forward=None, back=None):
def __init__(self, forward=None, backward=None):
if not all([forward, back]):
raise GPIODeviceError('forward and back pins must be provided')
raise OutputDeviceError('forward and back pins must be provided')
super(Motor, self).__init__()
self._forward = PWMOutputDevice(forward)
self._backward = PWMOutputDevice(back)
self._backward = PWMOutputDevice(backward)
@property
def value(self):
"""
Represents the speed of the motor as a floating point value between -1
(full speed backward) and 1 (full speed forward).
"""
return self._forward.value - self._backward.value
@value.setter
def value(self, value):
if not -1 <= value <= 1:
raise OutputDeviceError("Motor value must be between -1 and 1")
if value > 0:
self.forward(value)
elif value < 0:
self.backward(-value)
else:
self.stop()
@property
def is_active(self):
"""
Returns `True` if the motor is currently active and `False` otherwise.
"""
return self.value != 0
def forward(self, speed=1):
"""
Drive the motor forwards
"""
self._backward.off()
self._forward.on()
if speed < 1:
sleep(0.1) # warm up the motor
self._forward.value = speed
self._forward.value = speed
def backward(self, speed=1):
"""
Drive the motor backwards
"""
self._forward.off()
self._backward.on()
if speed < 1:
sleep(0.1) # warm up the motor
self._backward.value = speed
self._backward.value = speed
def stop(self):
"""
@@ -409,3 +420,4 @@ class Motor(object):
"""
self._forward.off()
self._backward.off()