Fix #279 once and for all (ha! ;)

This implements the proposal discussed in the re-opened #279 to add a
pin_factory argument at the device level and remove the ability to
specify a pin instance to device constructors (they now only accept a
pin specification).

Note: there's still a couple of bits to tidy up (tests on "real" Pis,
and pin_factory.release_all needs refinement) but the test suite is now
at least capable of passing on a PC.
This commit is contained in:
Dave Jones
2017-07-04 00:26:41 +01:00
parent 448feaf68f
commit c820636fcb
14 changed files with 396 additions and 403 deletions

View File

@@ -8,6 +8,10 @@ from __future__ import (
)
str = type('')
from weakref import ref
from collections import defaultdict
from threading import Lock
from ..exc import (
PinInvalidFunction,
PinSetInput,
@@ -20,6 +24,7 @@ from ..exc import (
SPIFixedBitOrder,
SPIFixedSelect,
SPIFixedWordSize,
GPIOPinInUse,
)
@@ -36,10 +41,61 @@ class Factory(object):
applicable:
* :meth:`close`
* :meth:`reserve_pins`
* :meth:`release_pins`
* :meth:`release_all`
* :meth:`pin`
* :meth:`spi`
* :meth:`_get_pi_info`
"""
def __init__(self):
self._reservations = defaultdict(list)
self._res_lock = Lock()
def reserve_pins(self, requester, *pins):
"""
Called to indicate that the device reserves the right to use the
specified *pins*. 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`.
"""
with self._res_lock:
for pin in pins:
for reserver_ref in self._reservations[pin]:
reserver = reserver_ref()
if reserver is not None and requester._conflicts_with(reserver):
raise GPIOPinInUse('pin %s is already in use by %r' %
(pin, reserver))
self._reservations[pin].append(ref(requester))
def release_pins(self, reserver, *pins):
"""
Releases the reservation of *reserver* against *pins*. This is
typically called during :meth:`Device.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).
"""
with self._res_lock:
for pin in pins:
self._reservations[pin] = [
ref for ref in self._reservations[pin]
if ref() not in (reserver, None) # may as well clean up dead refs
]
def release_all(self, reserver):
"""
Releases all pin reservations taken out by *reserver*. See
:meth:`release_pins` for further information).
"""
with self._res_lock:
self._reservations = defaultdict(list, {
pin: [
ref for ref in conflictors
if ref() not in (reserver, None)
]
for pin, conflictors in self._reservations.items()
})
def close(self):
"""
@@ -63,19 +119,6 @@ class Factory(object):
"""
raise PinUnsupported("Individual pins are not supported by this pin factory")
def pin_address(self, spec):
"""
Returns the address that a pin *would* have if constructed from the
given *spec*.
This unusual method is used by the pin reservation system to check
for conflicts *prior* to pin construction; with most implementations,
pin construction implicitly alters the state of the pin (e.g. setting
it to an input). This allows pin reservation to take place without
affecting the state of other components.
"""
raise NotImplementedError
def spi(self, **spi_args):
"""
Returns an instance of an :class:`SPI` interface, for the specified SPI
@@ -89,21 +132,6 @@ class Factory(object):
def _get_address(self):
raise NotImplementedError
address = property(
lambda self: self._get_address(),
doc="""\
Returns a tuple of strings representing the address of the factory.
For the Pi itself this is a tuple of one string representing the Pi's
address (e.g. "localhost"). Expander chips can return a tuple appending
whatever string they require to uniquely identify the expander chip
amongst all factories in the system.
.. note::
This property *must* return an immutable object capable of being
used as a dictionary key.
""")
def _get_pi_info(self):
return None
@@ -128,7 +156,6 @@ class Pin(object):
represent the capabilities of pins. Descendents *must* override the
following methods:
* :meth:`_get_address`
* :meth:`_get_function`
* :meth:`_set_function`
* :meth:`_get_state`
@@ -153,7 +180,7 @@ class Pin(object):
"""
def __repr__(self):
return self.address[-1]
return "<Pin>"
def close(self):
"""
@@ -195,18 +222,6 @@ class Pin(object):
self.function = 'input'
self.pull = pull
def _get_address(self):
raise NotImplementedError
address = property(
lambda self: self._get_address(),
doc="""\
The address of the pin. This property is a tuple of strings constructed
from the owning factory's address with the unique address of the pin
appended to it. The tuple as a whole uniquely identifies the pin
amongst all pins attached to the system.
""")
def _get_function(self):
return "input"

