Refactor low level implementation

This commit is a fairly major piece of work that abstracts all pin
operations (function, state, edge detection, PWM, etc.) into a base
"Pin" class which is then used by input/output/composite devices to
perform all required configuration.

The idea is to pave the way for I2C based IO extenders which can present
additional GPIO ports with similar capabilities to the Pi's "native"
GPIO ports. As a bonus it also abstracts away the reliance on the
RPi.GPIO library to allow alternative pin implementations (e.g. using
RPIO to take advantage of DMA based PWM), or even pure Python
implementations.
This commit is contained in:
Dave Jones
2015-11-19 11:55:16 +00:00
parent c7ee499989
commit 8e0c6e243b
14 changed files with 1231 additions and 133 deletions

84
docs/api_pins.rst Normal file
View File

@@ -0,0 +1,84 @@
====
Pins
====
.. currentmodule:: gpiozero
As of release 1.1, the GPIO Zero library can be roughly divided into two
things: pins and the devices that are connected to them. The majority of the
documentation focuses on devices as pins are below the level that most users
are concerned with. However, some users may wish to take advantage of the
capabilities of alternative GPIO implementations or (in future) use GPIO
extender chips. This is the purpose of the pins portion of the library.
When you construct a device, you pass in a GPIO pin number. However, what the
library actually expects is a :class:`Pin` implementation. If it finds a simple
integer number instead, it uses one of the following classes to provide the
:class:`Pin` implementation (classes are listed in favoured order):
1. :class:`gpiozero.pins.rpigpio.RPiGPIOPin`
2. :class:`gpiozero.pins.rpio.RPIOPin`
3. :class:`gpiozero.pins.native.NativePin`
You can change the default pin implementation by over-writing the
``DefaultPin`` global in devices like so::
from gpiozero.pins.native import NativePin
import gpiozero.devices
# Force the default pin implementation to be NativePin
gpiozero.devices.DefaultPin = NativePin
from gpiozero import LED
# This will now use NativePin instead of RPiGPIOPin
led = LED(16)
In future, this separation should allow the library to utilize pins that are
part of IO extender chips. For example::
from gpiozero import IOExtender, LED
ext = IOExtender()
led = LED(ext.pins[0])
led.on()
.. warning::
While the devices API is now considered stable and won't change in
backwards incompatible ways, the pins API is *not* yet considered stable.
It is potentially subject to change in future versions. We welcome any
comments from testers!
Abstract Pin
============
.. autoclass:: Pin
:members:
RPiGPIOPin
==========
.. currentmodule:: gpiozero.pins.rpigpio
.. autoclass:: RPiGPIOPin
RPIOPin
=======
.. currentmodule:: gpiozero.pins.rpio
.. autoclass:: RPIOPin
NativePin
=========
.. currentmodule:: gpiozero.pins.native
.. autoclass:: NativePin

View File

@@ -37,6 +37,8 @@ class Mock(object):
sys.modules['RPi'] = Mock()
sys.modules['RPi.GPIO'] = sys.modules['RPi'].GPIO
sys.modules['RPIO'] = Mock()
sys.modules['RPIO.PWM'] = sys.modules['RPIO'].PWM
sys.modules['w1thermsensor'] = Mock()
sys.modules['spidev'] = Mock()

View File

@@ -12,5 +12,6 @@ Table of Contents
api_output
api_boards
api_generic
api_pins
changelog
license

View File

@@ -281,7 +281,7 @@ Each button plays a different sound!
buttons = [Button(pin) for pin in sound_pins]
for button in buttons:
sound = sound_pins[button.pin]
sound = sound_pins[button.pin.number]
button.when_pressed = sound.play
pause()

View File

