From fa0a1b3cddd51fa2a557a5d699400c39e74c76d5 Mon Sep 17 00:00:00 2001 From: Dave Jones Date: Mon, 19 Oct 2015 13:36:31 +0100 Subject: [PATCH] Fix #76, fix #79 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! --- gpiozero/__init__.py | 9 +- gpiozero/boards.py | 203 ++++++++++++++++++++++++------------- gpiozero/devices.py | 146 +++++++++++++++++++++----- gpiozero/input_devices.py | 104 +++++++++---------- gpiozero/output_devices.py | 180 +++++++++++++++++--------------- 5 files changed, 403 insertions(+), 239 deletions(-) diff --git a/gpiozero/__init__.py b/gpiozero/__init__.py index 47636f0..23aa203 100644 --- a/gpiozero/__init__.py +++ b/gpiozero/__init__.py @@ -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, diff --git a/gpiozero/boards.py b/gpiozero/boards.py index 867d381..6930ab6 100644 --- a/gpiozero/boards.py +++ b/gpiozero/boards.py @@ -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)) diff --git a/gpiozero/devices.py b/gpiozero/devices.py index 260760c..75b759a 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -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 "" % ( @@ -238,3 +335,4 @@ class GPIOQueue(GPIOThread): except ReferenceError: # Parent is dead; time to die! pass + diff --git a/gpiozero/input_devices.py b/gpiozero/input_devices.py index 6b37f02..8153006 100644 --- a/gpiozero/input_devices.py +++ b/gpiozero/input_devices.py @@ -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) + diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index 474726f..f5ca5ea 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -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() +