360 lines
12 KiB
Python
360 lines
12 KiB
Python
# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
|
|
#
|
|
# Copyright (c) 2021 Dave Jones <dave@waveform.org.uk>
|
|
#
|
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
from __future__ import (
|
|
unicode_literals,
|
|
absolute_import,
|
|
print_function,
|
|
division,
|
|
)
|
|
str = type('')
|
|
|
|
import os
|
|
|
|
import lgpio
|
|
|
|
from . import SPI
|
|
from .pi import spi_port_device
|
|
from .local import LocalPiFactory, LocalPiPin
|
|
from ..mixins import SharedMixin
|
|
from ..exc import (
|
|
PinInvalidFunction,
|
|
PinSetInput,
|
|
PinFixedPull,
|
|
PinInvalidPull,
|
|
PinInvalidBounce,
|
|
PinInvalidState,
|
|
SPIBadArgs,
|
|
SPIInvalidClockMode,
|
|
PinPWMFixedValue,
|
|
DeviceClosed
|
|
)
|
|
|
|
|
|
class LGPIOFactory(LocalPiFactory):
|
|
"""
|
|
Extends :class:`~gpiozero.pins.local.LocalPiFactory`. Uses the `lgpio`_
|
|
library to interface to the local computer's GPIO pins. The lgpio library
|
|
simply talks to Linux gpiochip devices; it is not specific to the Raspberry
|
|
Pi although this class is currently constructed under the assumption that
|
|
it is running on a Raspberry Pi.
|
|
|
|
You can construct lgpio pins manually like so::
|
|
|
|
from gpiozero.pins.lgpio import LGPIOFactory
|
|
from gpiozero import LED
|
|
|
|
factory = LGPIOFactory(chip=0)
|
|
led = LED(12, pin_factory=factory)
|
|
|
|
The *chip* parameter to the factory constructor specifies which gpiochip
|
|
device to attempt to open. It defaults to 0 and thus doesn't normally need
|
|
to be specified (the example above only includes it for completeness).
|
|
|
|
The lgpio library relies on access to the :file:`/dev/gpiochip*` devices.
|
|
If you run into issues, please check that your user has read/write access
|
|
to the specific gpiochip device you are attempting to open (0 by default).
|
|
|
|
.. _lgpio: http://abyz.me.uk/lg/py_lgpio.html
|
|
"""
|
|
def __init__(self, chip=0):
|
|
super(LGPIOFactory, self).__init__()
|
|
self._handle = lgpio.gpiochip_open(chip)
|
|
self._chip = chip
|
|
self.pin_class = LGPIOPin
|
|
|
|
def close(self):
|
|
super(LGPIOFactory, self).close()
|
|
if self._handle is not None:
|
|
lgpio.gpiochip_close(self._handle)
|
|
self._handle = None
|
|
|
|
@property
|
|
def chip(self):
|
|
return self._chip
|
|
|
|
def _get_spi_class(self, shared, hardware):
|
|
# support via lgpio instead of spidev
|
|
if hardware:
|
|
return [LGPIOHardwareSPI, LGPIOHardwareSPIShared][shared]
|
|
return super(LGPIOFactory, self)._get_spi_class(shared, hardware=False)
|
|
|
|
|
|
class LGPIOPin(LocalPiPin):
|
|
"""
|
|
Extends :class:`~gpiozero.pins.local.LocalPiPin`. Pin implementation for
|
|
the `lgpio`_ library. See :class:`LGPIOFactory` for more information.
|
|
|
|
.. _lgpio: http://abyz.me.uk/lg/py_lgpio.html
|
|
"""
|
|
GPIO_IS_KERNEL = 1 << 0
|
|
GPIO_IS_OUT = 1 << 1
|
|
GPIO_IS_ACTIVE_LOW = 1 << 2
|
|
GPIO_IS_OPEN_DRAIN = 1 << 3
|
|
GPIO_IS_OPEN_SOURCE = 1 << 4
|
|
GPIO_IS_BIAS_PULL_UP = 1 << 5
|
|
GPIO_IS_BIAS_PULL_DOWN = 1 << 6
|
|
GPIO_IS_BIAS_DISABLE = 1 << 7
|
|
GPIO_IS_LG_INPUT = 1 << 8
|
|
GPIO_IS_LG_OUTPUT = 1 << 9
|
|
GPIO_IS_LG_ALERT = 1 << 10
|
|
GPIO_IS_LG_GROUP = 1 << 11
|
|
|
|
GPIO_LINE_FLAGS_MASK = (
|
|
GPIO_IS_ACTIVE_LOW | GPIO_IS_OPEN_DRAIN | GPIO_IS_OPEN_SOURCE |
|
|
GPIO_IS_BIAS_PULL_UP | GPIO_IS_BIAS_PULL_DOWN | GPIO_IS_BIAS_DISABLE)
|
|
|
|
GPIO_EDGES = {
|
|
'both': lgpio.BOTH_EDGES,
|
|
'rising': lgpio.RISING_EDGE,
|
|
'falling': lgpio.FALLING_EDGE,
|
|
}
|
|
|
|
GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()}
|
|
|
|
def __init__(self, factory, number):
|
|
super(LGPIOPin, self).__init__(factory, number)
|
|
self._pwm = None
|
|
self._bounce = None
|
|
self._callback = None
|
|
self._edges = lgpio.BOTH_EDGES
|
|
lgpio.gpio_claim_input(
|
|
self.factory._handle, self.number, lgpio.SET_BIAS_DISABLE)
|
|
|
|
def close(self):
|
|
if self.factory._handle is not None:
|
|
# Closing is really just "resetting" the function of the pin;
|
|
# we let the factory close deal with actually freeing stuff
|
|
lgpio.gpio_claim_input(
|
|
self.factory._handle, self.number, lgpio.SET_BIAS_DISABLE)
|
|
|
|
def _get_function(self):
|
|
mode = lgpio.gpio_get_mode(self.factory._handle, self.number)
|
|
return ['input', 'output'][bool(mode & self.GPIO_IS_OUT)]
|
|
|
|
def _set_function(self, value):
|
|
if self._callback is not None:
|
|
self._callback.cancel()
|
|
self._callback = None
|
|
try:
|
|
{
|
|
'input': lgpio.gpio_claim_input,
|
|
'output': lgpio.gpio_claim_output,
|
|
}[value](self.factory._handle, self.number)
|
|
except KeyError:
|
|
raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self))
|
|
|
|
def _get_state(self):
|
|
if self._pwm:
|
|
return self._pwm[1] / 100
|
|
else:
|
|
return bool(lgpio.gpio_read(self.factory._handle, self.number))
|
|
|
|
def _set_state(self, value):
|
|
if self._pwm:
|
|
freq, duty = self._pwm
|
|
self._pwm = (freq, int(value * 100))
|
|
try:
|
|
lgpio.tx_pwm(self.factory._handle, self.number, *self._pwm)
|
|
except lgpio.error:
|
|
raise PinInvalidState('invalid state "%s" for pin %r' % (value, self))
|
|
elif self.function == 'input':
|
|
raise PinSetInput('cannot set state of pin %r' % self)
|
|
else:
|
|
lgpio.gpio_write(self.factory._handle, self.number, bool(value))
|
|
|
|
def _get_pull(self):
|
|
mode = lgpio.gpio_get_mode(self.factory._handle, self.number)
|
|
if mode & self.GPIO_IS_BIAS_PULL_UP:
|
|
return 'up'
|
|
elif mode & self.GPIO_IS_BIAS_PULL_DOWN:
|
|
return 'down'
|
|
else:
|
|
return 'floating'
|
|
|
|
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.factory.pi_info.pulled_up(repr(self)):
|
|
raise PinFixedPull('%r has a physical pull-up resistor' % self)
|
|
try:
|
|
flags = {
|
|
'up': lgpio.SET_BIAS_PULL_UP,
|
|
'down': lgpio.SET_BIAS_PULL_DOWN,
|
|
'floating': lgpio.SET_BIAS_DISABLE,
|
|
}[value]
|
|
except KeyError:
|
|
raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self))
|
|
else:
|
|
# Simply calling gpio_claim_input is insufficient to change the
|
|
# line flags on a pin; it needs to be freed and re-claimed
|
|
lgpio.gpio_free(self.factory._handle, self.number)
|
|
lgpio.gpio_claim_input(self.factory._handle, self.number, flags)
|
|
|
|
def _get_frequency(self):
|
|
if self._pwm:
|
|
freq, duty = self._pwm
|
|
return freq
|
|
else:
|
|
return None
|
|
|
|
def _set_frequency(self, value):
|
|
if not self._pwm and value is not None and value > 0:
|
|
if self.function != 'output':
|
|
raise PinPWMFixedValue('cannot start PWM on pin %r' % self)
|
|
lgpio.tx_pwm(self.factory._handle, self.number, value, 0)
|
|
self._pwm = (value, 0)
|
|
elif self._pwm and value is not None and value > 0:
|
|
freq, duty = self._pwm
|
|
lgpio.tx_pwm(self.factory._handle, self.number, value, duty)
|
|
self._pwm = (value, duty)
|
|
elif self._pwm and (value is None or value == 0):
|
|
lgpio.tx_pwm(self.factory._handle, self.number, 0, 0)
|
|
self._pwm = None
|
|
|
|
def _get_bounce(self):
|
|
return None if not self._bounce else self._bounce / 1000000
|
|
|
|
def _set_bounce(self, value):
|
|
if value is None:
|
|
value = 0
|
|
elif value < 0:
|
|
raise PinInvalidBounce('bounce must be 0 or greater')
|
|
value = int(value * 1000000)
|
|
lgpio.gpio_set_debounce_micros(self.factory._handle, self.number, value)
|
|
self._bounce = value
|
|
|
|
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 _call_when_changed(self, chip, gpio, level, ticks):
|
|
super(LGPIOPin, self)._call_when_changed(ticks / 1000000000, level)
|
|
|
|
def _enable_event_detect(self):
|
|
lgpio.gpio_claim_alert(
|
|
self.factory._handle, self.number, self._edges,
|
|
lgpio.gpio_get_mode(self.factory._handle, self.number) &
|
|
self.GPIO_LINE_FLAGS_MASK)
|
|
self._callback = lgpio.callback(
|
|
self.factory._handle, self.number, self._edges,
|
|
self._call_when_changed)
|
|
|
|
def _disable_event_detect(self):
|
|
if self._callback is not None:
|
|
self._callback.cancel()
|
|
self._callback = None
|
|
lgpio.gpio_claim_input(
|
|
self.factory._handle, self.number,
|
|
lgpio.gpio_get_mode(self.factory._handle, self.number) &
|
|
self.GPIO_LINE_FLAGS_MASK)
|
|
|
|
|
|
class LGPIOHardwareSPI(SPI):
|
|
"""
|
|
Hardware SPI implementation for the `lgpio`_ library. Uses the ``spi_*``
|
|
functions from the lgpio API.
|
|
|
|
.. _lgpio: http://abyz.me.uk/lg/py_lgpio.html
|
|
"""
|
|
def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory):
|
|
port, device = spi_port_device(
|
|
clock_pin, mosi_pin, miso_pin, select_pin)
|
|
self._port = port
|
|
self._device = device
|
|
self._baud = 500000
|
|
self._spi_flags = 0
|
|
self._handle = None
|
|
super(LGPIOHardwareSPI, self).__init__(pin_factory=pin_factory)
|
|
to_reserve = {clock_pin, select_pin}
|
|
if mosi_pin is not None:
|
|
to_reserve.add(mosi_pin)
|
|
if miso_pin is not None:
|
|
to_reserve.add(miso_pin)
|
|
self.pin_factory.reserve_pins(self, *to_reserve)
|
|
self._handle = lgpio.spi_open(port, device, self._baud, self._spi_flags)
|
|
|
|
def _conflicts_with(self, other):
|
|
return not (
|
|
isinstance(other, LGPIOHardwareSPI) and
|
|
(self._port, self._device) != (other._port, other._device)
|
|
)
|
|
|
|
def close(self):
|
|
if not self.closed:
|
|
lgpio.spi_close(self._handle)
|
|
self._handle = None
|
|
self.pin_factory.release_all(self)
|
|
super(LGPIOHardwareSPI, self).close()
|
|
|
|
@property
|
|
def closed(self):
|
|
return self._handle is None
|
|
|
|
def __repr__(self):
|
|
try:
|
|
self._check_open()
|
|
return 'SPI(port=%d, device=%d)' % (self._port, self._device)
|
|
except DeviceClosed:
|
|
return 'SPI(closed)'
|
|
|
|
def _get_clock_mode(self):
|
|
return self._spi_flags
|
|
|
|
def _set_clock_mode(self, value):
|
|
self._check_open()
|
|
if not 0 <= value < 4:
|
|
raise SPIInvalidClockMode("%d is not a valid SPI clock mode" % value)
|
|
lgpio.spi_close(self._handle)
|
|
self._spi_flags = value
|
|
self._handle = lgpio.spi_open(
|
|
self._port, self._device, self._baud, self._spi_flags)
|
|
|
|
def _get_rate(self):
|
|
return self._baud
|
|
|
|
def _set_rate(self, value):
|
|
self._check_open()
|
|
value = int(value)
|
|
lgpio.spi_close(self._handle)
|
|
self._baud = value
|
|
self._handle = lgpio.spi_open(
|
|
self._port, self._device, self._baud, self._spi_flags)
|
|
|
|
def read(self, n):
|
|
self._check_open()
|
|
count, data = lgpio.spi_read(self._handle, n)
|
|
if count < 0:
|
|
raise IOError('SPI transfer error %d' % count)
|
|
return [int(b) for b in data]
|
|
|
|
def write(self, data):
|
|
self._check_open()
|
|
count = lgpio.spi_write(self._handle, data)
|
|
if count < 0:
|
|
raise IOError('SPI transfer error %d' % count)
|
|
return len(data)
|
|
|
|
def transfer(self, data):
|
|
self._check_open()
|
|
count, data = lgpio.spi_xfer(self._handle, data)
|
|
if count < 0:
|
|
raise IOError('SPI transfer error %d' % count)
|
|
return [int(b) for b in data]
|
|
|
|
|
|
class LGPIOHardwareSPIShared(SharedMixin, LGPIOHardwareSPI):
|
|
@classmethod
|
|
def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory):
|
|
return (clock_pin, select_pin)
|