View File

@@ -8,6 +8,8 @@ str = type('')
import io
import warnings
from collections import defaultdict
from threading import Lock
try:
from spidev import SpiDev
@@ -31,6 +33,8 @@ class LocalPiFactory(PiFactory):
:class:`~gpiozero.pins.native.NativePin`).
"""
pins = {}
_reservations = defaultdict(list)
_res_lock = Lock()
def __init__(self):
super(LocalPiFactory, self).__init__()
@@ -40,14 +44,13 @@ class LocalPiFactory(PiFactory):
('software', 'exclusive'): LocalPiSoftwareSPI,
('software', 'shared'): LocalPiSoftwareSPIShared,
}
# Override the pins dict to be this class' pins dict. This is a bit of
# a dirty hack, but ensures that anyone evil enough to mix pin
# implementations doesn't try and control the same pin with different
# backends
# Override the reservations and pins dict to be this class' attributes.
# This is a bit of a dirty hack, but ensures that anyone evil enough to
# mix pin implementations doesn't try and control the same pin with
# different backends
self.pins = LocalPiFactory.pins
def _get_address(self):
return ('localhost',)
self._reservations = LocalPiFactory._reservations
self._res_lock = LocalPiFactory._res_lock
def _get_revision(self):
# Cache the result as we can reasonably assume it won't change during
@@ -74,19 +77,19 @@ class LocalPiPin(PiPin):
class LocalPiHardwareSPI(SPI, Device):
def __init__(self, factory, port, device):
if SpiDev is None:
raise ImportError('failed to import spidev')
self._port = port
self._device = device
self._interface = None
self._address = factory.address + ('SPI(port=%d, device=%d)' % (port, device),)
if SpiDev is None:
raise ImportError('failed to import spidev')
super(LocalPiHardwareSPI, self).__init__()
pins = SPI_HARDWARE_PINS[port]
self._reserve_pins(
factory.pin_address(pins['clock']),
factory.pin_address(pins['mosi']),
factory.pin_address(pins['miso']),
factory.pin_address(pins['select'][device])
self.pin_factory.reserve_pins(
self,
pins['clock'],
pins['mosi'],
pins['miso'],
pins['select'][device]
)
self._interface = SpiDev()
self._interface.open(port, device)
@@ -98,7 +101,7 @@ class LocalPiHardwareSPI(SPI, Device):
self._interface.close()
finally:
self._interface = None
self._release_all()
self.pin_factory.release_all(self)
super(LocalPiHardwareSPI, self).close()
@property
@@ -148,10 +151,6 @@ class LocalPiHardwareSPI(SPI, Device):
class LocalPiSoftwareSPI(SPI, OutputDevice):
def __init__(self, factory, clock_pin, mosi_pin, miso_pin, select_pin):
self._bus = None
self._address = factory.address + (
'SPI(clock_pin=%d, mosi_pin=%d, miso_pin=%d, select_pin=%d)' % (
clock_pin, mosi_pin, miso_pin, select_pin),
)
super(LocalPiSoftwareSPI, self).__init__(select_pin, active_high=False)
try:
self._clock_phase = False
@@ -163,6 +162,7 @@ class LocalPiSoftwareSPI(SPI, OutputDevice):
raise
def _conflicts_with(self, other):
# XXX Need to refine this
return not (
isinstance(other, LocalPiSoftwareSPI) and
(self.pin.number != other.pin.number)

View File

@@ -40,7 +40,7 @@ class MockPin(PiPin):
def __init__(self, factory, number):
super(MockPin, self).__init__(factory, number)
self._function = 'input'
self._pull = 'up' if factory.pi_info.pulled_up(self.address[-1]) else 'floating'
self._pull = 'up' if factory.pi_info.pulled_up(repr(self)) else 'floating'
self._state = self._pull == 'up'
self._bounce = None
self._edges = 'both'
@@ -94,7 +94,7 @@ class MockPin(PiPin):
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(self.address[-1]):
if value != 'up' and self.factory.pi_info.pulled_up(repr(self)):
raise PinFixedPull('%r has a physical pull-up resistor' % self)
if value not in ('floating', 'up', 'down'):
raise PinInvalidPull('pull must be floating, up, or down')
@@ -423,14 +423,12 @@ class MockFactory(LocalPiFactory):
pin_class = pkg_resources.load_entry_point(dist, group, pin_class.lower())
self.pin_class = pin_class
def _get_address(self):
return ('mock',)
def _get_revision(self):
return self._revision
def reset(self):
self.pins.clear()
self._reservations.clear()
def pin(self, spec, pin_class=None, **kwargs):
if pin_class is None:

View File

@@ -228,7 +228,7 @@ class NativePin(LocalPiPin):
self._change_thread = None
self._change_event = Event()
self.function = 'input'
self.pull = 'up' if factory.pi_info.pulled_up(self.address[-1]) else 'floating'
self.pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating'
self.bounce = None
self.edges = 'both'
@@ -236,7 +236,7 @@ class NativePin(LocalPiPin):
self.frequency = None
self.when_changed = None
self.function = 'input'
self.pull = 'up' if self.factory.pi_info.pulled_up(self.address[-1]) else 'floating'
self.pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating'
def _get_function(self):
return self.GPIO_FUNCTION_NAMES[(self.factory.mem[self._func_offset] >> self._func_shift) & 7]
@@ -269,7 +269,7 @@ class NativePin(LocalPiPin):
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(self.address[-1]):
if value != 'up' and self.factory.pi_info.pulled_up(repr(self)):
raise PinFixedPull('%r has a physical pull-up resistor' % self)
try:
value = self.GPIO_PULL_UPS[value]

View File

@@ -7,8 +7,9 @@ from __future__ import (
str = type('')
import io
from threading import RLock
from threading import RLock, Lock
from types import MethodType
from collections import defaultdict
try:
from weakref import ref, WeakMethod
except ImportError:
@@ -48,6 +49,7 @@ class PiFactory(Factory):
forms the base of :class:`~gpiozero.pins.local.LocalPiFactory`.
"""
def __init__(self):
super(PiFactory, self).__init__()
self._info = None
self.pins = {}
self.pin_class = None
@@ -72,10 +74,6 @@ class PiFactory(Factory):
self.pins[n] = pin
return pin
def pin_address(self, spec):
n = self._to_gpio(spec)
return self.address + ('GPIO%d' % n,)
def _to_gpio(self, spec):
"""
Converts the pin *spec* to a GPIO port number.
@@ -240,23 +238,23 @@ class PiPin(Pin):
self._when_changed = None
self._number = number
try:
factory.pi_info.physical_pin(self.address[-1])
factory.pi_info.physical_pin('GPIO%d' % self.number)
except PinNoPins:
warnings.warn(
PinNonPhysical(
'no physical pins exist for %s' % self.address[-1]))
'no physical pins exist for GPIO%d' % self.number))
@property
def number(self):
return self._number
def __repr__(self):
return 'GPIO%d' % self._number
@property
def factory(self):
return self._factory
def _get_address(self):
return self.factory.address + ('GPIO%d' % self.number,)
def _call_when_changed(self):
"""
Called to fire the :attr:`when_changed` event handler; override this

