mirror of
https://github.com/KevinMidboe/python-gpiozero.git
synced 2025-10-29 17:50:37 +00:00
Fix #459 - properly support remote SPI with pigpio
Sorry! Dave's messing around with the pin implementations again. Hopefully the last time. The pin_factory is now really a factory object which can be asked to produce individual pins or pin-based interfaces like SPI (which can be supported properly via pigpio).
This commit is contained in:
@@ -10,15 +10,16 @@ str = type('')
|
||||
import os
|
||||
import atexit
|
||||
import weakref
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
from itertools import chain
|
||||
from types import FunctionType
|
||||
from threading import RLock
|
||||
from threading import Lock
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from .pins import Pin
|
||||
from .threads import _threads_shutdown
|
||||
from .pins import _pins_shutdown
|
||||
from .mixins import (
|
||||
ValuesMixin,
|
||||
SharedMixin,
|
||||
@@ -32,52 +33,11 @@ from .exc import (
|
||||
GPIOPinMissing,
|
||||
GPIOPinInUse,
|
||||
GPIODeviceClosed,
|
||||
PinFactoryFallback,
|
||||
)
|
||||
from .compat import frozendict
|
||||
|
||||
|
||||
def _default_pin_factory(name=os.getenv('GPIOZERO_PIN_FACTORY', None)):
|
||||
group = 'gpiozero_pin_factories'
|
||||
if name is None:
|
||||
# If no factory is explicitly specified, try various names in
|
||||
# "preferred" order. Note that in this case we only select from
|
||||
# gpiozero distribution so without explicitly specifying a name (via
|
||||
# the environment) it's impossible to auto-select a factory from
|
||||
# outside the base distribution
|
||||
#
|
||||
# We prefer RPi.GPIO 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
|
||||
dist = pkg_resources.get_distribution('gpiozero')
|
||||
for name in ('RPiGPIOPin', 'RPIOPin', 'PiGPIOPin', 'NativePin'):
|
||||
try:
|
||||
return pkg_resources.load_entry_point(dist, group, name)
|
||||
except ImportError:
|
||||
pass
|
||||
raise BadPinFactory('Unable to locate any default pin factory!')
|
||||
else:
|
||||
for factory in pkg_resources.iter_entry_points(group, name):
|
||||
return factory.load()
|
||||
raise BadPinFactory('Unable to locate pin factory "%s"' % name)
|
||||
|
||||
pin_factory = _default_pin_factory()
|
||||
|
||||
|
||||
_PINS = set()
|
||||
_PINS_LOCK = RLock() # Yes, this needs to be re-entrant
|
||||
|
||||
def _shutdown():
|
||||
_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
|
||||
_pins_shutdown()
|
||||
|
||||
atexit.register(_shutdown)
|
||||
|
||||
|
||||
class GPIOMeta(type):
|
||||
# NOTE Yes, this is a metaclass. Don't be scared - it's a simple one.
|
||||
|
||||
@@ -106,7 +66,7 @@ class GPIOMeta(type):
|
||||
# already exists. Only construct the instance if the key's new.
|
||||
key = cls._shared_key(*args, **kwargs)
|
||||
try:
|
||||
self = cls._INSTANCES[key]
|
||||
self = cls._instances[key]
|
||||
self._refs += 1
|
||||
except (KeyError, ReferenceError) as e:
|
||||
self = super(GPIOMeta, cls).__call__(*args, **kwargs)
|
||||
@@ -122,14 +82,14 @@ class GPIOMeta(type):
|
||||
old_close()
|
||||
finally:
|
||||
try:
|
||||
del cls._INSTANCES[key]
|
||||
del cls._instances[key]
|
||||
except KeyError:
|
||||
# If the _refs go negative (too many closes)
|
||||
# just ignore the resulting KeyError here -
|
||||
# it's already gone
|
||||
pass
|
||||
self.close = close
|
||||
cls._INSTANCES[key] = weakref.proxy(self)
|
||||
cls._instances[key] = weakref.proxy(self)
|
||||
else:
|
||||
# Construct the instance as normal
|
||||
self = super(GPIOMeta, cls).__call__(*args, **kwargs)
|
||||
@@ -229,13 +189,100 @@ class GPIOBase(GPIOMeta(nstr('GPIOBase'), (), {})):
|
||||
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. It defines the
|
||||
basic services applicable to all devices (specifically the :attr:`is_active`
|
||||
etc. This is the base class of the device hierarchy. It defines the basic
|
||||
services applicable to all devices (specifically the :attr:`is_active`
|
||||
property, the :attr:`value` property, and the :meth:`close` method).
|
||||
"""
|
||||
_pin_factory = None # instance of a Factory sub-class
|
||||
_reservations = {} # maps pin addresses to lists of devices
|
||||
_res_lock = Lock()
|
||||
|
||||
def __repr__(self):
|
||||
return "<gpiozero.%s object>" % (self.__class__.__name__)
|
||||
|
||||
@classmethod
|
||||
def _set_pin_factory(cls, new_factory):
|
||||
if cls._pin_factory is not None:
|
||||
cls._pin_factory.close()
|
||||
cls._pin_factory = new_factory
|
||||
|
||||
def _reserve_pins(self, *pins_or_addresses):
|
||||
"""
|
||||
Called to indicate that the device reserves the right to use the
|
||||
specified *pins_or_addresses*. This should be done during device
|
||||
construction. If pins are reserved, you must ensure that the
|
||||
reservation is released by eventually called :meth:`_release_pins`.
|
||||
|
||||
The *pins_or_addresses* can be actual :class:`Pin` instances or the
|
||||
addresses of pin instances (each address is a tuple of strings). The
|
||||
latter form is permitted to ensure that devices do not have to
|
||||
construct :class:`Pin` objects to reserve pins. This is important as
|
||||
constructing a pin often configures it (e.g. as an input) which
|
||||
conflicts with alternate pin functions like SPI.
|
||||
"""
|
||||
addresses = (
|
||||
p.address if isinstance(p, Pin) else p
|
||||
for p in pins_or_addresses
|
||||
)
|
||||
with self._res_lock:
|
||||
for address in addresses:
|
||||
try:
|
||||
conflictors = self._reservations[address]
|
||||
except KeyError:
|
||||
conflictors = []
|
||||
self._reservations[address] = conflictors
|
||||
for device_ref in conflictors:
|
||||
device = device_ref()
|
||||
if device is not None and self._conflicts_with(device):
|
||||
raise GPIOPinInUse(
|
||||
'pin %s is already in use by %r' % (
|
||||
'/'.join(address), device)
|
||||
)
|
||||
conflictors.append(weakref.ref(self))
|
||||
|
||||
def _release_pins(self, *pins_or_addresses):
|
||||
"""
|
||||
Releases the reservation of this device against *pins_or_addresses*.
|
||||
This is typically called during :meth:`close` to clean up reservations
|
||||
taken during construction. Releasing a reservation that is not
|
||||
currently held will be silently ignored (to permit clean-up after
|
||||
failed / partial construction).
|
||||
"""
|
||||
addresses = (
|
||||
p.address if isinstance(p, Pin) else p
|
||||
for p in pins_or_addresses
|
||||
)
|
||||
with self._res_lock:
|
||||
for address in addresses:
|
||||
self._reservations[address] = [
|
||||
ref for ref in self._reservations[address]
|
||||
if ref() not in (self, None) # may as well clean up dead refs
|
||||
]
|
||||
|
||||
def _release_all(self):
|
||||
"""
|
||||
Releases all pin reservations taken out by this device. See
|
||||
:meth:`_release_pins` for further information).
|
||||
"""
|
||||
with self._res_lock:
|
||||
Device._reservations = {
|
||||
address: [
|
||||
ref for ref in conflictors
|
||||
if ref() not in (self, None)
|
||||
]
|
||||
for address, conflictors in self._reservations.items()
|
||||
}
|
||||
|
||||
def _conflicts_with(self, other):
|
||||
"""
|
||||
Called by :meth:`_reserve_pin` to test whether the *other*
|
||||
:class:`Device` using a common pin conflicts with this device's intent
|
||||
to use it. The default is ``True`` indicating that all devices conflict
|
||||
with common pins. Sub-classes may override this to permit more nuanced
|
||||
replies.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
@@ -378,14 +425,12 @@ class GPIODevice(Device):
|
||||
self._pin = None
|
||||
if pin is None:
|
||||
raise GPIOPinMissing('No pin given')
|
||||
if isinstance(pin, int):
|
||||
pin = pin_factory(pin)
|
||||
with _PINS_LOCK:
|
||||
if pin in _PINS:
|
||||
raise GPIOPinInUse(
|
||||
'pin %r is already in use by another gpiozero object' % pin
|
||||
)
|
||||
_PINS.add(pin)
|
||||
if isinstance(pin, Pin):
|
||||
self._reserve_pins(pin)
|
||||
else:
|
||||
# Check you can reserve *before* constructing the pin
|
||||
self._reserve_pins(self._pin_factory.pin_address(pin))
|
||||
pin = self._pin_factory.pin(pin)
|
||||
self._pin = pin
|
||||
self._active_state = True
|
||||
self._inactive_state = False
|
||||
@@ -402,12 +447,10 @@ class GPIODevice(Device):
|
||||
|
||||
def close(self):
|
||||
super(GPIODevice, self).close()
|
||||
with _PINS_LOCK:
|
||||
pin = self._pin
|
||||
if self._pin is not None:
|
||||
self._release_pins(self._pin)
|
||||
self._pin.close()
|
||||
self._pin = None
|
||||
if pin in _PINS:
|
||||
_PINS.remove(pin)
|
||||
pin.close()
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
@@ -441,3 +484,41 @@ class GPIODevice(Device):
|
||||
except DeviceClosed:
|
||||
return "<gpiozero.%s object closed>" % self.__class__.__name__
|
||||
|
||||
|
||||
# Defined last to ensure Device is defined before attempting to load any pin
|
||||
# factory; pin factories want to load spi which in turn relies on devices (for
|
||||
# the soft-SPI implementation)
|
||||
def _default_pin_factory(name=os.getenv('GPIOZERO_PIN_FACTORY', None)):
|
||||
group = 'gpiozero_pin_factories'
|
||||
if name is None:
|
||||
# If no factory is explicitly specified, try various names in
|
||||
# "preferred" order. Note that in this case we only select from
|
||||
# gpiozero distribution so without explicitly specifying a name (via
|
||||
# the environment) it's impossible to auto-select a factory from
|
||||
# outside the base distribution
|
||||
#
|
||||
# We prefer RPi.GPIO 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
|
||||
dist = pkg_resources.get_distribution('gpiozero')
|
||||
for name in ('rpigpio', 'rpio', 'pigpio', 'native'):
|
||||
try:
|
||||
return pkg_resources.load_entry_point(dist, group, name)()
|
||||
except Exception as e:
|
||||
warnings.warn(
|
||||
PinFactoryFallback(
|
||||
'Failed to load factory %s: %s' % (name, str(e))))
|
||||
raise BadPinFactory('Unable to load any default pin factory!')
|
||||
else:
|
||||
for factory in pkg_resources.iter_entry_points(group, name.lower()):
|
||||
return factory.load()()
|
||||
raise BadPinFactory('Unable to find pin factory "%s"' % name)
|
||||
|
||||
Device._set_pin_factory(_default_pin_factory())
|
||||
|
||||
def _shutdown():
|
||||
_threads_shutdown()
|
||||
Device._set_pin_factory(None)
|
||||
|
||||
atexit.register(_shutdown)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user