mirror of
https://github.com/KevinMidboe/python-gpiozero.git
synced 2025-10-29 17:50:37 +00:00
This PR adds a software SPI implementation. Firstly this removes the absolute necessity for spidev (#140), which also means when it's not present things still work (effectively fixes #185), and also enables any four pins to be used for SPI devices (which don't require the hardware implementation). The software implementation is simplistic but still supports clock polarity and phase, select-high, and variable bits per word. However it doesn't allow precise speeds to be implemented because it just wibbles the clock as fast as it can (which being pure Python isn't actually that fast). Finally, because this PR involves creating a framework for "shared" devices (like SPI devices with multiple channels), it made sense to bung Energenie (#69) in as wells as this is a really simple shared device.
This commit is contained in:
@@ -10,7 +10,13 @@ from .pins import (
|
||||
)
|
||||
from .exc import (
|
||||
GPIOZeroError,
|
||||
DeviceClosed,
|
||||
CompositeDeviceError,
|
||||
CompositeDeviceBadName,
|
||||
SPIError,
|
||||
SPIBadArgs,
|
||||
EnergenieSocketMissing,
|
||||
EnergenieBadSocket,
|
||||
GPIODeviceError,
|
||||
GPIODeviceClosed,
|
||||
GPIOPinInUse,
|
||||
@@ -32,10 +38,15 @@ from .exc import (
|
||||
PinPWMError,
|
||||
PinPWMUnsupported,
|
||||
PinPWMFixedValue,
|
||||
GPIOZeroWarning,
|
||||
SPIWarning,
|
||||
SPISoftwareFallback,
|
||||
)
|
||||
from .devices import (
|
||||
Device,
|
||||
GPIODevice,
|
||||
CompositeDevice,
|
||||
SharedMixin,
|
||||
SourceMixin,
|
||||
ValuesMixin,
|
||||
)
|
||||
@@ -44,12 +55,14 @@ from .input_devices import (
|
||||
WaitableInputDevice,
|
||||
DigitalInputDevice,
|
||||
SmoothedInputDevice,
|
||||
AnalogInputDevice,
|
||||
Button,
|
||||
LineSensor,
|
||||
MotionSensor,
|
||||
LightSensor,
|
||||
DistanceSensor,
|
||||
)
|
||||
from .spi_devices import (
|
||||
SPIDevice,
|
||||
AnalogInputDevice,
|
||||
MCP3001,
|
||||
MCP3002,
|
||||
@@ -74,6 +87,8 @@ from .output_devices import (
|
||||
RGBLED,
|
||||
)
|
||||
from .boards import (
|
||||
CompositeOutputDevice,
|
||||
LEDCollection,
|
||||
LEDBoard,
|
||||
LEDBarGraph,
|
||||
PiLiter,
|
||||
@@ -86,4 +101,5 @@ from .boards import (
|
||||
Robot,
|
||||
RyanteckRobot,
|
||||
CamJamKitRobot,
|
||||
Energenie,
|
||||
)
|
||||
|
||||
@@ -12,36 +12,84 @@ except ImportError:
|
||||
from time import sleep
|
||||
from collections import namedtuple
|
||||
from itertools import repeat, cycle, chain
|
||||
from threading import Lock
|
||||
|
||||
from .exc import InputDeviceError, OutputDeviceError
|
||||
from .exc import (
|
||||
GPIOPinMissing,
|
||||
EnergenieSocketMissing,
|
||||
EnergenieBadSocket,
|
||||
)
|
||||
from .input_devices import Button
|
||||
from .output_devices import LED, PWMLED, Buzzer, Motor
|
||||
from .devices import GPIOThread, CompositeDevice, SourceMixin
|
||||
from .output_devices import OutputDevice, LED, PWMLED, Buzzer, Motor
|
||||
from .threads import GPIOThread
|
||||
from .devices import Device, CompositeDevice, SharedMixin, SourceMixin
|
||||
|
||||
|
||||
class LEDCollection(SourceMixin, CompositeDevice):
|
||||
class CompositeOutputDevice(SourceMixin, CompositeDevice):
|
||||
"""
|
||||
Abstract base class for :class:`LEDBoard` and :class:`LEDBarGraph`.
|
||||
Extends :class:`CompositeDevice` with :meth:`on`, :meth:`off`, and
|
||||
:meth:`toggle` methods for controlling subordinate output devices. Also
|
||||
extends :attr:`value` to be writeable.
|
||||
"""
|
||||
|
||||
def __init__(self, *pins, **kwargs):
|
||||
self._blink_thread = None
|
||||
super(LEDCollection, self).__init__()
|
||||
pwm = kwargs.get('pwm', False)
|
||||
active_high = kwargs.get('active_high', True)
|
||||
initial_value = kwargs.get('initial_value', False)
|
||||
LEDClass = PWMLED if pwm else LED
|
||||
self._leds = tuple(
|
||||
LEDClass(pin, active_high, initial_value) for pin in pins
|
||||
)
|
||||
def on(self):
|
||||
"""
|
||||
Turn all the output devices on.
|
||||
"""
|
||||
for device in self.all:
|
||||
if isinstance(device, OutputDevice):
|
||||
device.on()
|
||||
|
||||
def close(self):
|
||||
for led in self.leds:
|
||||
led.close()
|
||||
def off(self):
|
||||
"""
|
||||
Turn all the output devices off.
|
||||
"""
|
||||
for device in self.all:
|
||||
if isinstance(device, OutputDevice):
|
||||
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.all:
|
||||
if isinstance(device, OutputDevice):
|
||||
device.toggle()
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
return all(led.closed for led in self.leds)
|
||||
def value(self):
|
||||
"""
|
||||
A tuple containing a value for each subordinate device. This property
|
||||
can also be set to update the state of all output subordinate devices.
|
||||
"""
|
||||
return super(CompositeOutputDevice, self).value
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
for device, v in zip(self.all, value):
|
||||
if isinstance(device, OutputDevice):
|
||||
device.value = v
|
||||
# Simply ignore values for non-output devices
|
||||
|
||||
|
||||
class LEDCollection(CompositeOutputDevice):
|
||||
"""
|
||||
Extends :class:`CompositeOutputDevice`. Abstract base class for
|
||||
:class:`LEDBoard` and :class:`LEDBarGraph`.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._blink_thread = None
|
||||
pwm = kwargs.pop('pwm', False)
|
||||
active_high = kwargs.pop('active_high', True)
|
||||
initial_value = kwargs.pop('initial_value', False)
|
||||
order = kwargs.pop('_order', None)
|
||||
LEDClass = PWMLED if pwm else LED
|
||||
super(LEDCollection, self).__init__(
|
||||
*(LEDClass(pin, active_high, initial_value) for pin in args),
|
||||
_order=order,
|
||||
**{name: LEDClass(pin, active_high, initial_value) for name, pin in kwargs.items()})
|
||||
|
||||
@property
|
||||
def leds(self):
|
||||
@@ -49,35 +97,12 @@ class LEDCollection(SourceMixin, CompositeDevice):
|
||||
A tuple of all the :class:`LED` or :class:`PWMLED` objects contained by
|
||||
the instance.
|
||||
"""
|
||||
return self._leds
|
||||
|
||||
def on(self):
|
||||
"""
|
||||
Turn all the LEDs on.
|
||||
"""
|
||||
for led in self.leds:
|
||||
led.on()
|
||||
|
||||
def off(self):
|
||||
"""
|
||||
Turn all the LEDs off.
|
||||
"""
|
||||
for led in self.leds:
|
||||
led.off()
|
||||
|
||||
def toggle(self):
|
||||
"""
|
||||
Toggle all the LEDs. For each LED, if it's on, turn it off; if it's
|
||||
off, turn it on.
|
||||
"""
|
||||
for led in self.leds:
|
||||
led.toggle()
|
||||
|
||||
return self.all
|
||||
|
||||
|
||||
class LEDBoard(LEDCollection):
|
||||
"""
|
||||
Extends :class:`CompositeDevice` and represents a generic LED board or
|
||||
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
|
||||
@@ -96,26 +121,23 @@ class LEDBoard(LEDCollection):
|
||||
If ``True``, construct :class:`PWMLED` instances for each pin. If
|
||||
``False`` (the default), construct regular :class:`LED` instances. This
|
||||
parameter can only be specified as a keyword parameter.
|
||||
|
||||
:param bool active_high:
|
||||
If ``True`` (the default), the :meth:`on` method will set all the
|
||||
associates pins to HIGH. If ``False``, the :meth:`on` method will set
|
||||
all pins to LOW (the :meth:`off` method always does the opposite).
|
||||
|
||||
:param bool initial_value:
|
||||
If ``False`` (the default), all LEDs will be off initially. If
|
||||
``None``, each device will be left in whatever state the pin is found
|
||||
in when configured for output (warning: this can be on). The ``True``,
|
||||
the device will be switched on initially.
|
||||
"""
|
||||
|
||||
def close(self):
|
||||
self._stop_blink()
|
||||
super(LEDBoard, self).close()
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
A tuple containing a 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):
|
||||
self._stop_blink()
|
||||
for l, v in zip(self.leds, value):
|
||||
l.value = v
|
||||
|
||||
def on(self):
|
||||
self._stop_blink()
|
||||
super(LEDBoard, self).on()
|
||||
@@ -266,8 +288,12 @@ class LEDBarGraph(LEDCollection):
|
||||
|
||||
def __init__(self, *pins, **kwargs):
|
||||
super(LEDBarGraph, self).__init__(*pins, pwm=False)
|
||||
initial_value = kwargs.get('initial_value', 0)
|
||||
self.value = initial_value
|
||||
try:
|
||||
initial_value = kwargs.pop('initial_value', 0)
|
||||
self.value = initial_value
|
||||
except:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
@@ -336,6 +362,7 @@ class PiLiter(LEDBoard):
|
||||
|
||||
.. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/
|
||||
"""
|
||||
|
||||
def __init__(self, pwm=False):
|
||||
super(PiLiter, self).__init__(4, 17, 27, 18, 22, 23, 24, 25, pwm=pwm)
|
||||
|
||||
@@ -360,14 +387,12 @@ class PiLiterBarGraph(LEDBarGraph):
|
||||
|
||||
.. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/
|
||||
"""
|
||||
|
||||
def __init__(self, initial_value=0):
|
||||
super(PiLiterBarGraph, self).__init__(
|
||||
4, 17, 27, 18, 22, 23, 24, 25, initial_value=initial_value)
|
||||
|
||||
|
||||
TrafficLightTuple = namedtuple('TrafficLightTuple', ('red', 'amber', 'green'))
|
||||
|
||||
|
||||
class TrafficLights(LEDBoard):
|
||||
"""
|
||||
Extends :class:`LEDBoard` for devices containing red, amber, and green
|
||||
@@ -397,44 +422,12 @@ class TrafficLights(LEDBoard):
|
||||
"""
|
||||
def __init__(self, red=None, amber=None, green=None, pwm=False):
|
||||
if not all([red, amber, green]):
|
||||
raise OutputDeviceError(
|
||||
raise GPIOPinMissing(
|
||||
'red, amber and green pins must be provided'
|
||||
)
|
||||
super(TrafficLights, self).__init__(red, amber, green, pwm=pwm)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
A 3-tuple containing values for the red, amber, and green LEDs. This
|
||||
property can also be set to alter the state of the LEDs.
|
||||
"""
|
||||
return TrafficLightTuple(*super(TrafficLights, self).value)
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
# Eurgh, this is horrid but necessary (see #90)
|
||||
super(TrafficLights, self.__class__).value.fset(self, value)
|
||||
|
||||
@property
|
||||
def red(self):
|
||||
"""
|
||||
The :class:`LED` or :class:`PWMLED` object representing the red LED.
|
||||
"""
|
||||
return self.leds[0]
|
||||
|
||||
@property
|
||||
def amber(self):
|
||||
"""
|
||||
The :class:`LED` or :class:`PWMLED` object representing the red LED.
|
||||
"""
|
||||
return self.leds[1]
|
||||
|
||||
@property
|
||||
def green(self):
|
||||
"""
|
||||
The :class:`LED` or :class:`PWMLED` object representing the green LED.
|
||||
"""
|
||||
return self.leds[2]
|
||||
super(TrafficLights, self).__init__(
|
||||
red=red, amber=amber, green=green, pwm=pwm,
|
||||
_order=('red', 'amber', 'green'))
|
||||
|
||||
|
||||
class PiTraffic(TrafficLights):
|
||||
@@ -454,15 +447,12 @@ class PiTraffic(TrafficLights):
|
||||
To use the PI-TRAFFIC board when attached to a non-standard set of pins,
|
||||
simply use the parent class, :class:`TrafficLights`.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(PiTraffic, self).__init__(9, 10, 11)
|
||||
|
||||
|
||||
TrafficLightsBuzzerTuple = namedtuple('TrafficLightsBuzzerTuple', (
|
||||
'red', 'amber', 'green', 'buzzer'))
|
||||
|
||||
|
||||
class TrafficLightsBuzzer(SourceMixin, CompositeDevice):
|
||||
class TrafficLightsBuzzer(CompositeOutputDevice):
|
||||
"""
|
||||
Extends :class:`CompositeDevice` and is a generic class for HATs with
|
||||
traffic lights, a button and a buzzer.
|
||||
@@ -477,146 +467,11 @@ class TrafficLightsBuzzer(SourceMixin, CompositeDevice):
|
||||
:param Button button:
|
||||
An instance of :class:`Button` representing the button on the HAT.
|
||||
"""
|
||||
|
||||
def __init__(self, lights, buzzer, button):
|
||||
self._blink_thread = None
|
||||
super(TrafficLightsBuzzer, self).__init__()
|
||||
self.lights = lights
|
||||
self.buzzer = buzzer
|
||||
self.button = button
|
||||
self._all = self.lights.leds + (self.buzzer,)
|
||||
|
||||
def close(self):
|
||||
self.lights.close()
|
||||
self.buzzer.close()
|
||||
self.button.close()
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
return all(o.closed for o in self.all)
|
||||
|
||||
@property
|
||||
def all(self):
|
||||
"""
|
||||
A tuple containing objects for all the items on the board (several
|
||||
:class:`LED` objects, a :class:`Buzzer`, and a :class:`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 TrafficLightsBuzzerTuple(
|
||||
self.lights.red.value,
|
||||
self.lights.amber.value,
|
||||
self.lights.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.
|
||||
"""
|
||||
for thing in self.all:
|
||||
thing.on()
|
||||
|
||||
def off(self):
|
||||
"""
|
||||
Turn all the board's components off.
|
||||
"""
|
||||
for thing in self.all:
|
||||
thing.off()
|
||||
|
||||
def toggle(self):
|
||||
"""
|
||||
Toggle all the board's components. For each component, if it's on, turn
|
||||
it off; if it's off, turn it on.
|
||||
"""
|
||||
for thing in self.all:
|
||||
thing.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 ``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 ``False`` when the class was constructed
|
||||
(:exc:`ValueError` will be raised if not).
|
||||
|
||||
:param int n:
|
||||
Number of times to blink; ``None`` (the default) means forever.
|
||||
|
||||
:param bool background:
|
||||
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).
|
||||
"""
|
||||
if isinstance(self.lights.leds[0], 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(
|
||||
target=self._blink_device,
|
||||
args=(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):
|
||||
if self._blink_thread:
|
||||
self._blink_thread.stop()
|
||||
self._blink_thread = None
|
||||
|
||||
def _blink_device(self, on_time, off_time, fade_in_time, fade_out_time, n, fps=50):
|
||||
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))
|
||||
sequence = (
|
||||
cycle(sequence) if n is None else
|
||||
chain.from_iterable(repeat(sequence, n))
|
||||
)
|
||||
for value, delay in sequence:
|
||||
for thing in self._all:
|
||||
thing.value = value
|
||||
if self._blink_thread.stopping.wait(delay):
|
||||
break
|
||||
super(TrafficLightsBuzzer, self).__init__(
|
||||
lights=lights, buzzer=buzzer, button=button,
|
||||
_order=('lights', 'buzzer', 'button'))
|
||||
|
||||
|
||||
class FishDish(TrafficLightsBuzzer):
|
||||
@@ -639,6 +494,7 @@ class FishDish(TrafficLightsBuzzer):
|
||||
LED. If ``False`` (the default), construct regular :class:`LED`
|
||||
instances.
|
||||
"""
|
||||
|
||||
def __init__(self, pwm=False):
|
||||
super(FishDish, self).__init__(
|
||||
TrafficLights(9, 22, 4, pwm=pwm),
|
||||
@@ -667,6 +523,7 @@ class TrafficHat(TrafficLightsBuzzer):
|
||||
LED. If ``False`` (the default), construct regular :class:`LED`
|
||||
instances.
|
||||
"""
|
||||
|
||||
def __init__(self, pwm=False):
|
||||
super(TrafficHat, self).__init__(
|
||||
TrafficLights(24, 23, 22, pwm=pwm),
|
||||
@@ -701,9 +558,10 @@ class Robot(SourceMixin, CompositeDevice):
|
||||
A tuple of two GPIO pins representing the forward and backward inputs
|
||||
of the right motor's controller.
|
||||
"""
|
||||
|
||||
def __init__(self, left=None, right=None):
|
||||
if not all([left, right]):
|
||||
raise OutputDeviceError(
|
||||
raise GPIOPinMissing(
|
||||
'left and right motor pins must be provided'
|
||||
)
|
||||
super(Robot, self).__init__()
|
||||
@@ -822,6 +680,7 @@ class RyanteckRobot(Robot):
|
||||
robot = RyanteckRobot()
|
||||
robot.left()
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(RyanteckRobot, self).__init__(left=(17, 18), right=(22, 23))
|
||||
|
||||
@@ -841,5 +700,111 @@ class CamJamKitRobot(Robot):
|
||||
|
||||
.. _CamJam #3 EduKit: http://camjam.me/?page_id=1035
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(CamJamKitRobot, self).__init__(left=(9, 10), right=(7, 8))
|
||||
|
||||
|
||||
class _EnergenieMaster(SharedMixin, CompositeOutputDevice):
|
||||
def __init__(self):
|
||||
self._lock = Lock()
|
||||
super(_EnergenieMaster, self).__init__(
|
||||
*(OutputDevice(pin) for pin in (17, 22, 23, 27)),
|
||||
mode=OutputDevice(24), enable=OutputDevice(25))
|
||||
|
||||
def close(self):
|
||||
if self._lock:
|
||||
with self._lock:
|
||||
super(_EnergenieMaster, self).close()
|
||||
self._lock = None
|
||||
|
||||
@classmethod
|
||||
def _shared_key(cls):
|
||||
# There's only one Energenie master
|
||||
return None
|
||||
|
||||
def transmit(self, socket, enable):
|
||||
with self._lock:
|
||||
try:
|
||||
code = (8 * bool(enable)) + (7 - socket)
|
||||
for bit in self.all[: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 ``False``, meaning off). Instances of this class can
|
||||
be used to switch peripherals on and off. For example::
|
||||
|
||||
from gpiozero import Energenie
|
||||
|
||||
lamp = Energenie(0)
|
||||
lamp.on()
|
||||
|
||||
:param int socket:
|
||||
Which socket this instance should control. This is an integer number
|
||||
between 0 and 3.
|
||||
|
||||
:param bool initial_value:
|
||||
The initial state of the socket. As Energenie sockets provide no
|
||||
means of reading their state, you must provide an initial state for
|
||||
the socket, which will be set upon construction. This defaults to
|
||||
``False`` which will switch the socket off.
|
||||
|
||||
.. _Energenie socket: https://energenie4u.co.uk/index.php/catalogue/product/ENER002-2PI
|
||||
"""
|
||||
|
||||
def __init__(self, socket=None, initial_value=False):
|
||||
if socket is None:
|
||||
raise EnergenieSocketMissing('socket number must be provided')
|
||||
if not (0 <= socket < 4):
|
||||
raise EnergenieBadSocket('socket number must be between 0 and 3')
|
||||
super(Energenie, self).__init__()
|
||||
self._socket = socket
|
||||
self._master = _EnergenieMaster()
|
||||
if initial_value:
|
||||
self.on()
|
||||
else:
|
||||
self.off()
|
||||
|
||||
def close(self):
|
||||
if self._master:
|
||||
m = self._master
|
||||
self._master = None
|
||||
m.close()
|
||||
|
||||
@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 value(self):
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
self._master.transmit(self._socket, bool(value))
|
||||
self._value = bool(value)
|
||||
|
||||
def on(self):
|
||||
self.value = True
|
||||
|
||||
def off(self):
|
||||
self.value = False
|
||||
|
||||
|
||||
@@ -9,20 +9,17 @@ str = type('')
|
||||
|
||||
import atexit
|
||||
import weakref
|
||||
from threading import Thread, Event, RLock
|
||||
from collections import deque
|
||||
from collections import namedtuple
|
||||
from itertools import chain
|
||||
from types import FunctionType
|
||||
try:
|
||||
from statistics import median, mean
|
||||
except ImportError:
|
||||
from .compat import median, mean
|
||||
from threading import RLock
|
||||
|
||||
from .threads import GPIOThread, _threads_shutdown
|
||||
from .exc import (
|
||||
DeviceClosed,
|
||||
GPIOPinMissing,
|
||||
GPIOPinInUse,
|
||||
GPIODeviceClosed,
|
||||
GPIOBadQueueLen,
|
||||
GPIOBadSampleWait,
|
||||
GPIOBadSourceDelay,
|
||||
)
|
||||
|
||||
@@ -30,7 +27,7 @@ from .exc import (
|
||||
# as it supports PWM, and all Pi revisions. If no third-party libraries are
|
||||
# available, however, we fall back to a pure Python implementation which
|
||||
# supports platforms like PyPy
|
||||
from .pins import PINS_CLEANUP
|
||||
from .pins import _pins_shutdown
|
||||
try:
|
||||
from .pins.rpigpio import RPiGPIOPin
|
||||
DefaultPin = RPiGPIOPin
|
||||
@@ -47,24 +44,17 @@ except ImportError:
|
||||
DefaultPin = NativePin
|
||||
|
||||
|
||||
_THREADS = set()
|
||||
_PINS = set()
|
||||
# Due to interactions between RPi.GPIO cleanup and the GPIODevice.close()
|
||||
# method the same thread may attempt to acquire this lock, leading to deadlock
|
||||
# unless the lock is re-entrant
|
||||
_PINS_LOCK = RLock()
|
||||
_PINS_LOCK = RLock() # Yes, this needs to be re-entrant
|
||||
|
||||
def _shutdown():
|
||||
while _THREADS:
|
||||
for t in _THREADS.copy():
|
||||
t.stop()
|
||||
_threads_shutdown()
|
||||
with _PINS_LOCK:
|
||||
while _PINS:
|
||||
_PINS.pop().close()
|
||||
# Any cleanup routines registered by pins libraries must be called *after*
|
||||
# cleanup of pin objects used by devices
|
||||
for routine in PINS_CLEANUP:
|
||||
routine()
|
||||
_pins_shutdown()
|
||||
|
||||
atexit.register(_shutdown)
|
||||
|
||||
@@ -75,9 +65,9 @@ class GPIOMeta(type):
|
||||
def __new__(mcls, name, bases, cls_dict):
|
||||
# Construct the class as normal
|
||||
cls = super(GPIOMeta, mcls).__new__(mcls, name, bases, cls_dict)
|
||||
# If there's a method in the class which has no docstring, search
|
||||
# the base classes recursively for a docstring to copy
|
||||
for attr_name, attr in cls_dict.items():
|
||||
# If there's a method in the class which has no docstring, search
|
||||
# the base classes recursively for a docstring to copy
|
||||
if isinstance(attr, FunctionType) and not attr.__doc__:
|
||||
for base_cls in cls.__mro__:
|
||||
if hasattr(base_cls, attr_name):
|
||||
@@ -87,17 +77,45 @@ class GPIOMeta(type):
|
||||
break
|
||||
return cls
|
||||
|
||||
def __call__(mcls, *args, **kwargs):
|
||||
# Construct the instance as normal and ensure it's an instance of
|
||||
# GPIOBase (defined below with a custom __setattrs__)
|
||||
result = super(GPIOMeta, mcls).__call__(*args, **kwargs)
|
||||
assert isinstance(result, GPIOBase)
|
||||
def __call__(cls, *args, **kwargs):
|
||||
# Make sure cls has GPIOBase somewhere in its ancestry (otherwise
|
||||
# setting __attrs__ below will be rather pointless)
|
||||
assert issubclass(cls, GPIOBase)
|
||||
if issubclass(cls, SharedMixin):
|
||||
# If SharedMixin appears in the class' ancestry, convert the
|
||||
# constructor arguments to a key and check whether an instance
|
||||
# already exists. Only construct the instance if the key's new.
|
||||
key = cls._shared_key(*args, **kwargs)
|
||||
try:
|
||||
self = cls._INSTANCES[key]
|
||||
self._refs += 1
|
||||
except (KeyError, ReferenceError) as e:
|
||||
self = super(GPIOMeta, cls).__call__(*args, **kwargs)
|
||||
self._refs = 1
|
||||
# Replace the close method with one that merely decrements
|
||||
# the refs counter and calls the original close method when
|
||||
# it reaches zero
|
||||
old_close = self.close
|
||||
def close():
|
||||
self._refs = max(0, self._refs - 1)
|
||||
if not self._refs:
|
||||
try:
|
||||
old_close()
|
||||
finally:
|
||||
del cls._INSTANCES[key]
|
||||
self.close = close
|
||||
cls._INSTANCES[key] = weakref.proxy(self)
|
||||
else:
|
||||
# Construct the instance as normal
|
||||
self = super(GPIOMeta, cls).__call__(*args, **kwargs)
|
||||
# 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
|
||||
# GPIOBase.__setattr__). An exception is made for SharedMixin devices
|
||||
# which can be constructed multiple times, returning the same instance
|
||||
if not issubclass(cls, SharedMixin) or self._refs == 1:
|
||||
self.__attrs__ = frozenset(dir(self))
|
||||
return self
|
||||
|
||||
|
||||
# Cross-version compatible method of using a metaclass
|
||||
@@ -119,13 +137,47 @@ class GPIOBase(GPIOMeta(nstr('GPIOBase'), (), {})):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Shut down the device and release all associated resources. This method
|
||||
can be called on an already closed device without raising an exception.
|
||||
|
||||
This method is primarily intended for interactive use at the command
|
||||
line. It disables the device and releases its pin(s) for use by another
|
||||
device.
|
||||
|
||||
You can attempt to do this simply by deleting an object, but unless
|
||||
you've cleaned up all references to the object this may not work (even
|
||||
if you've cleaned up all references, there's still no guarantee the
|
||||
garbage collector will actually delete the object at that point). By
|
||||
contrast, the close method provides a means of ensuring that the object
|
||||
is shut down.
|
||||
|
||||
For example, if you have a breadboard with a buzzer connected to pin
|
||||
16, but then wish to attach an LED instead:
|
||||
|
||||
>>> from gpiozero import *
|
||||
>>> bz = Buzzer(16)
|
||||
>>> bz.on()
|
||||
>>> bz.off()
|
||||
>>> bz.close()
|
||||
>>> led = LED(16)
|
||||
>>> led.blink()
|
||||
|
||||
:class:`Device` descendents can also be used as context managers using
|
||||
the :keyword:`with` statement. For example:
|
||||
|
||||
>>> from gpiozero import *
|
||||
>>> with Buzzer(16) as bz:
|
||||
... bz.on()
|
||||
...
|
||||
>>> with LED(16) as led:
|
||||
... led.on()
|
||||
...
|
||||
"""
|
||||
# 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).
|
||||
"""
|
||||
Shut down the device and release all associated resources.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@@ -137,6 +189,11 @@ class GPIOBase(GPIOMeta(nstr('GPIOBase'), (), {})):
|
||||
"""
|
||||
return False
|
||||
|
||||
def _check_open(self):
|
||||
if self.closed:
|
||||
raise DeviceClosed(
|
||||
'%s is closed or uninitialized' % self.__class__.__name__)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
@@ -145,7 +202,14 @@ class GPIOBase(GPIOMeta(nstr('GPIOBase'), (), {})):
|
||||
|
||||
|
||||
class ValuesMixin(object):
|
||||
# NOTE Use this mixin *first* in the parent list
|
||||
"""
|
||||
Adds a :attr:`values` property to the class which returns an infinite
|
||||
generator of readings from the :attr:`value` property.
|
||||
|
||||
.. note::
|
||||
|
||||
Use this mixin *first* in the parent class list.
|
||||
"""
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
@@ -160,7 +224,14 @@ class ValuesMixin(object):
|
||||
|
||||
|
||||
class SourceMixin(object):
|
||||
# NOTE Use this mixin *first* in the parent list
|
||||
"""
|
||||
Adds a :attr:`source` property to the class which, given an iterable,
|
||||
sets :attr:`value` to each member of that iterable until it is exhausted.
|
||||
|
||||
.. note::
|
||||
|
||||
Use this mixin *first* in the parent class list.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._source = None
|
||||
@@ -214,18 +285,121 @@ class SourceMixin(object):
|
||||
self._source_thread.start()
|
||||
|
||||
|
||||
class CompositeDevice(ValuesMixin, GPIOBase):
|
||||
class SharedMixin(object):
|
||||
"""
|
||||
Represents a device composed of multiple GPIO devices like simple HATs,
|
||||
H-bridge motor controllers, robots composed of multiple motors, etc.
|
||||
This mixin marks a class as "shared". In this case, the meta-class
|
||||
(GPIOMeta) will use :meth:`_shared_key` to convert the constructor
|
||||
arguments to an immutable key, and will check whether any existing
|
||||
instances match that key. If they do, they will be returned by the
|
||||
constructor instead of a new instance. An internal reference counter is
|
||||
used to determine how many times an instance has been "constructed" in this
|
||||
way.
|
||||
|
||||
When :meth:`close` is called, an internal reference counter will be
|
||||
decremented and the instance will only close when it reaches zero.
|
||||
"""
|
||||
_INSTANCES = {}
|
||||
|
||||
def __del__(self):
|
||||
self._refs = 0
|
||||
super(SharedMixin, self).__del__()
|
||||
|
||||
@classmethod
|
||||
def _shared_key(cls, *args, **kwargs):
|
||||
"""
|
||||
Given the constructor arguments, returns an immutable key representing
|
||||
the instance. The default simply assumes all positional arguments are
|
||||
immutable.
|
||||
"""
|
||||
return args
|
||||
|
||||
|
||||
class Device(ValuesMixin, GPIOBase):
|
||||
"""
|
||||
Represents a single device of any type; GPIO-based, SPI-based, I2C-based,
|
||||
etc. This is the base class of the device hierarchy.
|
||||
"""
|
||||
def __repr__(self):
|
||||
return "<gpiozero.%s object>" % (self.__class__.__name__)
|
||||
|
||||
|
||||
class GPIODevice(ValuesMixin, GPIOBase):
|
||||
class CompositeDevice(Device):
|
||||
"""
|
||||
Represents a generic GPIO device.
|
||||
Extends :class:`Device`. Represents a device composed of multiple devices
|
||||
like simple HATs, H-bridge motor controllers, robots composed of multiple
|
||||
motors, etc.
|
||||
|
||||
The constructor accepts subordinate devices as positional or keyword
|
||||
arguments. Positional arguments form unnamed devices accessed via the
|
||||
:attr:`all` attribute, while keyword arguments are added to the device
|
||||
as named (read-only) attributes.
|
||||
|
||||
:param list _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 arbitrary order will be selected for keyword
|
||||
arguments.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._all = ()
|
||||
self._named = {}
|
||||
self._tuple = None
|
||||
self._order = kwargs.pop('_order', None)
|
||||
if self._order is None:
|
||||
self._order = kwargs.keys()
|
||||
self._order = tuple(self._order)
|
||||
for missing_name in set(self._order) - set(kwargs.keys()):
|
||||
raise ValueError('%s missing from _order' % missing_name)
|
||||
super(CompositeDevice, self).__init__()
|
||||
for name in set(self._order) & set(dir(self)):
|
||||
raise CompositeDeviceBadName('%s is a reserved name' % name)
|
||||
self._all = args + tuple(kwargs[v] for v in self._order)
|
||||
self._named = kwargs
|
||||
self._tuple = namedtuple('CompositeDeviceValue', chain(
|
||||
(str(i) for i in range(len(args))), self._order),
|
||||
rename=True)
|
||||
|
||||
def __getattr__(self, name):
|
||||
# if _named doesn't exist yet, pretend it's an empty dict
|
||||
if name == '_named':
|
||||
return {}
|
||||
try:
|
||||
return self._named[name]
|
||||
except KeyError:
|
||||
raise AttributeError("no such attribute %s" % name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# make named components read-only properties
|
||||
if name in self._named:
|
||||
raise AttributeError("can't set attribute %s" % name)
|
||||
return super(CompositeDevice, self).__setattr__(name, value)
|
||||
|
||||
@property
|
||||
def all(self):
|
||||
return self._all
|
||||
|
||||
def close(self):
|
||||
for device in self._all:
|
||||
device.close()
|
||||
self._all = ()
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
return bool(self._all)
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
return self._tuple
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.tuple(*(device.value for device in self._all))
|
||||
|
||||
|
||||
class GPIODevice(Device):
|
||||
"""
|
||||
Extends :class:`Device`. Represents a generic GPIO device.
|
||||
|
||||
This is the class at the root of the gpiozero class hierarchy. It handles
|
||||
ensuring that two GPIO devices do not share the same pin, and provides
|
||||
@@ -267,48 +441,7 @@ class GPIODevice(ValuesMixin, GPIOBase):
|
||||
def _fire_events(self):
|
||||
pass
|
||||
|
||||
def _check_open(self):
|
||||
if self.closed:
|
||||
raise GPIODeviceClosed(
|
||||
'%s is closed or uninitialized' % self.__class__.__name__)
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Shut down the device and release all associated resources.
|
||||
|
||||
This method is primarily intended for interactive use at the command
|
||||
line. It disables the device and releases its pin for use by another
|
||||
device.
|
||||
|
||||
You can attempt to do this simply by deleting an object, but unless
|
||||
you've cleaned up all references to the object this may not work (even
|
||||
if you've cleaned up all references, there's still no guarantee the
|
||||
garbage collector will actually delete the object at that point). By
|
||||
contrast, the close method provides a means of ensuring that the object
|
||||
is shut down.
|
||||
|
||||
For example, if you have a breadboard with a buzzer connected to pin
|
||||
16, but then wish to attach an LED instead:
|
||||
|
||||
>>> from gpiozero import *
|
||||
>>> bz = Buzzer(16)
|
||||
>>> bz.on()
|
||||
>>> bz.off()
|
||||
>>> bz.close()
|
||||
>>> led = LED(16)
|
||||
>>> led.blink()
|
||||
|
||||
:class:`GPIODevice` descendents can also be used as context managers
|
||||
using the :keyword:`with` statement. For example:
|
||||
|
||||
>>> from gpiozero import *
|
||||
>>> with Buzzer(16) as bz:
|
||||
... bz.on()
|
||||
...
|
||||
>>> with LED(16) as led:
|
||||
... led.on()
|
||||
...
|
||||
"""
|
||||
super(GPIODevice, self).close()
|
||||
with _PINS_LOCK:
|
||||
pin = self._pin
|
||||
@@ -321,6 +454,13 @@ class GPIODevice(ValuesMixin, GPIOBase):
|
||||
def closed(self):
|
||||
return self._pin is None
|
||||
|
||||
def _check_open(self):
|
||||
try:
|
||||
super(GPIODevice, self)._check_open()
|
||||
except DeviceClosed as e:
|
||||
# For backwards compatibility; GPIODeviceClosed is deprecated
|
||||
raise GPIODeviceClosed(str(e))
|
||||
|
||||
@property
|
||||
def pin(self):
|
||||
"""
|
||||
@@ -349,66 +489,3 @@ class GPIODevice(ValuesMixin, GPIOBase):
|
||||
return "<gpiozero.%s object closed>" % self.__class__.__name__
|
||||
|
||||
|
||||
class GPIOThread(Thread):
|
||||
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
|
||||
super(GPIOThread, self).__init__(group, target, name, args, kwargs)
|
||||
self.stopping = Event()
|
||||
self.daemon = True
|
||||
|
||||
def start(self):
|
||||
self.stopping.clear()
|
||||
_THREADS.add(self)
|
||||
super(GPIOThread, self).start()
|
||||
|
||||
def stop(self):
|
||||
self.stopping.set()
|
||||
self.join()
|
||||
|
||||
def join(self):
|
||||
super(GPIOThread, self).join()
|
||||
_THREADS.discard(self)
|
||||
|
||||
|
||||
class GPIOQueue(GPIOThread):
|
||||
def __init__(
|
||||
self, parent, queue_len=5, sample_wait=0.0, partial=False,
|
||||
average=median):
|
||||
assert isinstance(parent, GPIODevice)
|
||||
assert callable(average)
|
||||
super(GPIOQueue, self).__init__(target=self.fill)
|
||||
if queue_len < 1:
|
||||
raise GPIOBadQueueLen('queue_len must be at least one')
|
||||
if sample_wait < 0:
|
||||
raise GPIOBadSampleWait('sample_wait must be 0 or greater')
|
||||
self.queue = deque(maxlen=queue_len)
|
||||
self.partial = partial
|
||||
self.sample_wait = sample_wait
|
||||
self.full = Event()
|
||||
self.parent = weakref.proxy(parent)
|
||||
self.average = average
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
if not self.partial:
|
||||
self.full.wait()
|
||||
try:
|
||||
return self.average(self.queue)
|
||||
except ZeroDivisionError:
|
||||
# No data == inactive value
|
||||
return 0.0
|
||||
|
||||
def fill(self):
|
||||
try:
|
||||
while (not self.stopping.wait(self.sample_wait) and
|
||||
len(self.queue) < self.queue.maxlen):
|
||||
self.queue.append(self.parent._read())
|
||||
if self.partial:
|
||||
self.parent._fire_events()
|
||||
self.full.set()
|
||||
while not self.stopping.wait(self.sample_wait):
|
||||
self.queue.append(self.parent._read())
|
||||
self.parent._fire_events()
|
||||
except ReferenceError:
|
||||
# Parent is dead; time to die!
|
||||
pass
|
||||
|
||||
|
||||
@@ -10,14 +10,32 @@ str = type('')
|
||||
class GPIOZeroError(Exception):
|
||||
"Base class for all exceptions in GPIO Zero"
|
||||
|
||||
class DeviceClosed(GPIOZeroError):
|
||||
"Error raised when an operation is attempted on a closed device"
|
||||
|
||||
class CompositeDeviceError(GPIOZeroError):
|
||||
"Base class for errors specific to the CompositeDevice hierarchy"
|
||||
|
||||
class CompositeDeviceBadName(CompositeDeviceError, ValueError):
|
||||
"Error raised when a composite device is constructed with a reserved name"
|
||||
|
||||
class EnergenieSocketMissing(CompositeDeviceError, ValueError):
|
||||
"Error raised when socket number is not specified"
|
||||
|
||||
class EnergenieBadSocket(CompositeDeviceError, ValueError):
|
||||
"Error raised when an invalid socket number is passed to :class:`Energenie`"
|
||||
|
||||
class SPIError(GPIOZeroError):
|
||||
"Base class for errors related to the SPI implementation"
|
||||
|
||||
class SPIBadArgs(SPIError, ValueError):
|
||||
"Error raised when invalid arguments are given while constructing :class:`SPIDevice`"
|
||||
|
||||
class GPIODeviceError(GPIOZeroError):
|
||||
"Base class for errors specific to the GPIODevice hierarchy"
|
||||
|
||||
class GPIODeviceClosed(GPIODeviceError):
|
||||
"Error raised when an operation is attempted on a closed device"
|
||||
"Deprecated descendent of :exc:`DeviceClosed`"
|
||||
|
||||
class GPIOPinInUse(GPIODeviceError):
|
||||
"Error raised when attempting to use a pin already in use by another device"
|
||||
@@ -82,3 +100,12 @@ class PinPWMUnsupported(PinPWMError, AttributeError):
|
||||
class PinPWMFixedValue(PinPWMError, AttributeError):
|
||||
"Error raised when attempting to initialize PWM on an input pin"
|
||||
|
||||
class GPIOZeroWarning(Warning):
|
||||
"Base class for all warnings in GPIO Zero"
|
||||
|
||||
class SPIWarning(GPIOZeroWarning):
|
||||
"Base class for warnings related to the SPI implementation"
|
||||
|
||||
class SPISoftwareFallback(SPIWarning):
|
||||
"Warning raised when falling back to the software implementation"
|
||||
|
||||
|
||||
@@ -13,10 +13,9 @@ from functools import wraps
|
||||
from time import sleep, time
|
||||
from threading import Event
|
||||
|
||||
from spidev import SpiDev
|
||||
|
||||
from .exc import InputDeviceError, GPIODeviceError, GPIODeviceClosed
|
||||
from .devices import GPIODevice, CompositeDevice, GPIOQueue
|
||||
from .devices import GPIODevice, CompositeDevice
|
||||
from .threads import GPIOQueue
|
||||
|
||||
|
||||
class InputDevice(GPIODevice):
|
||||
@@ -77,8 +76,10 @@ class WaitableInputDevice(InputDevice):
|
||||
state (:meth:`when_activated` and :meth:`when_deactivated`). These are
|
||||
aliased appropriately in various subclasses.
|
||||
|
||||
Note that this class provides no means of actually firing its events; it's
|
||||
effectively an abstract base class.
|
||||
.. note::
|
||||
|
||||
Note that this class provides no means of actually firing its events;
|
||||
it's effectively an abstract base class.
|
||||
"""
|
||||
def __init__(self, pin=None, pull_up=False):
|
||||
super(WaitableInputDevice, self).__init__(pin, pull_up)
|
||||
@@ -244,6 +245,13 @@ class SmoothedInputDevice(WaitableInputDevice):
|
||||
threshold which is used to determine the state of the :attr:`is_active`
|
||||
property.
|
||||
|
||||
.. note::
|
||||
|
||||
The background queue is not automatically started upon construction.
|
||||
This is to allow descendents to set up additional components before the
|
||||
queue starts reading values. Effectively this is an abstract base
|
||||
class.
|
||||
|
||||
This class is intended for use with devices which either exhibit analog
|
||||
behaviour (such as the charging time of a capacitor with an LDR), or those
|
||||
which exhibit "twitchy" behaviour (such as certain motion sensors).
|
||||
@@ -760,350 +768,3 @@ DistanceSensor.wait_for_out_of_range = DistanceSensor.wait_for_active
|
||||
DistanceSensor.wait_for_in_range = DistanceSensor.wait_for_inactive
|
||||
|
||||
|
||||
class AnalogInputDevice(CompositeDevice):
|
||||
"""
|
||||
Represents an analog input device connected to SPI (serial interface).
|
||||
|
||||
Typical analog input devices are `analog to digital converters`_ (ADCs).
|
||||
Several classes are provided for specific ADC chips, including
|
||||
:class:`MCP3004`, :class:`MCP3008`, :class:`MCP3204`, and :class:`MCP3208`.
|
||||
|
||||
The following code demonstrates reading the first channel of an MCP3008
|
||||
chip attached to the Pi's SPI pins::
|
||||
|
||||
from gpiozero import MCP3008
|
||||
|
||||
pot = MCP3008(0)
|
||||
print(pot.value)
|
||||
|
||||
The :attr:`value` attribute is normalized such that its value is always
|
||||
between 0.0 and 1.0 (or in special cases, such as differential sampling,
|
||||
-1 to +1). Hence, you can use an analog input to control the brightness of
|
||||
a :class:`PWMLED` like so::
|
||||
|
||||
from gpiozero import MCP3008, PWMLED
|
||||
|
||||
pot = MCP3008(0)
|
||||
led = PWMLED(17)
|
||||
led.source = pot.values
|
||||
|
||||
.. _analog to digital converters: https://en.wikipedia.org/wiki/Analog-to-digital_converter
|
||||
"""
|
||||
|
||||
def __init__(self, device=0, bits=None):
|
||||
if bits is None:
|
||||
raise InputDeviceError('you must specify the bit resolution of the device')
|
||||
if device not in (0, 1):
|
||||
raise InputDeviceError('device must be 0 or 1')
|
||||
self._device = device
|
||||
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()
|
||||
super(AnalogInputDevice, self).close()
|
||||
|
||||
@property
|
||||
def bits(self):
|
||||
"""
|
||||
The bit-resolution of the device/channel.
|
||||
"""
|
||||
return self._bits
|
||||
|
||||
@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):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
The current value read from the device, scaled to a value between 0 and
|
||||
1.
|
||||
"""
|
||||
return self._read() / (2**self.bits - 1)
|
||||
|
||||
@property
|
||||
def raw_value(self):
|
||||
"""
|
||||
The raw value as read from the device.
|
||||
"""
|
||||
return self._read()
|
||||
|
||||
|
||||
class MCP3xxx(AnalogInputDevice):
|
||||
"""
|
||||
Extends :class:`AnalogInputDevice` to implement an interface for all ADC
|
||||
chips with a protocol similar to the Microchip MCP3xxx series of devices.
|
||||
"""
|
||||
|
||||
def __init__(self, channel=0, device=0, bits=10, differential=False):
|
||||
self._channel = channel
|
||||
self._bits = bits
|
||||
self._differential = bool(differential)
|
||||
super(MCP3xxx, self).__init__(device, bits)
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
"""
|
||||
The channel to read data from. The MCP3008/3208/3304 have 8 channels
|
||||
(0-7), while the MCP3004/3204/3302 have 4 channels (0-3), and the
|
||||
MCP3301 only has 1 channel.
|
||||
"""
|
||||
return self._channel
|
||||
|
||||
@property
|
||||
def differential(self):
|
||||
"""
|
||||
If ``True``, the device is operated in pseudo-differential mode. In
|
||||
this mode one channel (specified by the channel attribute) is read
|
||||
relative to the value of a second channel (implied by the chip's
|
||||
design).
|
||||
|
||||
Please refer to the device data-sheet to determine which channel is
|
||||
used as the relative base value (for example, when using an
|
||||
:class:`MCP3008` in differential mode, channel 0 is read relative to
|
||||
channel 1).
|
||||
"""
|
||||
return self._differential
|
||||
|
||||
def _read(self):
|
||||
# MCP3008/04 or MCP3208/04 protocol looks like the following:
|
||||
#
|
||||
# Byte 0 1 2
|
||||
# ==== ======== ======== ========
|
||||
# Tx 0001MCCC xxxxxxxx xxxxxxxx
|
||||
# Rx xxxxxxxx x0RRRRRR RRRRxxxx for the 3004/08
|
||||
# Rx xxxxxxxx x0RRRRRR RRRRRRxx for the 3204/08
|
||||
#
|
||||
# The transmit bits start with 3 preamble bits "000" (to warm up), a
|
||||
# start bit "1" followed by the single/differential bit (M) which is 1
|
||||
# for single-ended read, and 0 for differential read, followed by
|
||||
# 3-bits for the channel (C). The remainder of the transmission are
|
||||
# "don't care" bits (x).
|
||||
#
|
||||
# The first byte received and the top 1 bit of the second byte are
|
||||
# don't care bits (x). These are followed by a null bit (0), and then
|
||||
# the result bits (R). 10 bits for the MCP300x, 12 bits for the
|
||||
# MCP320x.
|
||||
#
|
||||
# XXX Differential mode still requires testing
|
||||
data = self._spi.xfer2([16 + [8, 0][self.differential] + self.channel, 0, 0])
|
||||
return ((data[1] & 63) << (self.bits - 6)) | (data[2] >> (14 - self.bits))
|
||||
|
||||
|
||||
class MCP33xx(MCP3xxx):
|
||||
"""
|
||||
Extends :class:`MCP3xxx` with functionality specific to the MCP33xx family
|
||||
of ADCs; specifically this handles the full differential capability of
|
||||
these chips supporting the full 13-bit signed range of output values.
|
||||
"""
|
||||
|
||||
def __init__(self, channel=0, device=0, differential=False):
|
||||
super(MCP33xx, self).__init__(channel, device, 12, differential)
|
||||
|
||||
def _read(self):
|
||||
# MCP3304/02 protocol looks like the following:
|
||||
#
|
||||
# Byte 0 1 2
|
||||
# ==== ======== ======== ========
|
||||
# Tx 0001MCCC xxxxxxxx xxxxxxxx
|
||||
# Rx xxxxxxxx x0SRRRRR RRRRRRRx
|
||||
#
|
||||
# The transmit bits start with 3 preamble bits "000" (to warm up), a
|
||||
# start bit "1" followed by the single/differential bit (M) which is 1
|
||||
# for single-ended read, and 0 for differential read, followed by
|
||||
# 3-bits for the channel (C). The remainder of the transmission are
|
||||
# "don't care" bits (x).
|
||||
#
|
||||
# The first byte received and the top 1 bit of the second byte are
|
||||
# don't care bits (x). These are followed by a null bit (0), then the
|
||||
# sign bit (S), and then the 12 result bits (R).
|
||||
#
|
||||
# In single read mode (the default) the sign bit is always zero and the
|
||||
# result is effectively 12-bits. In differential mode, the sign bit is
|
||||
# significant and the result is a two's-complement 13-bit value.
|
||||
#
|
||||
# The MCP3301 variant of the chip always operates in differential
|
||||
# mode and effectively only has one channel (composed of an IN+ and
|
||||
# IN-). As such it requires no input, just output. This is the reason
|
||||
# we split out _send() below; so that MCP3301 can override it.
|
||||
data = self._spi.xfer2(self._send())
|
||||
# Extract the last two bytes (again, for MCP3301)
|
||||
data = data[-2:]
|
||||
result = ((data[0] & 63) << 7) | (data[1] >> 1)
|
||||
# Account for the sign bit
|
||||
if self.differential and value > 4095:
|
||||
result = -(8192 - result)
|
||||
assert -4096 <= result < 4096
|
||||
return result
|
||||
|
||||
def _send(self):
|
||||
return [16 + [8, 0][self.differential] + self.channel, 0, 0]
|
||||
|
||||
|
||||
class MCP3001(MCP3xxx):
|
||||
"""
|
||||
The `MCP3001`_ is a 10-bit analog to digital converter with 1 channel
|
||||
|
||||
.. _MCP3001: http://www.farnell.com/datasheets/630400.pdf
|
||||
"""
|
||||
def __init__(self, device=0):
|
||||
super(MCP3001, self).__init__(0, device, 10, differential=True)
|
||||
|
||||
|
||||
class MCP3002(MCP3xxx):
|
||||
"""
|
||||
The `MCP3002`_ is a 10-bit analog to digital converter with 2 channels
|
||||
(0-3).
|
||||
|
||||
.. _MCP3002: http://www.farnell.com/datasheets/1599363.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, device=0, differential=False):
|
||||
if not 0 <= channel < 2:
|
||||
raise InputDeviceError('channel must be 0 or 1')
|
||||
super(MCP3002, self).__init__(channel, device, 10, differential)
|
||||
|
||||
|
||||
class MCP3004(MCP3xxx):
|
||||
"""
|
||||
The `MCP3004`_ is a 10-bit analog to digital converter with 4 channels
|
||||
(0-3).
|
||||
|
||||
.. _MCP3004: http://www.farnell.com/datasheets/808965.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, device=0, differential=False):
|
||||
if not 0 <= channel < 4:
|
||||
raise InputDeviceError('channel must be between 0 and 3')
|
||||
super(MCP3004, self).__init__(channel, device, 10, differential)
|
||||
|
||||
|
||||
class MCP3008(MCP3xxx):
|
||||
"""
|
||||
The `MCP3008`_ is a 10-bit analog to digital converter with 8 channels
|
||||
(0-7).
|
||||
|
||||
.. _MCP3008: http://www.farnell.com/datasheets/808965.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, device=0, differential=False):
|
||||
if not 0 <= channel < 8:
|
||||
raise InputDeviceError('channel must be between 0 and 7')
|
||||
super(MCP3008, self).__init__(channel, device, 10, differential)
|
||||
|
||||
|
||||
class MCP3201(MCP3xxx):
|
||||
"""
|
||||
The `MCP3201`_ is a 12-bit analog to digital converter with 1 channel
|
||||
|
||||
.. _MCP3201: http://www.farnell.com/datasheets/1669366.pdf
|
||||
"""
|
||||
def __init__(self, device=0):
|
||||
super(MCP3201, self).__init__(0, device, 12, differential=True)
|
||||
|
||||
|
||||
class MCP3202(MCP3xxx):
|
||||
"""
|
||||
The `MCP3202`_ is a 12-bit analog to digital converter with 2 channels
|
||||
(0-1).
|
||||
|
||||
.. _MCP3202: http://www.farnell.com/datasheets/1669376.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, device=0, differential=False):
|
||||
if not 0 <= channel < 2:
|
||||
raise InputDeviceError('channel must be 0 or 1')
|
||||
super(MCP3202, self).__init__(channel, device, 12, differential)
|
||||
|
||||
|
||||
class MCP3204(MCP3xxx):
|
||||
"""
|
||||
The `MCP3204`_ is a 12-bit analog to digital converter with 4 channels
|
||||
(0-3).
|
||||
|
||||
.. _MCP3204: http://www.farnell.com/datasheets/808967.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, device=0, differential=False):
|
||||
if not 0 <= channel < 4:
|
||||
raise InputDeviceError('channel must be between 0 and 3')
|
||||
super(MCP3204, self).__init__(channel, device, 12, differential)
|
||||
|
||||
|
||||
class MCP3208(MCP3xxx):
|
||||
"""
|
||||
The `MCP3208`_ is a 12-bit analog to digital converter with 8 channels
|
||||
(0-7).
|
||||
|
||||
.. _MCP3208: http://www.farnell.com/datasheets/808967.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, device=0, differential=False):
|
||||
if not 0 <= channel < 8:
|
||||
raise InputDeviceError('channel must be between 0 and 7')
|
||||
super(MCP3208, self).__init__(channel, device, 12, differential)
|
||||
|
||||
|
||||
class MCP3301(MCP33xx):
|
||||
"""
|
||||
The `MCP3301`_ is a signed 13-bit analog to digital converter. Please note
|
||||
that the MCP3301 always operates in differential mode between its two
|
||||
channels and the output value is scaled from -1 to +1.
|
||||
|
||||
.. _MCP3301: http://www.farnell.com/datasheets/1669397.pdf
|
||||
"""
|
||||
def __init__(self, device=0):
|
||||
super(MCP3301, self).__init__(0, device, differential=True)
|
||||
|
||||
def _send(self):
|
||||
return [0, 0]
|
||||
|
||||
|
||||
class MCP3302(MCP33xx):
|
||||
"""
|
||||
The `MCP3302`_ is a 12/13-bit analog to digital converter with 4 channels
|
||||
(0-3). When operated in differential mode, the device outputs a signed
|
||||
13-bit value which is scaled from -1 to +1. When operated in single-ended
|
||||
mode (the default), the device outputs an unsigned 12-bit value scaled from
|
||||
0 to 1.
|
||||
|
||||
.. _MCP3302: http://www.farnell.com/datasheets/1486116.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, device=0, differential=False):
|
||||
if not 0 <= channel < 4:
|
||||
raise InputDeviceError('channel must be between 0 and 4')
|
||||
super(MCP3302, self).__init__(channel, device, differential)
|
||||
|
||||
|
||||
class MCP3304(MCP33xx):
|
||||
"""
|
||||
The `MCP3304`_ is a 12/13-bit analog to digital converter with 8 channels
|
||||
(0-7). When operated in differential mode, the device outputs a signed
|
||||
13-bit value which is scaled from -1 to +1. When operated in single-ended
|
||||
mode (the default), the device outputs an unsigned 12-bit value scaled from
|
||||
0 to 1.
|
||||
|
||||
.. _MCP3304: http://www.farnell.com/datasheets/1486116.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, device=0, differential=False):
|
||||
if not 0 <= channel < 8:
|
||||
raise InputDeviceError('channel must be between 0 and 7')
|
||||
super(MCP3304, self).__init__(channel, device, differential)
|
||||
|
||||
@@ -10,8 +10,9 @@ from time import sleep
|
||||
from threading import Lock
|
||||
from itertools import repeat, cycle, chain
|
||||
|
||||
from .exc import OutputDeviceBadValue, GPIOPinMissing, GPIODeviceClosed
|
||||
from .devices import GPIODevice, GPIOThread, CompositeDevice, SourceMixin
|
||||
from .exc import OutputDeviceBadValue, GPIOPinMissing
|
||||
from .devices import GPIODevice, CompositeDevice, SourceMixin
|
||||
from .threads import GPIOThread
|
||||
|
||||
|
||||
class OutputDevice(SourceMixin, GPIODevice):
|
||||
@@ -38,10 +39,8 @@ class OutputDevice(SourceMixin, GPIODevice):
|
||||
device will be switched on initially.
|
||||
"""
|
||||
def __init__(self, pin=None, active_high=True, initial_value=False):
|
||||
self._active_high = active_high
|
||||
super(OutputDevice, self).__init__(pin)
|
||||
self._active_state = True if active_high else False
|
||||
self._inactive_state = False if active_high else True
|
||||
self.active_high = active_high
|
||||
if initial_value is None:
|
||||
self.pin.function = 'output'
|
||||
elif initial_value:
|
||||
@@ -72,6 +71,10 @@ class OutputDevice(SourceMixin, GPIODevice):
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
Returns ``True`` if the device is currently active and ``False``
|
||||
otherwise. Setting this property changes the state of the device.
|
||||
"""
|
||||
return super(OutputDevice, self).value
|
||||
|
||||
@value.setter
|
||||
@@ -80,7 +83,22 @@ class OutputDevice(SourceMixin, GPIODevice):
|
||||
|
||||
@property
|
||||
def active_high(self):
|
||||
return self._active_high
|
||||
"""
|
||||
When ``True``, the :attr:`value` property is ``True`` when the device's
|
||||
:attr:`pin` is high. When ``False`` the :attr:`value` property is
|
||||
``True`` when the device's pin is low (i.e. the value is inverted).
|
||||
|
||||
This property can be set after construction; be warned that changing it
|
||||
will invert :attr:`value` (i.e. changing this property doesn't change
|
||||
the device's pin state - it just changes how that state is
|
||||
interpreted).
|
||||
"""
|
||||
return self._active_state
|
||||
|
||||
@active_high.setter
|
||||
def active_high(self, value):
|
||||
self._active_state = True if value else False
|
||||
self._inactive_state = False if value else True
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
|
||||
@@ -16,6 +16,9 @@ from ..exc import (
|
||||
|
||||
|
||||
PINS_CLEANUP = []
|
||||
def _pins_shutdown():
|
||||
for routine in PINS_CLEANUP:
|
||||
routine()
|
||||
|
||||
|
||||
class Pin(object):
|
||||
|
||||
421
gpiozero/spi.py
Normal file
421
gpiozero/spi.py
Normal file
@@ -0,0 +1,421 @@
|
||||
from __future__ import (
|
||||
unicode_literals,
|
||||
print_function,
|
||||
absolute_import,
|
||||
division,
|
||||
)
|
||||
str = type('')
|
||||
|
||||
|
||||
import warnings
|
||||
import operator
|
||||
from threading import RLock
|
||||
|
||||
try:
|
||||
from spidev import SpiDev
|
||||
except ImportError:
|
||||
SpiDev = None
|
||||
|
||||
from .devices import Device, SharedMixin, _PINS, _PINS_LOCK
|
||||
from .input_devices import InputDevice
|
||||
from .output_devices import OutputDevice
|
||||
from .exc import SPIBadArgs, SPISoftwareFallback, GPIOPinInUse, DeviceClosed
|
||||
|
||||
|
||||
class SPIHardwareInterface(Device):
|
||||
def __init__(self, port, device):
|
||||
self._device = None
|
||||
super(SPIHardwareInterface, self).__init__()
|
||||
# XXX How can we detect conflicts with existing GPIO instances? This
|
||||
# isn't ideal ... in fact, it's downright crap and doesn't guard
|
||||
# against conflicts created *after* this instance, but it's all I can
|
||||
# come up with right now ...
|
||||
conflicts = (11, 10, 9, (8, 7)[device])
|
||||
with _PINS_LOCK:
|
||||
for pin in _PINS:
|
||||
if pin.number in conflicts:
|
||||
raise GPIOPinInUse(
|
||||
'pin %r is already in use by another gpiozero object' % pin
|
||||
)
|
||||
self._device_num = device
|
||||
self._device = SpiDev()
|
||||
self._device.open(port, device)
|
||||
self._device.max_speed_hz = 500000
|
||||
|
||||
def close(self):
|
||||
if self._device:
|
||||
try:
|
||||
self._device.close()
|
||||
finally:
|
||||
self._device = None
|
||||
super(SPIHardwareInterface, self).close()
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
return self._device is None
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
self._check_open()
|
||||
return (
|
||||
"hardware SPI on clock_pin=11, mosi_pin=10, miso_pin=9, "
|
||||
"select_pin=%d" % (
|
||||
8 if self._device_num == 0 else 7))
|
||||
except DeviceClosed:
|
||||
return "hardware SPI closed"
|
||||
|
||||
def read(self, n):
|
||||
return self.transfer((0,) * n)
|
||||
|
||||
def write(self, data):
|
||||
return len(self.transfer(data))
|
||||
|
||||
def transfer(self, data):
|
||||
"""
|
||||
Writes data (a list of integer words where each word is assumed to have
|
||||
:attr:`bits_per_word` bits or less) to the SPI interface, and reads an
|
||||
equivalent number of words, returning them as a list of integers.
|
||||
"""
|
||||
return self._device.xfer2(data)
|
||||
|
||||
def _get_clock_mode(self):
|
||||
return self._device.mode
|
||||
|
||||
def _set_clock_mode(self, value):
|
||||
self._device.mode = value
|
||||
|
||||
def _get_clock_polarity(self):
|
||||
return bool(self.mode & 2)
|
||||
|
||||
def _set_clock_polarity(self, value):
|
||||
self.mode = self.mode & (~2) | (bool(value) << 1)
|
||||
|
||||
def _get_clock_phase(self):
|
||||
return bool(self.mode & 1)
|
||||
|
||||
def _set_clock_phase(self, value):
|
||||
self.mode = self.mode & (~1) | bool(value)
|
||||
|
||||
def _get_lsb_first(self):
|
||||
return self._device.lsbfirst
|
||||
|
||||
def _set_lsb_first(self, value):
|
||||
self._device.lsbfirst = bool(value)
|
||||
|
||||
def _get_select_high(self):
|
||||
return self._device.cshigh
|
||||
|
||||
def _set_select_high(self, value):
|
||||
self._device.cshigh = bool(value)
|
||||
|
||||
def _get_bits_per_word(self):
|
||||
return self._device.bits_per_word
|
||||
|
||||
def _set_bits_per_word(self, value):
|
||||
self._device.bits_per_word = value
|
||||
|
||||
clock_polarity = property(_get_clock_polarity, _set_clock_polarity)
|
||||
clock_phase = property(_get_clock_phase, _set_clock_phase)
|
||||
clock_mode = property(_get_clock_mode, _set_clock_mode)
|
||||
lsb_first = property(_get_lsb_first, _set_lsb_first)
|
||||
select_high = property(_get_select_high, _set_select_high)
|
||||
bits_per_word = property(_get_bits_per_word, _set_bits_per_word)
|
||||
|
||||
|
||||
class SPISoftwareBus(SharedMixin, Device):
|
||||
def __init__(self, clock_pin, mosi_pin, miso_pin):
|
||||
self.lock = None
|
||||
self.clock = None
|
||||
self.mosi = None
|
||||
self.miso = None
|
||||
super(SPISoftwareBus, self).__init__()
|
||||
self.lock = RLock()
|
||||
self.clock_phase = False
|
||||
self.lsb_first = False
|
||||
self.bits_per_word = 8
|
||||
try:
|
||||
self.clock = OutputDevice(clock_pin, active_high=True)
|
||||
if mosi_pin is not None:
|
||||
self.mosi = OutputDevice(mosi_pin)
|
||||
if miso_pin is not None:
|
||||
self.miso = InputDevice(miso_pin)
|
||||
except:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
super(SPISoftwareBus, self).close()
|
||||
if self.lock:
|
||||
with self.lock:
|
||||
if self.miso is not None:
|
||||
self.miso.close()
|
||||
self.miso = None
|
||||
if self.mosi is not None:
|
||||
self.mosi.close()
|
||||
self.mosi = None
|
||||
if self.clock is not None:
|
||||
self.clock.close()
|
||||
self.clock = None
|
||||
self.lock = None
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
return self.lock is None
|
||||
|
||||
@classmethod
|
||||
def _shared_key(self, clock_pin, mosi_pin, miso_pin):
|
||||
return (clock_pin, mosi_pin, miso_pin)
|
||||
|
||||
def read(self, n):
|
||||
return self.transfer((0,) * n)
|
||||
|
||||
def write(self, data):
|
||||
return len(self.transfer(data))
|
||||
|
||||
def transfer(self, data):
|
||||
"""
|
||||
Writes data (a list of integer words where each word is assumed to have
|
||||
:attr:`bits_per_word` bits or less) to the SPI interface, and reads an
|
||||
equivalent number of words, returning them as a list of integers.
|
||||
"""
|
||||
result = []
|
||||
with self.lock:
|
||||
shift = operator.lshift if self.lsb_first else operator.rshift
|
||||
for write_word in data:
|
||||
mask = 1 if self.lsb_first else 1 << (self.bits_per_word - 1)
|
||||
read_word = 0
|
||||
for bit in range(self.bits_per_word):
|
||||
if self.mosi is not None:
|
||||
self.mosi.value = bool(write_word & mask)
|
||||
self.clock.on()
|
||||
if self.miso is not None and not self.clock_phase:
|
||||
if self.miso.value:
|
||||
read_word |= mask
|
||||
self.clock.off()
|
||||
if self.miso is not None and self.clock_phase:
|
||||
if self.miso.value:
|
||||
read_word |= mask
|
||||
mask = shift(mask, 1)
|
||||
result.append(read_word)
|
||||
return result
|
||||
|
||||
|
||||
class SPISoftwareInterface(OutputDevice):
|
||||
def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin):
|
||||
self._bus = None
|
||||
super(SPISoftwareInterface, self).__init__(select_pin, active_high=False)
|
||||
try:
|
||||
self._bus = SPISoftwareBus(clock_pin, mosi_pin, miso_pin)
|
||||
except:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
if self._bus:
|
||||
self._bus.close()
|
||||
self._bus = None
|
||||
super(SPISoftwareInterface, self).close()
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
self._check_open()
|
||||
return (
|
||||
"software SPI on clock_pin=%d, mosi_pin=%d, miso_pin=%d, "
|
||||
"select_pin=%d" % (
|
||||
self._bus.clock.pin.number,
|
||||
self._bus.mosi.pin.number,
|
||||
self._bus.miso.pin.number,
|
||||
self.pin.number))
|
||||
except DeviceClosed:
|
||||
return "software SPI closed"
|
||||
|
||||
def read(self, n):
|
||||
return self._bus.read(n)
|
||||
|
||||
def write(self, data):
|
||||
return self._bus.write(data)
|
||||
|
||||
def transfer(self, data):
|
||||
with self._bus.lock:
|
||||
self.on()
|
||||
try:
|
||||
return self._bus.transfer(data)
|
||||
finally:
|
||||
self.off()
|
||||
|
||||
def _get_clock_mode(self):
|
||||
return (self.clock_polarity << 1) | self.clock_phase
|
||||
|
||||
def _set_clock_mode(self, value):
|
||||
value = int(value)
|
||||
if not 0 <= value <= 3:
|
||||
raise ValueError('clock_mode must be a value between 0 and 3 inclusive')
|
||||
with self._bus.lock:
|
||||
self._bus.clock.active_high = not (value & 2)
|
||||
self._bus.clock.off()
|
||||
self._bus.clock_phase = bool(value & 1)
|
||||
|
||||
def _get_clock_polarity(self):
|
||||
return not self._bus.clock.active_high
|
||||
|
||||
def _set_clock_polarity(self, value):
|
||||
with self._bus.lock:
|
||||
self._bus.clock.active_high = not value
|
||||
|
||||
def _get_clock_phase(self):
|
||||
return self._bus.clock_phase
|
||||
|
||||
def _set_clock_phase(self, value):
|
||||
with self._bus.lock:
|
||||
self._bus.clock_phase = bool(value)
|
||||
|
||||
def _get_lsb_first(self):
|
||||
return self._bus.lsb_first
|
||||
|
||||
def _set_lsb_first(self, value):
|
||||
with self._bus.lock:
|
||||
self._bus.lsb_first = bool(value)
|
||||
|
||||
def _get_bits_per_word(self):
|
||||
return self._bus.bits_per_word
|
||||
|
||||
def _set_bits_per_word(self, value):
|
||||
if value < 1:
|
||||
raise ValueError('bits_per_word must be positive')
|
||||
with self._bus.lock:
|
||||
self._bus.bits_per_word = int(value)
|
||||
|
||||
def _get_select_high(self):
|
||||
return self.active_high
|
||||
|
||||
def _set_select_high(self, value):
|
||||
with self._bus.lock:
|
||||
self.active_high = value
|
||||
self.off()
|
||||
|
||||
clock_polarity = property(_get_clock_polarity, _set_clock_polarity)
|
||||
clock_phase = property(_get_clock_phase, _set_clock_phase)
|
||||
clock_mode = property(_get_clock_mode, _set_clock_mode)
|
||||
lsb_first = property(_get_lsb_first, _set_lsb_first)
|
||||
bits_per_word = property(_get_bits_per_word, _set_bits_per_word)
|
||||
select_high = property(_get_select_high, _set_select_high)
|
||||
|
||||
|
||||
class SharedSPIHardwareInterface(SharedMixin, SPIHardwareInterface):
|
||||
@classmethod
|
||||
def _shared_key(cls, port, device):
|
||||
return (port, device)
|
||||
|
||||
|
||||
class SharedSPISoftwareInterface(SharedMixin, SPISoftwareInterface):
|
||||
@classmethod
|
||||
def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin):
|
||||
return (clock_pin, mosi_pin, miso_pin, select_pin)
|
||||
|
||||
|
||||
def extract_spi_args(**kwargs):
|
||||
"""
|
||||
Given a set of keyword arguments, splits it into those relevant to SPI
|
||||
implementations and all the rest. SPI arguments are augmented with defaults
|
||||
and converted into the pin format (from the port/device format) if
|
||||
necessary.
|
||||
|
||||
Returns a tuple of ``(spi_args, other_args)``.
|
||||
"""
|
||||
pin_defaults = {
|
||||
'clock_pin': 11,
|
||||
'mosi_pin': 10,
|
||||
'miso_pin': 9,
|
||||
'select_pin': 8,
|
||||
}
|
||||
dev_defaults = {
|
||||
'port': 0,
|
||||
'device': 0,
|
||||
}
|
||||
spi_args = {
|
||||
key: value for (key, value) in kwargs.items()
|
||||
if key in pin_defaults or key in dev_defaults
|
||||
}
|
||||
kwargs = {
|
||||
key: value for (key, value) in kwargs.items()
|
||||
if key not in spi_args
|
||||
}
|
||||
if not spi_args:
|
||||
spi_args = pin_defaults
|
||||
elif set(spi_args) <= set(pin_defaults):
|
||||
spi_args = {
|
||||
key: spi_args.get(key, default)
|
||||
for key, default in pin_defaults.items()
|
||||
}
|
||||
elif set(spi_args) <= set(dev_defaults):
|
||||
spi_args = {
|
||||
key: spi_args.get(key, default)
|
||||
for key, default in dev_defaults.items()
|
||||
}
|
||||
if spi_args['port'] != 0:
|
||||
raise SPIBadArgs('port 0 is the only valid SPI port')
|
||||
if spi_args['device'] not in (0, 1):
|
||||
raise SPIBadArgs('device must be 0 or 1')
|
||||
spi_args = {
|
||||
key: value if key != 'select_pin' else (8, 7)[spi_args['device']]
|
||||
for key, value in pin_defaults.items()
|
||||
}
|
||||
else:
|
||||
raise SPIBadArgs(
|
||||
'you must either specify port and device, or clock_pin, mosi_pin, '
|
||||
'miso_pin, and select_pin; combinations of the two schemes (e.g. '
|
||||
'port and clock_pin) are not permitted')
|
||||
return spi_args, kwargs
|
||||
|
||||
|
||||
def SPI(**spi_args):
|
||||
"""
|
||||
Returns an SPI interface, for the specified SPI *port* and *device*, or for
|
||||
the specified pins (*clock_pin*, *mosi_pin*, *miso_pin*, and *select_pin*).
|
||||
Only one of the schemes can be used; attempting to mix *port* and *device*
|
||||
with pin numbers will raise :exc:`SPIBadArgs`.
|
||||
|
||||
If the pins specified match the hardware SPI pins (clock on GPIO11, MOSI on
|
||||
GPIO10, MISO on GPIO9, and chip select on GPIO8 or GPIO7), and the spidev
|
||||
module can be imported, a :class:`SPIHardwareInterface` instance will be
|
||||
returned. Otherwise, a :class:`SPISoftwareInterface` will be returned which
|
||||
will use simple bit-banging to communicate.
|
||||
|
||||
Both interfaces have the same API, support clock polarity and phase
|
||||
attributes, and can handle half and full duplex communications, but the
|
||||
hardware interface is significantly faster (though for many things this
|
||||
doesn't matter).
|
||||
|
||||
Finally, the *shared* keyword argument specifies whether the resulting
|
||||
SPI interface can be repeatedly created and used by multiple devices
|
||||
(useful with multi-channel devices like numerous ADCs).
|
||||
"""
|
||||
spi_args, kwargs = extract_spi_args(**spi_args)
|
||||
shared = kwargs.pop('shared', False)
|
||||
if kwargs:
|
||||
raise SPIBadArgs(
|
||||
'unrecognized keyword argument %s' % kwargs.popitem()[0])
|
||||
if all((
|
||||
SpiDev is not None,
|
||||
spi_args['clock_pin'] == 11,
|
||||
spi_args['mosi_pin'] == 10,
|
||||
spi_args['miso_pin'] == 9,
|
||||
spi_args['select_pin'] in (7, 8),
|
||||
)):
|
||||
try:
|
||||
if shared:
|
||||
return SharedSPIHardwareInterface(
|
||||
port=0, device={8: 0, 7: 1}[spi_args['select_pin']])
|
||||
else:
|
||||
return SPIHardwareInterface(
|
||||
port=0, device={8: 0, 7: 1}[spi_args['select_pin']])
|
||||
except Exception as e:
|
||||
warnings.warn(
|
||||
SPISoftwareFallback(
|
||||
'failed to initialize hardware SPI, falling back to '
|
||||
'software (error was: %s)' % str(e)))
|
||||
if shared:
|
||||
return SharedSPISoftwareInterface(**spi_args)
|
||||
else:
|
||||
return SPISoftwareInterface(**spi_args)
|
||||
|
||||
361
gpiozero/spi_devices.py
Normal file
361
gpiozero/spi_devices.py
Normal file
@@ -0,0 +1,361 @@
|
||||
from __future__ import (
|
||||
unicode_literals,
|
||||
print_function,
|
||||
absolute_import,
|
||||
division,
|
||||
)
|
||||
str = type('')
|
||||
|
||||
|
||||
from .exc import DeviceClosed
|
||||
from .devices import Device
|
||||
from .spi import extract_spi_args, SPI
|
||||
|
||||
|
||||
class SPIDevice(Device):
|
||||
"""
|
||||
Extends :class:`Device`. Represents a device that communicates via the SPI
|
||||
protocol.
|
||||
|
||||
See :ref:`spi_args` for information on the keyword arguments that can be
|
||||
specified with the constructor.
|
||||
"""
|
||||
def __init__(self, **spi_args):
|
||||
self._spi = SPI(**spi_args)
|
||||
|
||||
def close(self):
|
||||
if self._spi:
|
||||
s = self._spi
|
||||
self._spi = None
|
||||
s.close()
|
||||
super(SPIDevice, self).close()
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
return self._spi is None
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
self._check_open()
|
||||
return "<gpiozero.%s object using %r>" % (self.__class__.__name__, self._spi)
|
||||
except DeviceClosed:
|
||||
return "<gpiozero.%s object closed>" % self.__class__.__name__
|
||||
|
||||
|
||||
class AnalogInputDevice(SPIDevice):
|
||||
"""
|
||||
Represents an analog input device connected to SPI (serial interface).
|
||||
|
||||
Typical analog input devices are `analog to digital converters`_ (ADCs).
|
||||
Several classes are provided for specific ADC chips, including
|
||||
:class:`MCP3004`, :class:`MCP3008`, :class:`MCP3204`, and :class:`MCP3208`.
|
||||
|
||||
The following code demonstrates reading the first channel of an MCP3008
|
||||
chip attached to the Pi's SPI pins::
|
||||
|
||||
from gpiozero import MCP3008
|
||||
|
||||
pot = MCP3008(0)
|
||||
print(pot.value)
|
||||
|
||||
The :attr:`value` attribute is normalized such that its value is always
|
||||
between 0.0 and 1.0 (or in special cases, such as differential sampling,
|
||||
-1 to +1). Hence, you can use an analog input to control the brightness of
|
||||
a :class:`PWMLED` like so::
|
||||
|
||||
from gpiozero import MCP3008, PWMLED
|
||||
|
||||
pot = MCP3008(0)
|
||||
led = PWMLED(17)
|
||||
led.source = pot.values
|
||||
|
||||
.. _analog to digital converters: https://en.wikipedia.org/wiki/Analog-to-digital_converter
|
||||
"""
|
||||
|
||||
def __init__(self, bits=None, **spi_args):
|
||||
if bits is None:
|
||||
raise InputDeviceError('you must specify the bit resolution of the device')
|
||||
self._bits = bits
|
||||
super(AnalogInputDevice, self).__init__(shared=True, **spi_args)
|
||||
|
||||
@property
|
||||
def bits(self):
|
||||
"""
|
||||
The bit-resolution of the device/channel.
|
||||
"""
|
||||
return self._bits
|
||||
|
||||
def _read(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
The current value read from the device, scaled to a value between 0 and
|
||||
1 (or -1 to +1 for devices operating in differential mode).
|
||||
"""
|
||||
return self._read() / (2**self.bits - 1)
|
||||
|
||||
@property
|
||||
def raw_value(self):
|
||||
"""
|
||||
The raw value as read from the device.
|
||||
"""
|
||||
return self._read()
|
||||
|
||||
|
||||
class MCP3xxx(AnalogInputDevice):
|
||||
"""
|
||||
Extends :class:`AnalogInputDevice` to implement an interface for all ADC
|
||||
chips with a protocol similar to the Microchip MCP3xxx series of devices.
|
||||
"""
|
||||
|
||||
def __init__(self, channel=0, bits=10, differential=False, **spi_args):
|
||||
self._channel = channel
|
||||
self._bits = bits
|
||||
self._differential = bool(differential)
|
||||
super(MCP3xxx, self).__init__(bits, **spi_args)
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
"""
|
||||
The channel to read data from. The MCP3008/3208/3304 have 8 channels
|
||||
(0-7), while the MCP3004/3204/3302 have 4 channels (0-3), and the
|
||||
MCP3301 only has 1 channel.
|
||||
"""
|
||||
return self._channel
|
||||
|
||||
@property
|
||||
def differential(self):
|
||||
"""
|
||||
If ``True``, the device is operated in pseudo-differential mode. In
|
||||
this mode one channel (specified by the channel attribute) is read
|
||||
relative to the value of a second channel (implied by the chip's
|
||||
design).
|
||||
|
||||
Please refer to the device data-sheet to determine which channel is
|
||||
used as the relative base value (for example, when using an
|
||||
:class:`MCP3008` in differential mode, channel 0 is read relative to
|
||||
channel 1).
|
||||
"""
|
||||
return self._differential
|
||||
|
||||
def _read(self):
|
||||
# MCP3008/04 or MCP3208/04 protocol looks like the following:
|
||||
#
|
||||
# Byte 0 1 2
|
||||
# ==== ======== ======== ========
|
||||
# Tx 0001MCCC xxxxxxxx xxxxxxxx
|
||||
# Rx xxxxxxxx x0RRRRRR RRRRxxxx for the 3004/08
|
||||
# Rx xxxxxxxx x0RRRRRR RRRRRRxx for the 3204/08
|
||||
#
|
||||
# The transmit bits start with 3 preamble bits "000" (to warm up), a
|
||||
# start bit "1" followed by the single/differential bit (M) which is 1
|
||||
# for single-ended read, and 0 for differential read, followed by
|
||||
# 3-bits for the channel (C). The remainder of the transmission are
|
||||
# "don't care" bits (x).
|
||||
#
|
||||
# The first byte received and the top 1 bit of the second byte are
|
||||
# don't care bits (x). These are followed by a null bit (0), and then
|
||||
# the result bits (R). 10 bits for the MCP300x, 12 bits for the
|
||||
# MCP320x.
|
||||
#
|
||||
# XXX Differential mode still requires testing
|
||||
data = self._spi.transfer([16 + [8, 0][self.differential] + self.channel, 0, 0])
|
||||
return ((data[1] & 63) << (self.bits - 6)) | (data[2] >> (14 - self.bits))
|
||||
|
||||
|
||||
class MCP33xx(MCP3xxx):
|
||||
"""
|
||||
Extends :class:`MCP3xxx` with functionality specific to the MCP33xx family
|
||||
of ADCs; specifically this handles the full differential capability of
|
||||
these chips supporting the full 13-bit signed range of output values.
|
||||
"""
|
||||
|
||||
def __init__(self, channel=0, differential=False, **spi_args):
|
||||
super(MCP33xx, self).__init__(channel, 12, differential, **spi_args)
|
||||
|
||||
def _read(self):
|
||||
# MCP3304/02 protocol looks like the following:
|
||||
#
|
||||
# Byte 0 1 2
|
||||
# ==== ======== ======== ========
|
||||
# Tx 0001MCCC xxxxxxxx xxxxxxxx
|
||||
# Rx xxxxxxxx x0SRRRRR RRRRRRRx
|
||||
#
|
||||
# The transmit bits start with 3 preamble bits "000" (to warm up), a
|
||||
# start bit "1" followed by the single/differential bit (M) which is 1
|
||||
# for single-ended read, and 0 for differential read, followed by
|
||||
# 3-bits for the channel (C). The remainder of the transmission are
|
||||
# "don't care" bits (x).
|
||||
#
|
||||
# The first byte received and the top 1 bit of the second byte are
|
||||
# don't care bits (x). These are followed by a null bit (0), then the
|
||||
# sign bit (S), and then the 12 result bits (R).
|
||||
#
|
||||
# In single read mode (the default) the sign bit is always zero and the
|
||||
# result is effectively 12-bits. In differential mode, the sign bit is
|
||||
# significant and the result is a two's-complement 13-bit value.
|
||||
#
|
||||
# The MCP3301 variant of the chip always operates in differential
|
||||
# mode and effectively only has one channel (composed of an IN+ and
|
||||
# IN-). As such it requires no input, just output. This is the reason
|
||||
# we split out _send() below; so that MCP3301 can override it.
|
||||
data = self._spi.transfer(self._send())
|
||||
# Extract the last two bytes (again, for MCP3301)
|
||||
data = data[-2:]
|
||||
result = ((data[0] & 63) << 7) | (data[1] >> 1)
|
||||
# Account for the sign bit
|
||||
if self.differential and value > 4095:
|
||||
result = -(8192 - result)
|
||||
assert -4096 <= result < 4096
|
||||
return result
|
||||
|
||||
def _send(self):
|
||||
return [16 + [8, 0][self.differential] + self.channel, 0, 0]
|
||||
|
||||
|
||||
class MCP3001(MCP3xxx):
|
||||
"""
|
||||
The `MCP3001`_ is a 10-bit analog to digital converter with 1 channel
|
||||
|
||||
.. _MCP3001: http://www.farnell.com/datasheets/630400.pdf
|
||||
"""
|
||||
def __init__(self, **spi_args):
|
||||
super(MCP3001, self).__init__(0, 10, differential=True, **spi_args)
|
||||
|
||||
|
||||
class MCP3002(MCP3xxx):
|
||||
"""
|
||||
The `MCP3002`_ is a 10-bit analog to digital converter with 2 channels
|
||||
(0-3).
|
||||
|
||||
.. _MCP3002: http://www.farnell.com/datasheets/1599363.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, differential=False, **spi_args):
|
||||
if not 0 <= channel < 2:
|
||||
raise InputDeviceError('channel must be 0 or 1')
|
||||
super(MCP3002, self).__init__(channel, 10, differential, **spi_args)
|
||||
|
||||
|
||||
class MCP3004(MCP3xxx):
|
||||
"""
|
||||
The `MCP3004`_ is a 10-bit analog to digital converter with 4 channels
|
||||
(0-3).
|
||||
|
||||
.. _MCP3004: http://www.farnell.com/datasheets/808965.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, differential=False, **spi_args):
|
||||
if not 0 <= channel < 4:
|
||||
raise InputDeviceError('channel must be between 0 and 3')
|
||||
super(MCP3004, self).__init__(channel, 10, differential, **spi_args)
|
||||
|
||||
|
||||
class MCP3008(MCP3xxx):
|
||||
"""
|
||||
The `MCP3008`_ is a 10-bit analog to digital converter with 8 channels
|
||||
(0-7).
|
||||
|
||||
.. _MCP3008: http://www.farnell.com/datasheets/808965.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, differential=False, **spi_args):
|
||||
if not 0 <= channel < 8:
|
||||
raise InputDeviceError('channel must be between 0 and 7')
|
||||
super(MCP3008, self).__init__(channel, 10, differential, **spi_args)
|
||||
|
||||
|
||||
class MCP3201(MCP3xxx):
|
||||
"""
|
||||
The `MCP3201`_ is a 12-bit analog to digital converter with 1 channel
|
||||
|
||||
.. _MCP3201: http://www.farnell.com/datasheets/1669366.pdf
|
||||
"""
|
||||
def __init__(self, **spi_args):
|
||||
super(MCP3201, self).__init__(0, 12, differential=True, **spi_args)
|
||||
|
||||
|
||||
class MCP3202(MCP3xxx):
|
||||
"""
|
||||
The `MCP3202`_ is a 12-bit analog to digital converter with 2 channels
|
||||
(0-1).
|
||||
|
||||
.. _MCP3202: http://www.farnell.com/datasheets/1669376.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, differential=False, **spi_args):
|
||||
if not 0 <= channel < 2:
|
||||
raise InputDeviceError('channel must be 0 or 1')
|
||||
super(MCP3202, self).__init__(channel, 12, differential, **spi_args)
|
||||
|
||||
|
||||
class MCP3204(MCP3xxx):
|
||||
"""
|
||||
The `MCP3204`_ is a 12-bit analog to digital converter with 4 channels
|
||||
(0-3).
|
||||
|
||||
.. _MCP3204: http://www.farnell.com/datasheets/808967.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, differential=False, **spi_args):
|
||||
if not 0 <= channel < 4:
|
||||
raise InputDeviceError('channel must be between 0 and 3')
|
||||
super(MCP3204, self).__init__(channel, 12, differential, **spi_args)
|
||||
|
||||
|
||||
class MCP3208(MCP3xxx):
|
||||
"""
|
||||
The `MCP3208`_ is a 12-bit analog to digital converter with 8 channels
|
||||
(0-7).
|
||||
|
||||
.. _MCP3208: http://www.farnell.com/datasheets/808967.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, differential=False, **spi_args):
|
||||
if not 0 <= channel < 8:
|
||||
raise InputDeviceError('channel must be between 0 and 7')
|
||||
super(MCP3208, self).__init__(channel, 12, differential, **spi_args)
|
||||
|
||||
|
||||
class MCP3301(MCP33xx):
|
||||
"""
|
||||
The `MCP3301`_ is a signed 13-bit analog to digital converter. Please note
|
||||
that the MCP3301 always operates in differential mode between its two
|
||||
channels and the output value is scaled from -1 to +1.
|
||||
|
||||
.. _MCP3301: http://www.farnell.com/datasheets/1669397.pdf
|
||||
"""
|
||||
def __init__(self, **spi_args):
|
||||
super(MCP3301, self).__init__(0, differential=True, **spi_args)
|
||||
|
||||
def _send(self):
|
||||
return [0, 0]
|
||||
|
||||
|
||||
class MCP3302(MCP33xx):
|
||||
"""
|
||||
The `MCP3302`_ is a 12/13-bit analog to digital converter with 4 channels
|
||||
(0-3). When operated in differential mode, the device outputs a signed
|
||||
13-bit value which is scaled from -1 to +1. When operated in single-ended
|
||||
mode (the default), the device outputs an unsigned 12-bit value scaled from
|
||||
0 to 1.
|
||||
|
||||
.. _MCP3302: http://www.farnell.com/datasheets/1486116.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, differential=False, **spi_args):
|
||||
if not 0 <= channel < 4:
|
||||
raise InputDeviceError('channel must be between 0 and 4')
|
||||
super(MCP3302, self).__init__(channel, differential, **spi_args)
|
||||
|
||||
|
||||
class MCP3304(MCP33xx):
|
||||
"""
|
||||
The `MCP3304`_ is a 12/13-bit analog to digital converter with 8 channels
|
||||
(0-7). When operated in differential mode, the device outputs a signed
|
||||
13-bit value which is scaled from -1 to +1. When operated in single-ended
|
||||
mode (the default), the device outputs an unsigned 12-bit value scaled from
|
||||
0 to 1.
|
||||
|
||||
.. _MCP3304: http://www.farnell.com/datasheets/1486116.pdf
|
||||
"""
|
||||
def __init__(self, channel=0, differential=False, **spi_args):
|
||||
if not 0 <= channel < 8:
|
||||
raise InputDeviceError('channel must be between 0 and 7')
|
||||
super(MCP3304, self).__init__(channel, differential, **spi_args)
|
||||
|
||||
92
gpiozero/threads.py
Normal file
92
gpiozero/threads.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from __future__ import (
|
||||
unicode_literals,
|
||||
print_function,
|
||||
absolute_import,
|
||||
division,
|
||||
)
|
||||
str = type('')
|
||||
|
||||
import weakref
|
||||
from collections import deque
|
||||
from threading import Thread, Event, RLock
|
||||
try:
|
||||
from statistics import median, mean
|
||||
except ImportError:
|
||||
from .compat import median, mean
|
||||
|
||||
from .exc import (
|
||||
GPIOBadQueueLen,
|
||||
GPIOBadSampleWait,
|
||||
)
|
||||
|
||||
|
||||
_THREADS = set()
|
||||
def _threads_shutdown():
|
||||
while _THREADS:
|
||||
for t in _THREADS.copy():
|
||||
t.stop()
|
||||
|
||||
|
||||
class GPIOThread(Thread):
|
||||
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
|
||||
super(GPIOThread, self).__init__(group, target, name, args, kwargs)
|
||||
self.stopping = Event()
|
||||
self.daemon = True
|
||||
|
||||
def start(self):
|
||||
self.stopping.clear()
|
||||
_THREADS.add(self)
|
||||
super(GPIOThread, self).start()
|
||||
|
||||
def stop(self):
|
||||
self.stopping.set()
|
||||
self.join()
|
||||
|
||||
def join(self):
|
||||
super(GPIOThread, self).join()
|
||||
_THREADS.discard(self)
|
||||
|
||||
|
||||
class GPIOQueue(GPIOThread):
|
||||
def __init__(
|
||||
self, parent, queue_len=5, sample_wait=0.0, partial=False,
|
||||
average=median):
|
||||
assert isinstance(parent, GPIODevice)
|
||||
assert callable(average)
|
||||
super(GPIOQueue, self).__init__(target=self.fill)
|
||||
if queue_len < 1:
|
||||
raise GPIOBadQueueLen('queue_len must be at least one')
|
||||
if sample_wait < 0:
|
||||
raise GPIOBadSampleWait('sample_wait must be 0 or greater')
|
||||
self.queue = deque(maxlen=queue_len)
|
||||
self.partial = partial
|
||||
self.sample_wait = sample_wait
|
||||
self.full = Event()
|
||||
self.parent = weakref.proxy(parent)
|
||||
self.average = average
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
if not self.partial:
|
||||
self.full.wait()
|
||||
try:
|
||||
return self.average(self.queue)
|
||||
except ZeroDivisionError:
|
||||
# No data == inactive value
|
||||
return 0.0
|
||||
|
||||
def fill(self):
|
||||
try:
|
||||
while (not self.stopping.wait(self.sample_wait) and
|
||||
len(self.queue) < self.queue.maxlen):
|
||||
self.queue.append(self.parent._read())
|
||||
if self.partial:
|
||||
self.parent._fire_events()
|
||||
self.full.set()
|
||||
while not self.stopping.wait(self.sample_wait):
|
||||
self.queue.append(self.parent._read())
|
||||
self.parent._fire_events()
|
||||
except ReferenceError:
|
||||
# Parent is dead; time to die!
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user