View File

@@ -7,7 +7,6 @@ from __future__ import (
str = type('')
import os
from weakref import proxy
import pigpio
@@ -126,9 +125,6 @@ class PiGPIOFactory(PiFactory):
def _get_revision(self):
return self.connection.get_hardware_revision()
def _get_address(self):
return ("%s:%d" % (self.host, self.port),)
def spi(self, **spi_args):
intf = super(PiGPIOFactory, self).spi(**spi_args)
self._spis.append(intf)
@@ -166,7 +162,7 @@ class PiGPIOPin(PiPin):
def __init__(self, factory, number):
super(PiGPIOPin, self).__init__(factory, number)
self._pull = 'up' if factory.pi_info.pulled_up(self.address[-1]) else 'floating'
self._pull = 'up' if factory.pi_info.pulled_up(repr(self)) else 'floating'
self._pwm = False
self._bounce = None
self._callback = None
@@ -183,7 +179,7 @@ class PiGPIOPin(PiPin):
self.frequency = None
self.when_changed = None
self.function = 'input'
self.pull = 'up' if self.factory.pi_info.pulled_up(self.address[-1]) else 'floating'
self.pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating'
def _get_function(self):
return self.GPIO_FUNCTION_NAMES[self.factory.connection.get_mode(self.number)]
@@ -225,7 +221,7 @@ class PiGPIOPin(PiPin):
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(self.address[-1]):
if value != 'up' and self.factory.pi_info.pulled_up(repr(self)):
raise PinFixedPull('%r has a physical pull-up resistor' % self)
try:
self.factory.connection.set_pull_up_down(self.number, self.GPIO_PULL_UPS[value])
@@ -296,19 +292,17 @@ class PiGPIOHardwareSPI(SPI, Device):
def __init__(self, factory, port, device):
self._port = port
self._device = device
self._factory = proxy(factory)
self._factory = factory
self._handle = None
super(PiGPIOHardwareSPI, self).__init__()
pins = SPI_HARDWARE_PINS[port]
self._reserve_pins(*(
factory.address + ('GPIO%d' % pin,)
for pin in (
pins['clock'],
pins['mosi'],
pins['miso'],
pins['select'][device]
)
))
self._factory.reserve_pins(
self,
pins['clock'],
pins['mosi'],
pins['miso'],
pins['select'][device]
)
self._spi_flags = 8 << 16
self._baud = 500000
self._handle = self._factory.connection.spi_open(
@@ -330,7 +324,7 @@ class PiGPIOHardwareSPI(SPI, Device):
if not self.closed:
self._factory.connection.spi_close(self._handle)
self._handle = None
self._release_all()
self._factory.release_all(self)
super(PiGPIOHardwareSPI, self).close()
@property
@@ -397,13 +391,14 @@ class PiGPIOSoftwareSPI(SPI, Device):
self._clock_pin = clock_pin
self._mosi_pin = mosi_pin
self._miso_pin = miso_pin
self._factory = proxy(factory)
self._factory = factory
super(PiGPIOSoftwareSPI, self).__init__()
self._reserve_pins(
factory.pin_address(clock_pin),
factory.pin_address(mosi_pin),
factory.pin_address(miso_pin),
factory.pin_address(select_pin),
self._factory.reserve_pins(
self,
clock_pin,
mosi_pin,
miso_pin,
select_pin,
)
self._spi_flags = 0
self._baud = 100000
@@ -434,7 +429,7 @@ class PiGPIOSoftwareSPI(SPI, Device):
if not self.closed:
self._closed = True
self._factory.connection.bb_spi_close(self._select_pin)
self._release_all()
self.factory.release_all(self)
super(PiGPIOSoftwareSPI, self).close()
@property

View File

@@ -85,7 +85,7 @@ class RPiGPIOPin(LocalPiPin):
def __init__(self, factory, number):
super(RPiGPIOPin, self).__init__(factory, number)
self._pull = 'up' if factory.pi_info.pulled_up(self.address[-1]) else 'floating'
self._pull = 'up' if self.factory.pi_info.pulled_up(self.address[-1]) else 'floating'
self._pwm = None
self._frequency = None
self._duty_cycle = None

View File

@@ -80,7 +80,7 @@ class RPIOPin(LocalPiPin):
def __init__(self, factory, number):
super(RPIOPin, self).__init__(factory, number)
self._pull = 'up' if factory.pi_info.pulled_up(self.address[-1]) else 'floating'
self._pull = 'up' if self.factory.pi_info.pulled_up(self.address[-1]) else 'floating'
self._pwm = False
self._duty_cycle = None
self._bounce = None