@@ -5,6 +5,23 @@ from __future__ import (
division,
)
from .pins.exc import (
PinError,
PinFixedFunction,
PinInvalidFunction,
PinInvalidState,
PinInvalidPull,
PinInvalidEdges,
PinSetInput,
PinFixedPull,
PinEdgeDetectUnsupported,
PinPWMError,
PinPWMUnsupported,
PinPWMFixedValue,
)
from .pins import (
Pin,
)
from .exc import (
GPIODeviceClosed,
GPIODeviceError,

View File

@@ -13,29 +13,45 @@ from threading import Thread, Event, RLock
from collections import deque
from types import FunctionType
from RPi import GPIO
from .exc import GPIODeviceError, GPIODeviceClosed, InputDeviceError
_GPIO_THREADS = set()
_GPIO_PINS = set()
# Get a pin implementation to use as the default; we prefer RPi.GPIO's here
# as it supports PWM, and all Pi revisions. If no third-party libraries are
# available, however, we fall back to a pure Python implementation which
# supports platforms like PyPy
from .pins import PINS_CLEANUP
try:
from .pins.rpigpio import RPiGPIOPin
DefaultPin = RPiGPIOPin
except ImportError:
try:
from .pins.rpio import RPIOPin
DefaultPin = RPIOPin
except ImportError:
from .pins.native import NativePin
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
_GPIO_PINS_LOCK = RLock()
_PINS_LOCK = RLock()
def _gpio_threads_shutdown():
while _GPIO_THREADS:
for t in _GPIO_THREADS.copy():
def _shutdown():
while _THREADS:
for t in _THREADS.copy():
t.stop()
with _GPIO_PINS_LOCK:
while _GPIO_PINS:
GPIO.remove_event_detect(_GPIO_PINS.pop())
GPIO.cleanup()
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()
atexit.register(_gpio_threads_shutdown)
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
atexit.register(_shutdown)
class GPIOMeta(type):
@@ -196,20 +212,22 @@ class GPIODevice(ValuesMixin, GPIOBase):
# value of pin until we've verified that it isn't already allocated
self._pin = None
if pin is None:
raise GPIODeviceError('No GPIO pin number given')
with _GPIO_PINS_LOCK:
if pin in _GPIO_PINS:
raise GPIODeviceError('No pin given')
if isinstance(pin, int):
pin = DefaultPin(pin)
with _PINS_LOCK:
if pin in _PINS:
raise GPIODeviceError(
'pin %d is already in use by another gpiozero object' % pin
'pin %r is already in use by another gpiozero object' % pin
)
_GPIO_PINS.add(pin)
_PINS.add(pin)
self._pin = pin
self._active_state = GPIO.HIGH
self._inactive_state = GPIO.LOW
self._active_state = True
self._inactive_state = False
def _read(self):
try:
return GPIO.input(self.pin) == self._active_state
return self.pin.state == self._active_state
except TypeError:
self._check_open()
raise
@@ -260,13 +278,12 @@ class GPIODevice(ValuesMixin, GPIOBase):
...
"""
super(GPIODevice, self).close()
with _GPIO_PINS_LOCK:
with _PINS_LOCK:
pin = self._pin
self._pin = None
if pin in _GPIO_PINS:
_GPIO_PINS.remove(pin)
GPIO.remove_event_detect(pin)
GPIO.cleanup(pin)
if pin in _PINS:
_PINS.remove(pin)
pin.close()
@property
def closed(self):
@@ -275,9 +292,10 @@ class GPIODevice(ValuesMixin, GPIOBase):
@property
def pin(self):
"""
The pin (in BCM numbering) that the device is connected to. This will
be ``None`` if the device has been closed (see the :meth:`close`
method).
The :class:`Pin` that the device is connected to. This will be ``None``
if the device has been closed (see the :meth:`close` method). When
dealing with GPIO pins, query ``pin.number`` to discover the GPIO
pin (in BCM numbering) that the device is connected to.
"""
return self._pin
@@ -293,7 +311,7 @@ class GPIODevice(ValuesMixin, GPIOBase):
def __repr__(self):
try:
return "<gpiozero.%s object on pin=%d, is_active=%s>" % (
return "<gpiozero.%s object on pin %r, is_active=%s>" % (
self.__class__.__name__, self.pin, self.is_active)
except GPIODeviceClosed:
return "<gpiozero.%s object closed>" % self.__class__.__name__
@@ -307,7 +325,7 @@ class GPIOThread(Thread):
def start(self):
self.stopping.clear()
_GPIO_THREADS.add(self)
_THREADS.add(self)
super(GPIOThread, self).start()
def stop(self):
@@ -316,7 +334,7 @@ class GPIOThread(Thread):
def join(self):
super(GPIOThread, self).join()
_GPIO_THREADS.discard(self)
_THREADS.discard(self)
class GPIOQueue(GPIOThread):

View File

@@ -13,7 +13,6 @@ from functools import wraps
from time import sleep, time
from threading import Event
from RPi import GPIO
from spidev import SpiDev
from .exc import InputDeviceError, GPIODeviceError, GPIODeviceClosed
@@ -39,39 +38,18 @@ class InputDevice(GPIODevice):
``False`` (the default), the pin will be pulled low.
"""
def __init__(self, pin=None, pull_up=False):
if pin in (2, 3) and not pull_up:
raise InputDeviceError(
'GPIO pins 2 and 3 are fitted with physical pull up '
'resistors; you cannot initialize them with pull_up=False'
)
# _pull_up should be assigned first as __repr__ relies upon it to
# support the case where __repr__ is called during debugging of an
# instance that has failed to initialize (due to an exception in the
# super-class __init__)
self._pull_up = pull_up
super(InputDevice, self).__init__(pin)
self._active_edge = GPIO.FALLING if pull_up else GPIO.RISING
self._inactive_edge = GPIO.RISING if pull_up else GPIO.FALLING
self._active_state = GPIO.LOW if pull_up else GPIO.HIGH
self._inactive_state = GPIO.HIGH if pull_up else GPIO.LOW
pull = GPIO.PUD_UP if pull_up else GPIO.PUD_DOWN
try:
# NOTE: catch_warnings isn't thread-safe but hopefully no-one's
# messing around with GPIO init within background threads...
with warnings.catch_warnings(record=True) as w:
GPIO.setup(pin, GPIO.IN, pull)
# The only warning we want to squash is a RuntimeWarning that is
# thrown when setting pins 2 or 3. Anything else should be replayed
for warning in w:
if warning.category != RuntimeWarning or pin not in (2, 3):
warnings.showwarning(
warning.message, warning.category, warning.filename,
warning.lineno, warning.file, warning.line
)
if self.pin.function != 'input':
self.pin.function = 'input'
pull = 'up' if pull_up else 'down'
if self.pin.pull != pull:
self.pin.pull = pull
except:
self.close()
raise
self._active_state = False if pull_up else True
self._inactive_state = True if pull_up else False
@property
def pull_up(self):
@@ -79,11 +57,11 @@ class InputDevice(GPIODevice):
If ``True``, the device uses a pull-up resistor to set the GPIO pin
"high" by default. Defaults to ``False``.
"""
return self._pull_up
return self.pin.pull == 'up'
def __repr__(self):
try:
return "<gpiozero.%s object on pin=%d, pull_up=%s, is_active=%s>" % (
return "<gpiozero.%s object on pin %r, pull_up=%s, is_active=%s>" % (
self.__class__.__name__, self.pin, self.pull_up, self.is_active)
except:
return super(InputDevice, self).__repr__()
@@ -245,20 +223,15 @@ class DigitalInputDevice(WaitableInputDevice):
def __init__(self, pin=None, pull_up=False, bounce_time=None):
super(DigitalInputDevice, self).__init__(pin, pull_up)
try:
# Yes, that's really the default bouncetime in RPi.GPIO...
GPIO.add_event_detect(
self.pin, GPIO.BOTH, callback=self._fire_events,
bouncetime=-666 if bounce_time is None else int(bounce_time * 1000)
)
self.pin.bounce = bounce_time
self.pin.edges = 'both'
self.pin.when_changed = self._fire_events
# Call _fire_events once to set initial state of events
super(DigitalInputDevice, self)._fire_events()
self._fire_events()
except:
self.close()
raise
def _fire_events(self, channel):
super(DigitalInputDevice, self)._fire_events()
class SmoothedInputDevice(WaitableInputDevice):
"""
@@ -565,9 +538,9 @@ class LightSensor(SmoothedInputDevice):
try:
self._charge_time_limit = charge_time_limit
self._charged = Event()
GPIO.add_event_detect(
self.pin, GPIO.RISING, lambda channel: self._charged.set()
)
self.pin.edges = 'rising'
self.pin.bounce = None
self.pin.when_changed = self._charged.set
self._queue.start()
except:
self.close()
@@ -579,13 +552,13 @@ class LightSensor(SmoothedInputDevice):
def _read(self):
# Drain charge from the capacitor
GPIO.setup(self.pin, GPIO.OUT)
GPIO.output(self.pin, GPIO.LOW)
self.pin.function = 'output'
self.pin.state = False
sleep(0.1)
# Time the charging of the capacitor
start = time()
self._charged.clear()
GPIO.setup(self.pin, GPIO.IN)
self.pin.function = 'input'
self._charged.wait(self.charge_time_limit)
return (
1.0 - min(self.charge_time_limit, time() - start) /

View File

@@ -10,8 +10,6 @@ from time import sleep
from threading import Lock
from itertools import repeat, cycle, chain
from RPi import GPIO
from .exc import OutputDeviceError, GPIODeviceError, GPIODeviceClosed
from .devices import GPIODevice, GPIOThread, CompositeDevice, SourceMixin
@@ -42,36 +40,20 @@ class OutputDevice(SourceMixin, GPIODevice):
def __init__(self, pin=None, active_high=True, initial_value=False):
self._active_high = active_high
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
try:
# NOTE: catch_warnings isn't thread-safe but hopefully no-one's
# messing around with GPIO init within background threads...
with warnings.catch_warnings(record=True) as w:
# This is horrid, but we can't specify initial=None with setup
self._active_state = True if active_high else False
self._inactive_state = False if active_high else True
if initial_value is None:
GPIO.setup(pin, GPIO.OUT)
self.pin.function = 'output'
elif initial_value:
self.pin.output_with_state(self._active_state)
else:
GPIO.setup(pin, GPIO.OUT, initial=
[self._inactive_state, self._active_state][bool(initial_value)])
GPIO.setup(pin, GPIO.OUT)
# The only warning we want to squash is a RuntimeWarning that is
# thrown when setting pins 2 or 3. Anything else should be replayed
for warning in w:
if warning.category != RuntimeWarning or pin not in (2, 3):
warnings.showwarning(
warning.message, warning.category, warning.filename,
warning.lineno, warning.file, warning.line
)
except:
self.close()
raise
self.pin.output_with_state(self._inactive_state)
def _write(self, value):
if not self.active_high:
value = not value
try:
GPIO.output(self.pin, bool(value))
self.pin.state = bool(value)
except ValueError:
self._check_open()
raise
@@ -102,7 +84,7 @@ class OutputDevice(SourceMixin, GPIODevice):
def __repr__(self):
try:
return '<gpiozero.%s object on pin=%d, active_high=%s, is_active=%s>' % (
return '<gpiozero.%s object on pin %r, active_high=%s, is_active=%s>' % (
self.__class__.__name__, self.pin, self.active_high, self.is_active)
except:
return super(OutputDevice, self).__repr__()
@@ -285,40 +267,33 @@ class PWMOutputDevice(OutputDevice):
to 100Hz.
"""
def __init__(self, pin=None, active_high=True, initial_value=0, frequency=100):
self._pwm = None
self._blink_thread = None
if not 0 <= initial_value <= 1:
raise OutputDeviceError("initial_value must be between 0 and 1")
super(PWMOutputDevice, self).__init__(pin, active_high)
try:
self._pwm = GPIO.PWM(self.pin, frequency)
self._value = initial_value
if not active_high:
initial_value = 1 - initial_value
self._pwm.start(100 * initial_value)
self._frequency = frequency
# XXX need a way of setting these together
self.pin.frequency = frequency
self.value = initial_value
except:
self.close()
raise
def close(self):
self._stop_blink()
if self._pwm:
# Ensure we wipe out the PWM object so that re-runs don't attempt
# to re-stop the PWM thread (otherwise, the fact that close is
# called from __del__ can easily result in us stopping the PWM
# on *another* instance on the same pin)
p = self._pwm
self._pwm = None
p.stop()
try:
self.pin.frequency = None
except AttributeError:
# If the pin's already None, ignore the exception
pass
super(PWMOutputDevice, self).close()
def _read(self):
self._check_open()
if self.active_high:
return self._value
return self.pin.state
else:
return 1 - self._value
return 1 - self.pin.state
def _write(self, value):
if not self.active_high:
@@ -326,11 +301,10 @@ class PWMOutputDevice(OutputDevice):
if not 0 <= value <= 1:
raise OutputDeviceError("PWM value must be between 0 and 1")
try:
self._pwm.ChangeDutyCycle(value * 100)
self.pin.state = value
except AttributeError:
self._check_open()
raise
self._value = value
@property
def value(self):
@@ -361,7 +335,7 @@ class PWMOutputDevice(OutputDevice):
toggle it to 0.9, and so on.
"""
self._stop_blink()
self.value = 1.0 - self.value
self.value = 1 - self.value
@property
def is_active(self):
@@ -377,12 +351,11 @@ class PWMOutputDevice(OutputDevice):
The frequency of the pulses used with the PWM device, in Hz. The
default is 100Hz.
"""
return self._frequency
return self.pin.frequency
@frequency.setter
def frequency(self, value):
self._pwm.ChangeFrequency(value)
self._frequency = value
self.pin.frequency = value
def blink(
self, on_time=1, off_time=1, fade_in_time=0, fade_out_time=0,

241
gpiozero/pins/__init__.py Normal file
View File

@@ -0,0 +1,241 @@
from __future__ import (
unicode_literals,
absolute_import,
print_function,
division,
)
str = type('')
from .exc import (
PinFixedFunction,
PinSetInput,
PinFixedPull,
PinPWMUnsupported,
PinEdgeDetectUnsupported,
)
PINS_CLEANUP = []
class Pin(object):
"""
Abstract base class representing a GPIO pin or a pin from an IO extender.
Descendents should override property getters and setters to accurately
represent the capabilities of pins. The following functions *must* be
overridden:
* :meth:`_get_function`
* :meth:`_get_state`
The following functions *may* be overridden if applicable:
* :meth:`close`
* :meth:`_set_function`
* :meth:`_set_state`
* :meth:`_get_frequency`
* :meth:`_set_frequency`
* :meth:`_get_pull`
* :meth:`_set_pull`
* :meth:`_get_bounce`
* :meth:`_set_bounce`
* :meth:`_get_edges`
* :meth:`_set_edges`
* :meth:`_get_when_changed`
* :meth:`_set_when_changed`
* :meth:`output_with_state`
* :meth:`input_with_pull`
.. warning::
Descendents must ensure that pin instances representing the same
physical hardware are identical, right down to object identity. The
framework relies on this to correctly clean up resources at interpreter
shutdown.
"""
def __repr__(self):
return "Abstract pin"
def close(self):
"""
Cleans up the resources allocated to the pin. After this method is
called, this :class:`Pin` instance may no longer be used to query or
control the pin's state.
"""
pass
def output_with_state(self, state):
"""
Sets the pin's function to "output" and specifies an initial state
for the pin. By default this is equivalent to performing::
pin.function = 'output'
pin.state = state
However, descendents may override this in order to provide the smallest
possible delay between configuring the pin for output and specifying an
initial value (which can be important for avoiding "blips" in
active-low configurations).
"""
self.function = 'output'
self.state = state
def input_with_pull(self, pull):
"""
Sets the pin's function to "input" and specifies an initial pull-up
for the pin. By default this is equivalent to performing::
pin.function = 'input'
pin.pull = pull
However, descendents may override this order to provide the smallest
possible delay between configuring the pin for input and pulling the
pin up/down (which can be important for avoiding "blips" in some
configurations).
"""
self.function = 'input'
self.pull = pull
def _get_function(self):
return "input"
def _set_function(self, value):
raise PinFixedFunction("Cannot set the function of pin %r" % self)
function = property(
lambda self: self._get_function(),
lambda self, value: self._set_function(value),
doc="""\
The function of the pin. This property is a string indicating the
current function or purpose of the pin. Typically this is the string
"input" or "output". However, in some circumstances it can be other
strings indicating non-GPIO related functionality.
With certain pin types (e.g. GPIO pins), this attribute can be changed
to configure the function of a pin. If an invalid function is
specified, for this attribute, :exc:`PinInvalidFunction` will be
raised. If this pin is fixed function and an attempt is made to set
this attribute, :exc:`PinFixedFunction` will be raised.
""")
def _get_state(self):
return 0
def _set_state(self, value):
raise PinSetInput("Cannot set the state of input pin %r" % self)
state = property(
lambda self: self._get_state(),
lambda self, value: self._set_state(value),
doc="""\
The state of the pin. This is 0 for low, and 1 for high. As a low level
view of the pin, no swapping is performed in the case of pull ups (see
:attr:`pull` for more information).
If PWM is currently active (when :attr:`frequency` is not ``None``),
this represents the PWM duty cycle as a value between 0.0 and 1.0.
If a pin is currently configured for input, and an attempt is made to
set this attribute, :exc:`PinSetInput` will be raised. If an invalid
value is specified for this attribute, :exc:`PinInvalidState` will be
raised.
""")
def _get_pull(self):
return 'floating'
def _set_pull(self, value):
raise PinFixedPull("Cannot change pull-up on pin %r" % self)
pull = property(
lambda self: self._get_pull(),
lambda self, value: self._set_pull(value),
doc="""\
The pull-up state of the pin represented as a string. This is typically
one of the strings "up", "down", or "floating" but additional values
may be supported by the underlying hardware.
If the pin does not support changing pull-up state (for example because
of a fixed pull-up resistor), attempts to set this property will raise
:exc:`PinFixedPull`. If the specified value is not supported by the
underlying hardware, :exc:`PinInvalidPull` is raised.
""")
def _get_frequency(self):
return None
def _set_frequency(self, value):
if value is not None:
raise PinPWMUnsupported("PWM is not supported on pin %r" % self)
frequency = property(
lambda self: self._get_frequency(),
lambda self, value: self._set_frequency(value),
doc="""\
The frequency (in Hz) for the pin's PWM implementation, or ``None`` if
PWM is not currently in use. This value always defaults to ``None`` and
may be changed with certain pin types to activate or deactivate PWM.
If the pin does not support PWM, :exc:`PinPWMUnsupported` will be
raised when attempting to set this to a value other than ``None``.
""")
def _get_bounce(self):
return None
def _set_bounce(self, value):
if value is not None:
raise PinEdgeDetectUnsupported("Edge detection is not supported on pin %r" % self)
bounce = property(
lambda self: self._get_bounce(),
lambda self, value: self._set_bounce(value),
doc="""\
The amount of bounce detection (elimination) currently in use by edge
detection, measured in seconds. If bounce detection is not currently in
use, this is ``None``.
If the pin does not support edge detection, attempts to set this
property will raise :exc:`PinEdgeDetectUnsupported`. If the pin
supports edge detection, the class must implement bounce detection,
even if only in software.
""")
def _get_edges(self):
return 'both'
def _set_edges(self, value):
raise PinEdgeDetectUnsupported("Edge detection is not supported on pin %r" % self)
edges = property(
lambda self: self._get_edges(),
lambda self, value: self._set_edges(value),
doc="""\
The edge that will trigger execution of the function or bound method
assigned to :attr:`when_changed`. This can be one of the strings
"both" (the default), "rising", or "falling".
If the pin does not support edge detection, attempts to set this
property will raise :exc:`PinEdgeDetectUnsupported`.
""")
def _get_when_changed(self):
return None
def _set_when_changed(self, value):
raise PinEdgeDetectUnsupported("Edge detection is not supported on pin %r" % self)
when_changed = property(
lambda self: self._get_when_changed(),
lambda self, value: self._set_when_changed(value),
doc="""\
A function or bound method to be called when the pin's state changes
(more specifically when the edge specified by :attr:`edges` is detected
on the pin). The function or bound method must take no parameters.
If the pin does not support edge detection, attempts to set this
property will raise :exc:`PinEdgeDetectUnsupported`.
""")

45
gpiozero/pins/exc.py Normal file
View File

@@ -0,0 +1,45 @@
from __future__ import (
unicode_literals,
absolute_import,
print_function,
division,
)
str = type('')
class PinError(Exception):
"Base class for errors related to pin implementations"
class PinFixedFunction(PinError, AttributeError):
"Error raised when attempting to change the function of a fixed type pin"
class PinInvalidFunction(PinError, ValueError):
"Error raised when attempting to change the function of a pin to an invalid value"
class PinInvalidState(PinError, ValueError):
"Error raised when attempting to assign an invalid state to a pin"
class PinInvalidPull(PinError, ValueError):
"Error raised when attempting to assign an invalid pull-up to a pin"
class PinInvalidEdges(PinError, ValueError):
"Error raised when attempting to assign an invalid edge detection to a pin"
class PinSetInput(PinError, AttributeError):
"Error raised when attempting to set a read-only pin"
class PinFixedPull(PinError, AttributeError):
"Error raised when attempting to set the pull of a pin with fixed pull-up"
class PinEdgeDetectUnsupported(PinError, AttributeError):
"Error raised when attempting to use edge detection on unsupported pins"
class PinPWMError(PinError):
"Base class for errors related to PWM implementations"
class PinPWMUnsupported(PinPWMError, AttributeError):
"Error raised when attempting to activate PWM on unsupported pins"
class PinPWMFixedValue(PinPWMError, AttributeError):
"Error raised when attempting to initialize PWM on an input pin"

336
gpiozero/pins/native.py Normal file
View File

@@ -0,0 +1,336 @@
from __future__ import (
unicode_literals,
absolute_import,
print_function,
division,
)
nstr = str
str = type('')
import io
import os
import mmap
import errno
import struct
from time import sleep
from threading import Thread, Event, Lock
from collections import Counter
from . import Pin, PINS_CLEANUP
from .exc import (
PinInvalidPull,
PinInvalidEdges,
PinInvalidFunction,
PinFixedPull,
)
class GPIOMemory(object):
GPIO_BASE_OFFSET = 0x200000
PERI_BASE_OFFSET = {
'BCM2708': 0x20000000,
'BCM2835': 0x20000000,
'BCM2709': 0x3f000000,
'BCM2836': 0x3f000000,
}
# From BCM2835 data-sheet, p.91
GPFSEL_OFFSET = 0x00 >> 2
GPSET_OFFSET = 0x1c >> 2
GPCLR_OFFSET = 0x28 >> 2
GPLEV_OFFSET = 0x34 >> 2
GPEDS_OFFSET = 0x40 >> 2
GPREN_OFFSET = 0x4c >> 2
GPFEN_OFFSET = 0x58 >> 2
GPHEN_OFFSET = 0x64 >> 2
GPLEN_OFFSET = 0x70 >> 2
GPAREN_OFFSET = 0x7c >> 2
GPAFEN_OFFSET = 0x88 >> 2
GPPUD_OFFSET = 0x94 >> 2
GPPUDCLK_OFFSET = 0x98 >> 2
def __init__(self):
try:
self.fd = os.open('/dev/gpiomem', os.O_RDWR | os.O_SYNC)
except OSError:
try:
self.fd = os.open('/dev/mem', os.O_RDWR | os.O_SYNC)
except OSError:
raise IOError(
'unable to open /dev/gpiomem or /dev/mem; '
'upgrade your kernel or run as root')
else:
offset = self.peripheral_base() + self.GPIO_BASE_OFFSET
else:
offset = 0
self.mem = mmap.mmap(self.fd, 4096, offset=offset)
def close(self):
self.mem.close()
os.close(self.fd)
def peripheral_base(self):
try:
with io.open('/proc/device-tree/soc/ranges', 'rb') as f:
f.seek(4)
return struct.unpack(nstr('>L'), f.read(4))[0]
except IOError:
with io.open('/proc/cpuinfo', 'r') as f:
for line in f:
if line.startswith('Hardware'):
try:
return self.PERI_BASE_OFFSET[line.split(':')[1].strip()]
except KeyError:
raise IOError('unable to determine RPi revision')
raise IOError('unable to determine peripheral base')
def __getitem__(self, index):
return struct.unpack_from(nstr('<L'), self.mem, index * 4)[0]
def __setitem__(self, index, value):
struct.pack_into(nstr('<L'), self.mem, index * 4, value)
class GPIOFS(object):
GPIO_PATH = '/sys/class/gpio'
def __init__(self):
self._lock = Lock()
self._pin_refs = Counter()
def path(self, name):
return os.path.join(self.GPIO_PATH, name)
def export(self, pin):
with self._lock:
if self._pin_refs[pin] == 0:
# Set the count to 1 to indicate the GPIO is already exported
# (we'll correct this if we find it isn't, but this enables us
# to "leave the system in the state we found it")
self._pin_refs[pin] = 1
result = None
# Dirty hack to wait for udev to set permissions on
# gpioN/direction; there's no other way around this as there's
# no synchronous mechanism for setting permissions on sysfs
for i in range(10):
try:
result = io.open(self.path('gpio%d/value' % pin), 'w+b', buffering=0)
except IOError as e:
if e.errno == errno.ENOENT:
with io.open(self.path('export'), 'wb') as f:
f.write(str(pin).encode('ascii'))
# Pin wasn't exported, so correct the ref-count
self._pin_refs[pin] = 0
elif e.errno == errno.EACCES:
sleep(i / 100)
else:
raise
else:
break
if not result:
raise RuntimeError('failed to export pin %d' % pin)
else:
result = io.open(self.path('gpio%d/value' % pin), 'w+b', buffering=0)
self._pin_refs[pin] += 1
return result
def unexport(self, pin):
with self._lock:
self._pin_refs[pin] -= 1
if self._pin_refs[pin] == 0:
with io.open(self.path('unexport'), 'wb') as f:
f.write(str(pin).encode('ascii'))
class NativePin(Pin):
"""
Uses a built-in pure Python implementation to interface to the Pi's GPIO
pins. This is the default pin implementation if no third-party libraries
are discovered.
.. warning::
This implementation does *not* currently support PWM. Attempting to
use any class which requests PWM will raise an exception. This
implementation is also experimental; we make no guarantees it will
not eat your Pi for breakfast!
"""
_MEM = None
_PINS = {}
PULL_NAMES = {
0b00: 'floating',
0b01: 'down',
0b10: 'up',
0b11: 'reserved',
}
FUNCTION_NAMES = {
0b000: 'input',
0b001: 'output',
0b100: 'alt0',
0b101: 'alt1',
0b110: 'alt2',
0b111: 'alt3',
0b011: 'alt4',
0b010: 'alt5',
}
EDGE_NAMES = {
(True, True): 'both',
(True, False): 'rising',
(False, True): 'falling',
(False, False): 'none',
}
PULL_VALUES = {v: k for (k, v) in PULL_NAMES.items()}
FUNCTION_VALUES = {v: k for (k, v) in FUNCTION_NAMES.items()}
EDGE_VALUES = {v: k for (k, v) in EDGE_NAMES.items()}
def __new__(cls, number):
if not cls._PINS:
cls._MEM = GPIOMemory()
PINS_CLEANUP.append(cls._MEM.close)
if not (0 <= number < 54):
raise ValueError('invalid pin %d specified (must be 0..53)' % number)
try:
return cls._PINS[number]
except KeyError:
self = super(NativePin, cls).__new__(cls)
cls._PINS[number] = self
self._number = number
self._func_offset = self._MEM.GPFSEL_OFFSET + (number // 10)
self._func_shift = (number % 10) * 3
self._set_offset = self._MEM.GPSET_OFFSET + (number // 32)
self._set_shift = number % 32
self._clear_offset = self._MEM.GPCLR_OFFSET + (number // 32)
self._clear_shift = number % 32
self._level_offset = self._MEM.GPLEV_OFFSET + (number // 32)
self._level_shift = number % 32
self._pull_offset = self._MEM.GPPUDCLK_OFFSET + (number // 32)
self._pull_shift = number % 32
self._edge_offset = self._MEM.GPEDS_OFFSET + (number // 32)
self._edge_shift = number % 32
self._rising_offset = self._MEM.GPREN_OFFSET + (number // 32)
self._rising_shift = number % 32
self._falling_offset = self._MEM.GPFEN_OFFSET + (number // 32)
self._falling_shift = number % 32
self._when_changed = None
self._change_thread = None
self._change_event = Event()
self.function = 'input'
self.pull = 'up' if number in (2, 3) else 'floating'
self.bounce = None
self.edges = 'both'
return self
def __repr__(self):
return "GPIO%d" % self._number
@property
def number(self):
return self._number
def close(self):
self.when_changed = None
self.function = 'input'
def _get_function(self):
return self.FUNCTION_NAMES[(self._MEM[self._func_offset] >> self._func_shift) & 7]
def _set_function(self, value):
try:
value = self.FUNCTION_VALUES[value]
except KeyError:
raise PinInvalidFunction('invalid function "%s" for pin %r' % self)
self._MEM[self._func_offset] = (
self._MEM[self._func_offset]
& ~(7 << self._func_shift)
| (value << self._func_shift)
)
def _get_state(self):
return bool(self._MEM[self._level_offset] & (1 << self._level_shift))
def _set_state(self, value):
if value:
self._MEM[self._set_offset] = 1 << self._set_shift
else:
self._MEM[self._clear_offset] = 1 << self._clear_shift
def _get_pull(self):
return self.PULL_NAMES[self._pull]
def _set_pull(self, value):
if self.function != 'input':
raise PinFixedPull('cannot set pull on non-input pin %r' % self)
if value != 'up' and self._number in (2, 3):
raise PinFixedPull('%r has a physical pull-up resistor' % self)
try:
value = self.PULL_VALUES[value]
except KeyError:
raise PinInvalidPull('invalid pull direction "%s" for pin %r' % self)
self._MEM[self._MEM.GPPUD_OFFSET] = value
sleep(0.000000214)
self._MEM[self._pull_offset] = 1 << self._pull_shift
sleep(0.000000214)
self._MEM[self._MEM.GPPUD_OFFSET] = 0
self._MEM[self._pull_offset] = 0
self._pull = value
def _get_edges(self):
rising = bool(self._MEM[self._rising_offset] & (1 << self._rising_shift))
falling = bool(self._MEM[self._falling_offset] & (1 << self._falling_shift))
return self.EDGE_NAMES[(rising, falling)]
def _set_edges(self, value):
try:
rising, falling = self.EDGE_VALUES[value]
except KeyError:
raise PinInvalidEdges('invalid edge specification "%s" for pin %r' % self)
f = self.when_changed
self.when_changed = None
try:
self._MEM[self._rising_offset] = (
self._MEM[self._rising_offset]
& ~(1 << self._rising_shift)
| (rising << self._rising_shift)
)
self._MEM[self._falling_offset] = (
self._MEM[self._falling_offset]
& ~(1 << self._falling_shift)
| (falling << self._falling_shift)
)
finally:
self.when_changed = f
def _get_when_changed(self):
return self._when_changed
def _set_when_changed(self, value):
if self._when_changed is None and value is not None:
self._when_changed = value
self._change_thread = Thread(target=self._change_watch)
self._change_thread.daemon = True
self._change_event.clear()
self._change_thread.start()
elif self._when_changed is not None and value is None:
self._change_event.set()
self._change_thread.join()
self._change_thread = None
self._when_changed = None
else:
self._when_changed = value
def _change_watch(self):
offset = self._edge_offset
mask = 1 << self._edge_shift
self._MEM[offset] = mask # clear any existing detection bit
while not self._change_event.wait(0.001):
if self._MEM[offset] & mask:
self._MEM[offset] = mask
self._when_changed()

205
gpiozero/pins/rpigpio.py Normal file
View File

@@ -0,0 +1,205 @@
from __future__ import (
unicode_literals,
absolute_import,
print_function,
division,
)
str = type('')
from RPi import GPIO
from . import Pin
from .exc import (
PinInvalidFunction,
PinSetInput,
PinFixedPull,
)
class RPiGPIOPin(Pin):
"""
Uses the `RPi.GPIO`_ library to interface to the Pi's GPIO pins. This is
the default pin implementation if the RPi.GPIO library is installed.
Supports all features including PWM (via software).
.. _RPi.GPIO: https://pypi.python.org/pypi/RPi.GPIO
"""
_PINS = {}
GPIO_FUNCTIONS = {
'input': GPIO.IN,
'output': GPIO.OUT,
'i2c': GPIO.I2C,
'spi': GPIO.SPI,
'pwm': GPIO.PWM,
'serial': GPIO.SERIAL,
'unknown': GPIO.UNKNOWN,
}
GPIO_PULL_UPS = {
'up': GPIO.PUD_UP,
'down': GPIO.PUD_DOWN,
'floating': GPIO.PUD_OFF,
}
GPIO_EDGES = {
'both': GPIO.BOTH,
'rising': GPIO.RISING,
'falling': GPIO.FALLING,
}
GPIO_FUNCTION_NAMES = {v: k for (k, v) in GPIO_FUNCTIONS.items()}
GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()}
GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()}
def __new__(cls, number):
if not cls._PINS:
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
try:
return cls._PINS[number]
except KeyError:
self = super(RPiGPIOPin, cls).__new__(cls)
cls._PINS[number] = self
self._number = number
self._pull = 'up' if number in (2, 3) else 'floating'
self._pwm = None
self._frequency = None
self._duty_cycle = None
self._bounce = -666
self._when_changed = None
self._edges = GPIO.BOTH
GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[self._pull])
return self
def __repr__(self):
return "GPIO%d" % self._number
@property
def number(self):
return self._number
def close(self):
self.frequency = None
self.when_changed = None
GPIO.cleanup(self._number)
def output_with_state(self, state):
self._pull = 'floating'
GPIO.setup(self._number, GPIO.OUT, initial=state)
def input_with_pull(self, pull):
if pull != 'up' and self._number in (2, 3):
raise PinFixedPull('%r has a physical pull-up resistor' % self)
try:
GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[pull])
self._pull = pull
except KeyError:
raise PinInvalidPull('invalid pull "%s" for pin %r' % (pull, self))
def _get_function(self):
return self.GPIO_FUNCTION_NAMES[GPIO.gpio_function(self._number)]
def _set_function(self, value):
if value != 'input':
self._pull = 'floating'
try:
GPIO.setup(self._number, self.GPIO_FUNCTIONS[value], self.GPIO_PULL_UPS[self._pull])
except KeyError:
raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self))
def _get_state(self):
if self._pwm:
return self._duty_cycle
else:
return GPIO.input(self._number)
def _set_state(self, value):
if self._pwm:
try:
self._pwm.ChangeDutyCycle(value * 100)
except ValueError:
raise PinInvalidValue('invalid state "%s" for pin %r' % (value, self))
self._duty_cycle = value
else:
try:
GPIO.output(self._number, value)
except ValueError:
raise PinInvalidState('invalid state "%s" for pin %r' % (value, self))
except RuntimeError:
raise PinSetInput('cannot set state of pin %r' % self)
def _get_pull(self):
return self._pull
def _set_pull(self, value):
if self.function != 'input':
raise PinFixedPull('cannot set pull on non-input pin %r' % self)
if value != 'up' and self._number in (2, 3):
raise PinFixedPull('%r has a physical pull-up resistor' % self)
try:
GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[value])
self._pull = value
except KeyError:
raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self))
def _get_frequency(self):
return self._frequency
def _set_frequency(self, value):
if self._frequency is None and value is not None:
try:
self._pwm = GPIO.PWM(self._number, value)
except RuntimeError:
raise PinPWMFixedValue('cannot start PWM on pin %r' % self)
self._pwm.start(0)
self._duty_cycle = 0
self._frequency = value
elif self._frequency is not None and value is not None:
self._pwm.ChangeFrequency(value)
self._frequency = value
elif self._frequency is not None and value is None:
self._pwm.stop()
self._pwm = None
self._duty_cycle = None
self._frequency = None
def _get_bounce(self):
return None if self._bounce == -666 else (self._bounce / 1000)
def _set_bounce(self, value):
f = self.when_changed
self.when_changed = None
try:
self._bounce = -666 if value is None else (value * 1000)
finally:
self.when_changed = f
def _get_edges(self):
return self.GPIO_EDGES_NAMES[self._edges]
def _set_edges(self, value):
f = self.when_changed
self.when_changed = None
try:
self._edges = self.GPIO_EDGES[value]
finally:
self.when_changed = f
def _get_when_changed(self):
return self._when_changed
def _set_when_changed(self, value):
if self._when_changed is None and value is not None:
self._when_changed = value
GPIO.add_event_detect(
self._number, self._edges,
callback=lambda channel: self._when_changed(),
bouncetime=self._bounce)
elif self._when_changed is not None and value is None:
GPIO.remove_event_detect(self._number)
self._when_changed = None
else:
self._when_changed = value

204
gpiozero/pins/rpio.py Normal file
View File

@@ -0,0 +1,204 @@
from __future__ import (
unicode_literals,
absolute_import,
print_function,
division,
)
str = type('')
from threading import Lock
import RPIO
import RPIO.PWM
from . import Pin, PINS_CLEANUP
from .exc import (
PinInvalidFunction,
PinSetInput,
PinFixedPull,
)
class RPIOPin(Pin):
"""
Uses the `RPIO`_ library to interface to the Pi's GPIO pins. This is
the default pin implementation if the RPi.GPIO library is not installed,
but RPIO is. Supports all features including PWM (hardware via DMA).
.. note::
Please note that at the time of writing, RPIO is only compatible with
Pi 1's; the Raspberry Pi 2 Model B is *not* supported. Also note that
root access is required so scripts must typically be run with ``sudo``.
.. _RPIO: https://pythonhosted.org/RPIO/
"""
_PINS = {}
GPIO_FUNCTIONS = {
'input': RPIO.IN,
'output': RPIO.OUT,
'alt0': RPIO.ALT0,
}
GPIO_PULL_UPS = {
'up': RPIO.PUD_UP,
'down': RPIO.PUD_DOWN,
'floating': RPIO.PUD_OFF,
}
GPIO_FUNCTION_NAMES = {v: k for (k, v) in GPIO_FUNCTIONS.items()}
GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()}
def __new__(cls, number):
if not cls._PINS:
RPIO.setmode(RPIO.BCM)
RPIO.setwarnings(False)
RPIO.wait_for_interrupts(threaded=True)
RPIO.PWM.setup()
RPIO.PWM.init_channel(0, 10000)
PINS_CLEANUP.append(RPIO.PWM.cleanup)
PINS_CLEANUP.append(RPIO.stop_waiting_for_interrupts)
PINS_CLEANUP.append(RPIO.cleanup)
try:
return cls._PINS[number]
except KeyError:
self = super(RPIOPin, cls).__new__(cls)
cls._PINS[number] = self
self._number = number
self._pull = 'up' if number in (2, 3) else 'floating'
self._pwm = False
self._duty_cycle = None
self._bounce = None
self._when_changed = None
self._edges = 'both'
RPIO.setup(self._number, RPIO.IN, self.GPIO_PULL_UPS[self._pull])
return self
def __repr__(self):
return "GPIO%d" % self._number
@property
def number(self):
return self._number
def close(self):
self.frequency = None
self.when_changed = None
RPIO.setup(self._number, RPIO.IN, RPIO.PUD_OFF)
def _get_function(self):
return self.GPIO_FUNCTION_NAMES[RPIO.gpio_function(self._number)]
def _set_function(self, value):
if value != 'input':
self._pull = 'floating'
try:
RPIO.setup(self._number, self.GPIO_FUNCTIONS[value], self.GPIO_PULL_UPS[self._pull])
except KeyError:
raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self))
def _get_state(self):
if self._pwm:
return self._duty_cycle
else:
return RPIO.input(self._number)
def _set_state(self, value):
if not 0 <= value <= 1:
raise PinInvalidValue('invalid state "%s" for pin %r' % (value, self))
if self._pwm:
RPIO.PWM.clear_channel_gpio(0, self._number)
if value == 0:
RPIO.output(self._number, False)
elif value == 1:
RPIO.output(self._number, True)
else:
RPIO.PWM.add_channel_pulse(0, self._number, start=0, width=int(1000 * value))
self._duty_cycle = value
else:
try:
RPIO.output(self._number, value)
except ValueError:
raise PinInvalidState('invalid state "%s" for pin %r' % (value, self))
except RuntimeError:
raise PinSetInput('cannot set state of pin %r' % self)
def _get_pull(self):
return self._pull
def _set_pull(self, value):
if self.function != 'input':
raise PinFixedPull('cannot set pull on non-input pin %r' % self)
if value != 'up' and self._number in (2, 3):
raise PinFixedPull('%r has a physical pull-up resistor' % self)
try:
RPIO.setup(self._number, RPIO.IN, self.GPIO_PULL_UPS[value])
self._pull = value
except KeyError:
raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self))
def _get_frequency(self):
return 100
def _set_frequency(self, value):
if value is not None and value != 100:
raise PinPWMError(
'RPIOPin implementation is currently limited to '
'100Hz sub-cycles')
if not self._pwm and value is not None:
self._pwm = True
# Dirty hack to get RPIO's PWM support to setup, but do nothing,
# for a given GPIO pin
RPIO.PWM.add_channel_pulse(0, self._number, start=0, width=0)
RPIO.PWM.clear_channel_gpio(0, self._number)
elif self._pwm and value is None:
RPIO.PWM.clear_channel_gpio(0, self._number)
self._pwm = False
def _get_bounce(self):
return None if self._bounce is None else (self._bounce / 1000)
def _set_bounce(self, value):
f = self.when_changed
self.when_changed = None
try:
self._bounce = None if value is None else (value * 1000)
finally:
self.when_changed = f
def _get_edges(self):
return self._edges
def _set_edges(self, value):
f = self.when_changed
self.when_changed = None
try:
self._edges = value
finally:
self.when_changed = f
def _get_when_changed(self):
return self._when_changed
def _set_when_changed(self, value):
if self._when_changed is None and value is not None:
self._when_changed = value
RPIO.add_interrupt_callback(
self._number,
lambda channel, value: self._when_changed(),
self._edges, self.GPIO_PULL_UPS[self._pull], self._bounce)
elif self._when_changed is not None and value is None:
try:
RPIO.del_interrupt_callback(self._number)
except KeyError:
# Ignore this exception which occurs during shutdown; this
# simply means RPIO's built-in cleanup has already run and
# removed the handler
pass
self._when_changed = None
else:
self._when_changed = value

View File

@@ -45,7 +45,6 @@ __keywords__ = [
]
__requires__ = [
'RPi.GPIO',
'spidev',
]