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

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