From c820636fcba5e6e95cf938760c357ba86cf8f4fc Mon Sep 17 00:00:00 2001 From: Dave Jones Date: Tue, 4 Jul 2017 00:26:41 +0100 Subject: [PATCH 1/4] 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. --- gpiozero/devices.py | 99 +++---------- gpiozero/pins/__init__.py | 99 +++++++------ gpiozero/pins/local.py | 40 ++--- gpiozero/pins/mock.py | 8 +- gpiozero/pins/native.py | 6 +- gpiozero/pins/pi.py | 18 +-- gpiozero/pins/pigpio.py | 45 +++--- gpiozero/pins/rpigpio.py | 2 +- gpiozero/pins/rpio.py | 2 +- tests/test_boards.py | 85 ++++++----- tests/test_devices.py | 6 +- tests/test_inputs.py | 37 +++-- tests/test_outputs.py | 304 ++++++++++++++++++++++---------------- tests/test_spi.py | 48 +++--- 14 files changed, 396 insertions(+), 403 deletions(-) diff --git a/gpiozero/devices.py b/gpiozero/devices.py index 346c677..1a1c687 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -11,7 +11,7 @@ import os import atexit import weakref import warnings -from collections import namedtuple, defaultdict +from collections import namedtuple from itertools import chain from types import FunctionType from threading import Lock @@ -194,77 +194,21 @@ class Device(ValuesMixin, GPIOBase): property, the :attr:`value` property, and the :meth:`close` method). """ pin_factory = None # instance of a Factory sub-class - _reservations = defaultdict(list) # maps pin addresses to lists of devices - _res_lock = Lock() + + def __init__(self, **kwargs): + # Force pin_factory to be keyword-only, even in Python 2 + self.pin_factory = kwargs.pop('pin_factory', Device.pin_factory) + if kwargs: + raise TypeError("Device.__init__() got unexpected keyword " + "argument '%s'" % kwargs.popitem()[0]) + super(Device, self).__init__() def __repr__(self): return "" % (self.__class__.__name__) - 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 Device._res_lock: - for address in addresses: - for device_ref in Device._reservations[address]: - 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) - ) - Device._reservations[address].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 Device._res_lock: - for address in addresses: - Device._reservations[address] = [ - ref for ref in Device._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 Device._res_lock: - Device._reservations = defaultdict(list, { - address: [ - ref for ref in conflictors - if ref() not in (self, None) - ] - for address, conflictors in Device._reservations.items() - }) - def _conflicts_with(self, other): """ - Called by :meth:`_reserve_pin` to test whether the *other* + Called by :meth:`Factory.reserve_pins` 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 @@ -315,6 +259,7 @@ class CompositeDevice(Device): self._named = frozendict({}) self._namedtuple = None self._order = kwargs.pop('_order', None) + pin_factory = kwargs.pop('pin_factory', None) try: if self._order is None: self._order = sorted(kwargs.keys()) @@ -336,7 +281,8 @@ class CompositeDevice(Device): dev.close() raise self._all = args + tuple(kwargs[v] for v in self._order) - super(CompositeDevice, self).__init__() + kwargs = {'pin_factory': pin_factory} if pin_factory is not None else {} + super(CompositeDevice, self).__init__(**kwargs) def __getattr__(self, name): # if _named doesn't exist yet, pretend it's an empty dict @@ -412,20 +358,17 @@ class GPIODevice(Device): this is ``None``, :exc:`GPIOPinMissing` will be raised. If the pin is already in use by another device, :exc:`GPIOPinInUse` will be raised. """ - def __init__(self, pin=None): - super(GPIODevice, self).__init__() + def __init__(self, pin=None, **kwargs): + super(GPIODevice, self).__init__(**kwargs) # self._pin must be set before any possible exceptions can be raised # because it's accessed in __del__. However, it mustn't be given the # value of pin until we've verified that it isn't already allocated self._pin = None if pin is None: raise GPIOPinMissing('No pin given') - if isinstance(pin, Pin): - self._reserve_pins(pin) - else: - # Check you can reserve *before* constructing the pin - self._reserve_pins(Device.pin_factory.pin_address(pin)) - pin = Device.pin_factory.pin(pin) + # Check you can reserve *before* constructing the pin + self.pin_factory.reserve_pins(self, pin) + pin = self.pin_factory.pin(pin) self._pin = pin self._active_state = True self._inactive_state = False @@ -443,7 +386,7 @@ class GPIODevice(Device): def close(self): super(GPIODevice, self).close() if self._pin is not None: - self._release_pins(self._pin) + self.pin_factory.release_pins(self, self._pin.number) self._pin.close() self._pin = None @@ -512,10 +455,10 @@ def _default_pin_factory(name=os.getenv('GPIOZERO_PIN_FACTORY', None)): def _devices_shutdown(): if Device.pin_factory: - with Device._res_lock: + with Device.pin_factory._res_lock: reserved_devices = { dev - for ref_list in Device._reservations.values() + for ref_list in Device.pin_factory._reservations.values() for ref in ref_list for dev in (ref(),) if dev is not None diff --git a/gpiozero/pins/__init__.py b/gpiozero/pins/__init__.py index 60d325c..83ebc0c 100644 --- a/gpiozero/pins/__init__.py +++ b/gpiozero/pins/__init__.py @@ -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 "" 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" diff --git a/gpiozero/pins/local.py b/gpiozero/pins/local.py index 9121dac..65dbcf6 100644 --- a/gpiozero/pins/local.py +++ b/gpiozero/pins/local.py @@ -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) diff --git a/gpiozero/pins/mock.py b/gpiozero/pins/mock.py index 2bcf5a5..23a0e4f 100644 --- a/gpiozero/pins/mock.py +++ b/gpiozero/pins/mock.py @@ -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: diff --git a/gpiozero/pins/native.py b/gpiozero/pins/native.py index eebf0b3..b7de08a 100644 --- a/gpiozero/pins/native.py +++ b/gpiozero/pins/native.py @@ -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] diff --git a/gpiozero/pins/pi.py b/gpiozero/pins/pi.py index 1ec024d..f2c02a8 100644 --- a/gpiozero/pins/pi.py +++ b/gpiozero/pins/pi.py @@ -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 diff --git a/gpiozero/pins/pigpio.py b/gpiozero/pins/pigpio.py index fe5e59e..af82597 100644 --- a/gpiozero/pins/pigpio.py +++ b/gpiozero/pins/pigpio.py @@ -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 diff --git a/gpiozero/pins/rpigpio.py b/gpiozero/pins/rpigpio.py index e0274b6..4c43712 100644 --- a/gpiozero/pins/rpigpio.py +++ b/gpiozero/pins/rpigpio.py @@ -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 diff --git a/gpiozero/pins/rpio.py b/gpiozero/pins/rpio.py index 12fced2..5a51e41 100644 --- a/gpiozero/pins/rpio.py +++ b/gpiozero/pins/rpio.py @@ -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 diff --git a/tests/test_boards.py b/tests/test_boards.py index 983bfee..6251b73 100644 --- a/tests/test_boards.py +++ b/tests/test_boards.py @@ -36,7 +36,6 @@ def setup_function(function): def teardown_function(function): Device.pin_factory.reset() - Device._reservations.clear() def teardown_module(module): # make sure we reset the default @@ -47,7 +46,7 @@ def test_composite_output_on_off(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with CompositeOutputDevice(OutputDevice(pin1), OutputDevice(pin2), foo=OutputDevice(pin3)) as device: + with CompositeOutputDevice(OutputDevice(2), OutputDevice(3), foo=OutputDevice(4)) as device: device.on() assert all((pin1.state, pin2.state, pin3.state)) device.off() @@ -57,7 +56,7 @@ def test_composite_output_toggle(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with CompositeOutputDevice(OutputDevice(pin1), OutputDevice(pin2), foo=OutputDevice(pin3)) as device: + with CompositeOutputDevice(OutputDevice(2), OutputDevice(3), foo=OutputDevice(4)) as device: device.toggle() assert all((pin1.state, pin2.state, pin3.state)) device[0].off() @@ -70,7 +69,7 @@ def test_composite_output_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with CompositeOutputDevice(OutputDevice(pin1), OutputDevice(pin2), foo=OutputDevice(pin3)) as device: + with CompositeOutputDevice(OutputDevice(2), OutputDevice(3), foo=OutputDevice(4)) as device: assert device.value == (0, 0, 0) device.toggle() assert device.value == (1, 1, 1) @@ -83,7 +82,7 @@ def test_led_board_on_off(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBoard(pin1, pin2, foo=pin3) as board: + with LEDBoard(2, 3, foo=4) as board: assert isinstance(board[0], LED) assert isinstance(board[1], LED) assert isinstance(board[2], LED) @@ -140,7 +139,7 @@ def test_led_board_active_low(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBoard(pin1, pin2, foo=pin3, active_high=False) as board: + with LEDBoard(2, 3, foo=4, active_high=False) as board: assert not board.active_high assert not board[0].active_high assert not board[1].active_high @@ -164,7 +163,7 @@ def test_led_board_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBoard(pin1, pin2, foo=pin3) as board: + with LEDBoard(2, 3, foo=4) as board: assert board.value == (0, 0, 0) board.value = (0, 1, 0) assert board.value == (0, 1, 0) @@ -175,7 +174,7 @@ def test_led_board_pwm_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBoard(pin1, pin2, foo=pin3, pwm=True) as board: + with LEDBoard(2, 3, foo=4, pwm=True) as board: assert board.value == (0, 0, 0) board.value = (0, 1, 0) assert board.value == (0, 1, 0) @@ -186,7 +185,7 @@ def test_led_board_pwm_bad_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBoard(pin1, pin2, foo=pin3, pwm=True) as board: + with LEDBoard(2, 3, foo=4, pwm=True) as board: with pytest.raises(ValueError): board.value = (-1, 0, 0) with pytest.raises(ValueError): @@ -196,20 +195,20 @@ def test_led_board_initial_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBoard(pin1, pin2, foo=pin3, initial_value=0) as board: + with LEDBoard(2, 3, foo=4, initial_value=0) as board: assert board.value == (0, 0, 0) - with LEDBoard(pin1, pin2, foo=pin3, initial_value=1) as board: + with LEDBoard(2, 3, foo=4, initial_value=1) as board: assert board.value == (1, 1, 1) def test_led_board_pwm_initial_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=0) as board: + with LEDBoard(2, 3, foo=4, pwm=True, initial_value=0) as board: assert board.value == (0, 0, 0) - with LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=1) as board: + with LEDBoard(2, 3, foo=4, pwm=True, initial_value=1) as board: assert board.value == (1, 1, 1) - with LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=0.5) as board: + with LEDBoard(2, 3, foo=4, pwm=True, initial_value=0.5) as board: assert board.value == (0.5, 0.5, 0.5) def test_led_board_pwm_bad_initial_value(): @@ -217,15 +216,15 @@ def test_led_board_pwm_bad_initial_value(): pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) with pytest.raises(ValueError): - LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=-1) + LEDBoard(2, 3, foo=4, pwm=True, initial_value=-1) with pytest.raises(ValueError): - LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=2) + LEDBoard(2, 3, foo=4, pwm=True, initial_value=2) def test_led_board_nested(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBoard(pin1, LEDBoard(pin2, pin3)) as board: + with LEDBoard(2, LEDBoard(3, 4)) as board: assert list(led.pin for led in board.leds) == [pin1, pin2, pin3] assert board.value == (0, (0, 0)) board.value = (1, (0, 1)) @@ -237,7 +236,7 @@ def test_led_board_bad_blink(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBoard(pin1, LEDBoard(pin2, pin3)) as board: + with LEDBoard(2, LEDBoard(3, 4)) as board: with pytest.raises(ValueError): board.blink(fade_in_time=1, fade_out_time=1) with pytest.raises(ValueError): @@ -251,7 +250,7 @@ def test_led_board_blink_background(): pin1 = Device.pin_factory.pin(4) pin2 = Device.pin_factory.pin(5) pin3 = Device.pin_factory.pin(6) - with LEDBoard(pin1, LEDBoard(pin2, pin3)) as board: + with LEDBoard(4, LEDBoard(5, 6)) as board: # Instantiation takes a long enough time that it throws off our timing # here! pin1.clear_states() @@ -276,7 +275,7 @@ def test_led_board_blink_foreground(): pin1 = Device.pin_factory.pin(4) pin2 = Device.pin_factory.pin(5) pin3 = Device.pin_factory.pin(6) - with LEDBoard(pin1, LEDBoard(pin2, pin3)) as board: + with LEDBoard(4, LEDBoard(5, 6)) as board: pin1.clear_states() pin2.clear_states() pin3.clear_states() @@ -298,7 +297,7 @@ def test_led_board_blink_control(): pin1 = Device.pin_factory.pin(4) pin2 = Device.pin_factory.pin(5) pin3 = Device.pin_factory.pin(6) - with LEDBoard(pin1, LEDBoard(pin2, pin3)) as board: + with LEDBoard(4, LEDBoard(5, 6)) as board: pin1.clear_states() pin2.clear_states() pin3.clear_states() @@ -326,7 +325,7 @@ def test_led_board_blink_take_over(): pin1 = Device.pin_factory.pin(4) pin2 = Device.pin_factory.pin(5) pin3 = Device.pin_factory.pin(6) - with LEDBoard(pin1, LEDBoard(pin2, pin3)) as board: + with LEDBoard(4, LEDBoard(5, 6)) as board: pin1.clear_states() pin2.clear_states() pin3.clear_states() @@ -351,7 +350,7 @@ def test_led_board_blink_control_all(): pin1 = Device.pin_factory.pin(4) pin2 = Device.pin_factory.pin(5) pin3 = Device.pin_factory.pin(6) - with LEDBoard(pin1, LEDBoard(pin2, pin3)) as board: + with LEDBoard(4, LEDBoard(5, 6)) as board: pin1.clear_states() pin2.clear_states() pin3.clear_states() @@ -376,7 +375,7 @@ def test_led_board_blink_interrupt_on(): pin1 = Device.pin_factory.pin(4) pin2 = Device.pin_factory.pin(5) pin3 = Device.pin_factory.pin(6) - with LEDBoard(pin1, LEDBoard(pin2, pin3)) as board: + with LEDBoard(4, LEDBoard(5, 6)) as board: board.blink(1, 0.1) sleep(0.2) board.off() # should interrupt while on @@ -388,7 +387,7 @@ def test_led_board_blink_interrupt_off(): pin1 = Device.pin_factory.pin(4) pin2 = Device.pin_factory.pin(5) pin3 = Device.pin_factory.pin(6) - with LEDBoard(pin1, LEDBoard(pin2, pin3)) as board: + with LEDBoard(4, LEDBoard(5, 6)) as board: pin1.clear_states() pin2.clear_states() pin3.clear_states() @@ -405,7 +404,7 @@ def test_led_board_fade_background(): pin1 = Device.pin_factory.pin(4) pin2 = Device.pin_factory.pin(5) pin3 = Device.pin_factory.pin(6) - with LEDBoard(pin1, LEDBoard(pin2, pin3, pwm=True), pwm=True) as board: + with LEDBoard(4, LEDBoard(5, 6, pwm=True), pwm=True) as board: pin1.clear_states() pin2.clear_states() pin3.clear_states() @@ -442,7 +441,7 @@ def test_led_bar_graph_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBarGraph(pin1, pin2, pin3) as graph: + with LEDBarGraph(2, 3, 4) as graph: assert isinstance(graph[0], LED) assert isinstance(graph[1], LED) assert isinstance(graph[2], LED) @@ -475,7 +474,7 @@ def test_led_bar_graph_active_low(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBarGraph(pin1, pin2, pin3, active_high=False) as graph: + with LEDBarGraph(2, 3, 4, active_high=False) as graph: assert not graph.active_high assert not graph[0].active_high assert not graph[1].active_high @@ -497,7 +496,7 @@ def test_led_bar_graph_pwm_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBarGraph(pin1, pin2, pin3, pwm=True) as graph: + with LEDBarGraph(2, 3, 4, pwm=True) as graph: assert isinstance(graph[0], PWMLED) assert isinstance(graph[1], PWMLED) assert isinstance(graph[2], PWMLED) @@ -524,7 +523,7 @@ def test_led_bar_graph_bad_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBarGraph(pin1, pin2, pin3) as graph: + with LEDBarGraph(2, 3, 4) as graph: with pytest.raises(ValueError): graph.value = -2 with pytest.raises(ValueError): @@ -535,20 +534,20 @@ def test_led_bar_graph_bad_init(): pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) with pytest.raises(TypeError): - LEDBarGraph(pin1, pin2, foo=pin3) + LEDBarGraph(2, 3, foo=4) with pytest.raises(ValueError): - LEDBarGraph(pin1, pin2, pin3, initial_value=-2) + LEDBarGraph(2, 3, 4, initial_value=-2) with pytest.raises(ValueError): - LEDBarGraph(pin1, pin2, pin3, initial_value=2) + LEDBarGraph(2, 3, 4, initial_value=2) def test_led_bar_graph_initial_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBarGraph(pin1, pin2, pin3, initial_value=1/3) as graph: + with LEDBarGraph(2, 3, 4, initial_value=1/3) as graph: assert graph.value == 1/3 assert pin1.state and not (pin2.state or pin3.state) - with LEDBarGraph(pin1, pin2, pin3, initial_value=-1/3) as graph: + with LEDBarGraph(2, 3, 4, initial_value=-1/3) as graph: assert graph.value == -1/3 assert pin3.state and not (pin1.state or pin2.state) @@ -556,10 +555,10 @@ def test_led_bar_graph_pwm_initial_value(): pin1 = Device.pin_factory.pin(2) pin2 = Device.pin_factory.pin(3) pin3 = Device.pin_factory.pin(4) - with LEDBarGraph(pin1, pin2, pin3, pwm=True, initial_value=0.5) as graph: + with LEDBarGraph(2, 3, 4, pwm=True, initial_value=0.5) as graph: assert graph.value == 0.5 assert (pin1.state, pin2.state, pin3.state) == (1, 0.5, 0) - with LEDBarGraph(pin1, pin2, pin3, pwm=True, initial_value=-0.5) as graph: + with LEDBarGraph(2, 3, 4, pwm=True, initial_value=-0.5) as graph: assert graph.value == -0.5 assert (pin1.state, pin2.state, pin3.state) == (0, 0.5, 1) @@ -585,7 +584,7 @@ def test_traffic_lights(): red_pin = Device.pin_factory.pin(2) amber_pin = Device.pin_factory.pin(3) green_pin = Device.pin_factory.pin(4) - with TrafficLights(red_pin, amber_pin, green_pin) as board: + with TrafficLights(2, 3, 4) as board: board.red.on() assert board.red.value assert not board.amber.value @@ -598,7 +597,7 @@ def test_traffic_lights(): assert amber_pin.state board.yellow.off() assert not amber_pin.state - with TrafficLights(red=red_pin, yellow=amber_pin, green=green_pin) as board: + with TrafficLights(red=2, yellow=3, green=4) as board: board.yellow.on() assert not board.red.value assert board.amber.value @@ -618,7 +617,7 @@ def test_traffic_lights_bad_init(): green_pin = Device.pin_factory.pin(4) yellow_pin = Device.pin_factory.pin(5) with pytest.raises(ValueError): - TrafficLights(red=red_pin, amber=amber_pin, yellow=yellow_pin, green=green_pin) + TrafficLights(red=2, amber=3, yellow=5, green=4) def test_pi_traffic(): pins = [Device.pin_factory.pin(n) for n in (9, 10, 11)] @@ -677,9 +676,9 @@ def test_traffic_lights_buzzer(): buzzer_pin = Device.pin_factory.pin(5) button_pin = Device.pin_factory.pin(6) with TrafficLightsBuzzer( - TrafficLights(red_pin, amber_pin, green_pin), - Buzzer(buzzer_pin), - Button(button_pin)) as board: + TrafficLights(2, 3, 4), + Buzzer(5), + Button(6)) as board: board.lights.red.on() board.buzzer.on() assert red_pin.state diff --git a/tests/test_devices.py b/tests/test_devices.py index 7293741..aa7c83c 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -33,7 +33,7 @@ def test_device_non_physical(): def test_device_init(): pin = Device.pin_factory.pin(2) - with GPIODevice(pin) as device: + with GPIODevice(2) as device: assert not device.closed assert device.pin == pin @@ -56,9 +56,9 @@ def test_device_close(): def test_device_reopen_same_pin(): pin = Device.pin_factory.pin(2) - with GPIODevice(pin) as device: + with GPIODevice(2) as device: pass - with GPIODevice(pin) as device2: + with GPIODevice(2) as device2: assert not device2.closed assert device2.pin is pin assert device.closed diff --git a/tests/test_inputs.py b/tests/test_inputs.py index d05940c..0d9c80a 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -18,22 +18,21 @@ from gpiozero import * def teardown_function(function): Device.pin_factory.reset() - Device._reservations.clear() def test_input_initial_values(): pin = Device.pin_factory.pin(4) - with InputDevice(pin, pull_up=True) as device: + with InputDevice(4, pull_up=True) as device: assert pin.function == 'input' assert pin.pull == 'up' assert device.pull_up - with InputDevice(pin, pull_up=False) as device: + with InputDevice(4, pull_up=False) as device: assert pin.pull == 'down' assert not device.pull_up def test_input_is_active_low(): pin = Device.pin_factory.pin(2) - with InputDevice(pin, pull_up=True) as device: + with InputDevice(2, pull_up=True) as device: pin.drive_high() assert not device.is_active assert repr(device) == '' @@ -43,7 +42,7 @@ def test_input_is_active_low(): def test_input_is_active_high(): pin = Device.pin_factory.pin(4) - with InputDevice(pin, pull_up=False) as device: + with InputDevice(4, pull_up=False) as device: pin.drive_high() assert device.is_active assert repr(device) == '' @@ -54,12 +53,12 @@ def test_input_is_active_high(): def test_input_pulled_up(): pin = Device.pin_factory.pin(2) with pytest.raises(PinFixedPull): - InputDevice(pin, pull_up=False) + InputDevice(2, pull_up=False) def test_input_event_activated(): event = Event() pin = Device.pin_factory.pin(4) - with DigitalInputDevice(pin) as device: + with DigitalInputDevice(4) as device: device.when_activated = lambda: event.set() assert not event.is_set() pin.drive_high() @@ -68,7 +67,7 @@ def test_input_event_activated(): def test_input_event_deactivated(): event = Event() pin = Device.pin_factory.pin(4) - with DigitalInputDevice(pin) as device: + with DigitalInputDevice(4) as device: device.when_deactivated = lambda: event.set() assert not event.is_set() pin.drive_high() @@ -84,7 +83,7 @@ def test_input_partial_callback(): return a + b bar = partial(foo, 1) baz = partial(bar, 2) - with DigitalInputDevice(pin) as device: + with DigitalInputDevice(4) as device: device.when_activated = baz assert not event.is_set() pin.drive_high() @@ -92,20 +91,20 @@ def test_input_partial_callback(): def test_input_wait_active(): pin = Device.pin_factory.pin(4) - with DigitalInputDevice(pin) as device: + with DigitalInputDevice(4) as device: pin.drive_high() assert device.wait_for_active(1) assert not device.wait_for_inactive(0) def test_input_wait_inactive(): pin = Device.pin_factory.pin(4) - with DigitalInputDevice(pin) as device: + with DigitalInputDevice(4) as device: assert device.wait_for_inactive(1) assert not device.wait_for_active(0) def test_input_smoothed_attrib(): pin = Device.pin_factory.pin(4) - with SmoothedInputDevice(pin, threshold=0.5, queue_len=5, partial=False) as device: + with SmoothedInputDevice(4, threshold=0.5, queue_len=5, partial=False) as device: assert repr(device) == '' assert device.threshold == 0.5 assert device.queue_len == 5 @@ -117,7 +116,7 @@ def test_input_smoothed_attrib(): def test_input_smoothed_values(): pin = Device.pin_factory.pin(4) - with SmoothedInputDevice(pin) as device: + with SmoothedInputDevice(4) as device: device._queue.start() assert not device.is_active pin.drive_high() @@ -127,7 +126,7 @@ def test_input_smoothed_values(): def test_input_button(): pin = Device.pin_factory.pin(2) - with Button(pin) as button: + with Button(2) as button: assert pin.pull == 'up' assert not button.is_pressed pin.drive_low() @@ -139,7 +138,7 @@ def test_input_button(): def test_input_line_sensor(): pin = Device.pin_factory.pin(4) - with LineSensor(pin) as sensor: + with LineSensor(4) as sensor: pin.drive_low() # logic is inverted for line sensor assert sensor.wait_for_line(1) assert sensor.line_detected @@ -149,7 +148,7 @@ def test_input_line_sensor(): def test_input_motion_sensor(): pin = Device.pin_factory.pin(4) - with MotionSensor(pin) as sensor: + with MotionSensor(4) as sensor: pin.drive_high() assert sensor.wait_for_motion(1) assert sensor.motion_detected @@ -161,7 +160,7 @@ def test_input_motion_sensor(): reason='timing is too random on pypy') def test_input_light_sensor(): pin = Device.pin_factory.pin(4, pin_class=MockChargingPin) - with LightSensor(pin) as sensor: + with LightSensor(4) as sensor: pin.charge_time = 0.1 assert sensor.wait_for_dark(1) pin.charge_time = 0.0 @@ -173,10 +172,10 @@ def test_input_distance_sensor(): echo_pin = Device.pin_factory.pin(4) trig_pin = Device.pin_factory.pin(5, pin_class=MockTriggerPin, echo_pin=echo_pin, echo_time=0.02) with pytest.raises(ValueError): - DistanceSensor(echo_pin, trig_pin, max_distance=-1) + DistanceSensor(4, 5, max_distance=-1) # normal queue len is large (because the sensor is *really* jittery) but # we want quick tests and we've got precisely controlled pins :) - with DistanceSensor(echo_pin, trig_pin, queue_len=5, max_distance=1) as sensor: + with DistanceSensor(4, 5, queue_len=5, max_distance=1) as sensor: assert sensor.max_distance == 1 assert sensor.trigger is trig_pin assert sensor.echo is echo_pin diff --git a/tests/test_outputs.py b/tests/test_outputs.py index b50dda5..5403023 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -16,29 +16,73 @@ except ImportError: import pytest -from gpiozero.pins.mock import MockPWMPin +from gpiozero.pins.mock import MockPin, MockPWMPin from gpiozero import * +def setup_function(function): + # dirty, but it does the job + Device.pin_factory.pin_class = MockPWMPin if function.__name__ in ( + 'test_output_pwm_states', + 'test_output_pwm_read', + 'test_output_pwm_write', + 'test_output_pwm_toggle', + 'test_output_pwm_active_high_read', + 'test_output_pwm_bad_value', + 'test_output_pwm_write_closed', + 'test_output_pwm_write_silly', + 'test_output_pwm_blink_background', + 'test_output_pwm_blink_foreground', + 'test_output_pwm_fade_background', + 'test_output_pwm_fade_foreground', + 'test_output_pwm_pulse_background', + 'test_output_pwm_pulse_foreground', + 'test_output_pwm_blink_interrupt', + 'test_rgbled_initial_value', + 'test_rgbled_initial_bad_value', + 'test_rgbled_value', + 'test_rgbled_bad_value', + 'test_rgbled_toggle', + 'test_rgbled_blink_background', + 'test_rgbled_blink_foreground', + 'test_rgbled_fade_background', + 'test_rgbled_fade_foreground', + 'test_rgbled_pulse_background', + 'test_rgbled_pulse_foreground', + 'test_rgbled_blink_interrupt', + 'test_rgbled_close', + 'test_motor_pins', + 'test_motor_close', + 'test_motor_value', + 'test_motor_bad_value', + 'test_motor_reverse', + 'test_servo_pins', + 'test_servo_bad_value', + 'test_servo_close', + 'test_servo_pulse_width', + 'test_servo_values', + 'test_angular_servo_range', + 'test_angular_servo_angles', + ) else MockPin + def teardown_function(function): Device.pin_factory.reset() - Device._reservations.clear() def test_output_initial_values(): pin = Device.pin_factory.pin(2) - with OutputDevice(pin, initial_value=False) as device: + with OutputDevice(2, initial_value=False) as device: assert pin.function == 'output' assert not pin.state - with OutputDevice(pin, initial_value=True) as device: + with OutputDevice(2, initial_value=True) as device: assert pin.state state = pin.state - with OutputDevice(pin, initial_value=None) as device: + with OutputDevice(2, initial_value=None) as device: assert state == pin.state def test_output_write_active_high(): pin = Device.pin_factory.pin(2) - with OutputDevice(pin) as device: + with OutputDevice(2) as device: device.on() assert pin.state device.off() @@ -46,14 +90,14 @@ def test_output_write_active_high(): def test_output_write_active_low(): pin = Device.pin_factory.pin(2) - with OutputDevice(pin, active_high=False) as device: + with OutputDevice(2, active_high=False) as device: device.on() assert not pin.state device.off() assert pin.state def test_output_write_closed(): - with OutputDevice(Device.pin_factory.pin(2)) as device: + with OutputDevice(2) as device: device.close() assert device.closed device.close() @@ -63,14 +107,14 @@ def test_output_write_closed(): def test_output_write_silly(): pin = Device.pin_factory.pin(2) - with OutputDevice(pin) as device: + with OutputDevice(2) as device: pin.function = 'input' with pytest.raises(AttributeError): device.on() def test_output_value(): pin = Device.pin_factory.pin(2) - with OutputDevice(pin) as device: + with OutputDevice(2) as device: assert not device.value assert not pin.state device.on() @@ -82,7 +126,7 @@ def test_output_value(): def test_output_digital_toggle(): pin = Device.pin_factory.pin(2) - with DigitalOutputDevice(pin) as device: + with DigitalOutputDevice(2) as device: assert not device.value assert not pin.state device.toggle() @@ -96,7 +140,7 @@ def test_output_digital_toggle(): reason='timing is too random on pypy') def test_output_blink_background(): pin = Device.pin_factory.pin(4) - with DigitalOutputDevice(pin) as device: + with DigitalOutputDevice(4) as device: start = time() device.blink(0.1, 0.1, n=2) assert isclose(time() - start, 0, abs_tol=0.05) @@ -114,7 +158,7 @@ def test_output_blink_background(): reason='timing is too random on pypy') def test_output_blink_foreground(): pin = Device.pin_factory.pin(4) - with DigitalOutputDevice(pin) as device: + with DigitalOutputDevice(4) as device: start = time() device.blink(0.1, 0.1, n=2, background=False) assert isclose(time() - start, 0.4, abs_tol=0.05) @@ -128,7 +172,7 @@ def test_output_blink_foreground(): def test_output_blink_interrupt_on(): pin = Device.pin_factory.pin(4) - with DigitalOutputDevice(pin) as device: + with DigitalOutputDevice(4) as device: device.blink(1, 0.1) sleep(0.2) device.off() # should interrupt while on @@ -136,7 +180,7 @@ def test_output_blink_interrupt_on(): def test_output_blink_interrupt_off(): pin = Device.pin_factory.pin(4) - with DigitalOutputDevice(pin) as device: + with DigitalOutputDevice(4) as device: device.blink(0.1, 1) sleep(0.2) device.off() # should interrupt while off @@ -144,23 +188,23 @@ def test_output_blink_interrupt_off(): def test_output_pwm_bad_initial_value(): with pytest.raises(ValueError): - PWMOutputDevice(Device.pin_factory.pin(2), initial_value=2) + PWMOutputDevice(2, initial_value=2) def test_output_pwm_not_supported(): with pytest.raises(AttributeError): - PWMOutputDevice(Device.pin_factory.pin(2)) + PWMOutputDevice(2) def test_output_pwm_states(): - pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(4) + with PWMOutputDevice(4) as device: device.value = 0.1 device.value = 0.2 device.value = 0.0 pin.assert_states([0.0, 0.1, 0.2, 0.0]) def test_output_pwm_read(): - pin = Device.pin_factory.pin(2, pin_class=MockPWMPin) - with PWMOutputDevice(pin, frequency=100) as device: + pin = Device.pin_factory.pin(2) + with PWMOutputDevice(2, frequency=100) as device: assert device.frequency == 100 device.value = 0.1 assert isclose(device.value, 0.1) @@ -172,15 +216,15 @@ def test_output_pwm_read(): assert device.frequency is None def test_output_pwm_write(): - pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(4) + with PWMOutputDevice(4) as device: device.on() device.off() pin.assert_states([False, True, False]) def test_output_pwm_toggle(): - pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(4) + with PWMOutputDevice(4) as device: device.toggle() device.value = 0.5 device.value = 0.1 @@ -189,8 +233,8 @@ def test_output_pwm_toggle(): pin.assert_states([False, True, 0.5, 0.1, 0.9, False]) def test_output_pwm_active_high_read(): - pin = Device.pin_factory.pin(2, pin_class=MockPWMPin) - with PWMOutputDevice(pin, active_high=False) as device: + pin = Device.pin_factory.pin(2) + with PWMOutputDevice(2, active_high=False) as device: device.value = 0.1 assert isclose(device.value, 0.1) assert isclose(pin.state, 0.9) @@ -198,19 +242,21 @@ def test_output_pwm_active_high_read(): assert device.value def test_output_pwm_bad_value(): - with PWMOutputDevice(Device.pin_factory.pin(2, pin_class=MockPWMPin)) as device: + pin = Device.pin_factory.pin(2) + with PWMOutputDevice(2) as device: with pytest.raises(ValueError): device.value = 2 def test_output_pwm_write_closed(): - with PWMOutputDevice(Device.pin_factory.pin(2, pin_class=MockPWMPin)) as device: + pin = Device.pin_factory.pin(2) + with PWMOutputDevice(2) as device: device.close() with pytest.raises(GPIODeviceClosed): device.on() def test_output_pwm_write_silly(): - pin = Device.pin_factory.pin(2, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(2) + with PWMOutputDevice(2) as device: pin.function = 'input' with pytest.raises(AttributeError): device.off() @@ -218,8 +264,8 @@ def test_output_pwm_write_silly(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_output_pwm_blink_background(): - pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(4) + with PWMOutputDevice(4) as device: start = time() device.blink(0.1, 0.1, n=2) assert isclose(time() - start, 0, abs_tol=0.05) @@ -236,8 +282,8 @@ def test_output_pwm_blink_background(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_output_pwm_blink_foreground(): - pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(4) + with PWMOutputDevice(4) as device: start = time() device.blink(0.1, 0.1, n=2, background=False) assert isclose(time() - start, 0.4, abs_tol=0.05) @@ -252,8 +298,8 @@ def test_output_pwm_blink_foreground(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_output_pwm_fade_background(): - pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(4) + with PWMOutputDevice(4) as device: start = time() device.blink(0, 0, 0.2, 0.2, n=2) assert isclose(time() - start, 0, abs_tol=0.05) @@ -286,8 +332,8 @@ def test_output_pwm_fade_background(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_output_pwm_fade_foreground(): - pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(4) + with PWMOutputDevice(4) as device: start = time() device.blink(0, 0, 0.2, 0.2, n=2, background=False) assert isclose(time() - start, 0.8, abs_tol=0.05) @@ -318,8 +364,8 @@ def test_output_pwm_fade_foreground(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_output_pwm_pulse_background(): - pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(4) + with PWMOutputDevice(4) as device: start = time() device.pulse(0.2, 0.2, n=2) assert isclose(time() - start, 0, abs_tol=0.05) @@ -352,8 +398,8 @@ def test_output_pwm_pulse_background(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_output_pwm_pulse_foreground(): - pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(4) + with PWMOutputDevice(4) as device: start = time() device.pulse(0.2, 0.2, n=2, background=False) assert isclose(time() - start, 0.8, abs_tol=0.05) @@ -382,8 +428,8 @@ def test_output_pwm_pulse_foreground(): ]) def test_output_pwm_blink_interrupt(): - pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) - with PWMOutputDevice(pin) as device: + pin = Device.pin_factory.pin(4) + with PWMOutputDevice(4) as device: device.blink(1, 0.1) sleep(0.2) device.off() # should interrupt while on @@ -394,8 +440,8 @@ def test_rgbled_missing_pins(): RGBLED() def test_rgbled_initial_value(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) - with RGBLED(r, g, b, initial_value=(0.1, 0.2, 0)) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) + with RGBLED(1, 2, 3, initial_value=(0.1, 0.2, 0)) as device: assert r.frequency assert g.frequency assert b.frequency @@ -405,24 +451,24 @@ def test_rgbled_initial_value(): def test_rgbled_initial_value_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) - with RGBLED(r, g, b, pwm=False, initial_value=(0, 1, 1)) as device: + with RGBLED(1, 2, 3, pwm=False, initial_value=(0, 1, 1)) as device: assert r.state == 0 assert g.state == 1 assert b.state == 1 def test_rgbled_initial_bad_value(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) + r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) with pytest.raises(ValueError): - RGBLED(r, g, b, initial_value=(0.1, 0.2, 1.2)) + RGBLED(1, 2, 3, initial_value=(0.1, 0.2, 1.2)) def test_rgbled_initial_bad_value_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) with pytest.raises(ValueError): - RGBLED(r, g, b, pwm=False, initial_value=(0.1, 0.2, 0)) + RGBLED(1, 2, 3, pwm=False, initial_value=(0.1, 0.2, 0)) def test_rgbled_value(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) + with RGBLED(1, 2, 3) as device: assert isinstance(device._leds[0], PWMLED) assert isinstance(device._leds[1], PWMLED) assert isinstance(device._leds[2], PWMLED) @@ -440,7 +486,7 @@ def test_rgbled_value(): def test_rgbled_value_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: assert isinstance(device._leds[0], LED) assert isinstance(device._leds[1], LED) assert isinstance(device._leds[2], LED) @@ -454,35 +500,35 @@ def test_rgbled_value_nonpwm(): assert device.value == (0, 0, 0) def test_rgbled_bad_value(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) + with RGBLED(1, 2, 3) as device: with pytest.raises(ValueError): device.value = (2, 0, 0) - with RGBLED(r, g, b) as device: + with RGBLED(1, 2, 3) as device: with pytest.raises(ValueError): device.value = (0, -1, 0) def test_rgbled_bad_value_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: with pytest.raises(ValueError): device.value = (2, 0, 0) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: with pytest.raises(ValueError): device.value = (0, -1, 0) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: with pytest.raises(ValueError): device.value = (0.5, 0, 0) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: with pytest.raises(ValueError): device.value = (0, 0.5, 0) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: with pytest.raises(ValueError): device.value = (0, 0, 0.5) def test_rgbled_toggle(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) + with RGBLED(1, 2, 3) as device: assert not device.is_active assert device.value == (0, 0, 0) device.toggle() @@ -494,7 +540,7 @@ def test_rgbled_toggle(): def test_rgbled_toggle_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: assert not device.is_active assert device.value == (0, 0, 0) device.toggle() @@ -506,7 +552,7 @@ def test_rgbled_toggle_nonpwm(): def test_rgbled_blink_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: with pytest.raises(ValueError): device.blink(fade_in_time=1) with pytest.raises(ValueError): @@ -515,8 +561,8 @@ def test_rgbled_blink_nonpwm(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_blink_background(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (4, 5, 6)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (4, 5, 6)) + with RGBLED(1, 2, 3) as device: start = time() device.blink(0.1, 0.1, n=2) assert isclose(time() - start, 0, abs_tol=0.05) @@ -537,7 +583,7 @@ def test_rgbled_blink_background(): reason='timing is too random on pypy') def test_rgbled_blink_background_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (4, 5, 6)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: start = time() device.blink(0.1, 0.1, n=2) assert isclose(time() - start, 0, abs_tol=0.05) @@ -557,8 +603,8 @@ def test_rgbled_blink_background_nonpwm(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_blink_foreground(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (4, 5, 6)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (4, 5, 6)) + with RGBLED(1, 2, 3) as device: start = time() device.blink(0.1, 0.1, n=2, background=False) assert isclose(time() - start, 0.4, abs_tol=0.05) @@ -577,7 +623,7 @@ def test_rgbled_blink_foreground(): reason='timing is too random on pypy') def test_rgbled_blink_foreground_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (4, 5, 6)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: start = time() device.blink(0.1, 0.1, n=2, background=False) assert isclose(time() - start, 0.4, abs_tol=0.05) @@ -595,8 +641,8 @@ def test_rgbled_blink_foreground_nonpwm(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_fade_background(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (4, 5, 6)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (4, 5, 6)) + with RGBLED(1, 2, 3) as device: start = time() device.blink(0, 0, 0.2, 0.2, n=2) assert isclose(time() - start, 0, abs_tol=0.05) @@ -631,15 +677,15 @@ def test_rgbled_fade_background(): def test_rgbled_fade_background_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: with pytest.raises(ValueError): device.blink(0, 0, 0.2, 0.2, n=2) @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_fade_foreground(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (4, 5, 6)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (4, 5, 6)) + with RGBLED(1, 2, 3) as device: start = time() device.blink(0, 0, 0.2, 0.2, n=2, background=False) assert isclose(time() - start, 0.8, abs_tol=0.05) @@ -672,15 +718,15 @@ def test_rgbled_fade_foreground(): def test_rgbled_fade_foreground_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: with pytest.raises(ValueError): device.blink(0, 0, 0.2, 0.2, n=2, background=False) @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_pulse_background(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (4, 5, 6)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (4, 5, 6)) + with RGBLED(1, 2, 3) as device: start = time() device.pulse(0.2, 0.2, n=2) assert isclose(time() - start, 0, abs_tol=0.05) @@ -715,15 +761,15 @@ def test_rgbled_pulse_background(): def test_rgbled_pulse_background_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: with pytest.raises(ValueError): device.pulse(0.2, 0.2, n=2) @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_pulse_foreground(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (4, 5, 6)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (4, 5, 6)) + with RGBLED(1, 2, 3) as device: start = time() device.pulse(0.2, 0.2, n=2, background=False) assert isclose(time() - start, 0.8, abs_tol=0.05) @@ -756,13 +802,13 @@ def test_rgbled_pulse_foreground(): def test_rgbled_pulse_foreground_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: with pytest.raises(ValueError): device.pulse(0.2, 0.2, n=2, background=False) def test_rgbled_blink_interrupt(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (4, 5, 6)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (4, 5, 6)) + with RGBLED(1, 2, 3) as device: device.blink(1, 0.1) sleep(0.2) device.off() # should interrupt while on @@ -772,7 +818,7 @@ def test_rgbled_blink_interrupt(): def test_rgbled_blink_interrupt_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (4, 5, 6)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: device.blink(1, 0.1) sleep(0.2) device.off() # should interrupt while on @@ -781,8 +827,8 @@ def test_rgbled_blink_interrupt_nonpwm(): b.assert_states([0, 1, 0]) def test_rgbled_close(): - r, g, b = (Device.pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) - with RGBLED(r, g, b) as device: + r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) + with RGBLED(1, 2, 3) as device: assert not device.closed device.close() assert device.closed @@ -791,7 +837,7 @@ def test_rgbled_close(): def test_rgbled_close_nonpwm(): r, g, b = (Device.pin_factory.pin(i) for i in (1, 2, 3)) - with RGBLED(r, g, b, pwm=False) as device: + with RGBLED(1, 2, 3, pwm=False) as device: assert not device.closed device.close() assert device.closed @@ -803,9 +849,9 @@ def test_motor_missing_pins(): Motor() def test_motor_pins(): - f = Device.pin_factory.pin(1, pin_class=MockPWMPin) - b = Device.pin_factory.pin(2, pin_class=MockPWMPin) - with Motor(f, b) as device: + f = Device.pin_factory.pin(1) + b = Device.pin_factory.pin(2) + with Motor(1, 2) as device: assert device.forward_device.pin is f assert isinstance(device.forward_device, PWMOutputDevice) assert device.backward_device.pin is b @@ -814,16 +860,16 @@ def test_motor_pins(): def test_motor_pins_nonpwm(): f = Device.pin_factory.pin(1) b = Device.pin_factory.pin(2) - with Motor(f, b, pwm=False) as device: + with Motor(1, 2, pwm=False) as device: assert device.forward_device.pin is f assert isinstance(device.forward_device, DigitalOutputDevice) assert device.backward_device.pin is b assert isinstance(device.backward_device, DigitalOutputDevice) def test_motor_close(): - f = Device.pin_factory.pin(1, pin_class=MockPWMPin) - b = Device.pin_factory.pin(2, pin_class=MockPWMPin) - with Motor(f, b) as device: + f = Device.pin_factory.pin(1) + b = Device.pin_factory.pin(2) + with Motor(1, 2) as device: device.close() assert device.closed assert device.forward_device.pin is None @@ -834,16 +880,16 @@ def test_motor_close(): def test_motor_close_nonpwm(): f = Device.pin_factory.pin(1) b = Device.pin_factory.pin(2) - with Motor(f, b, pwm=False) as device: + with Motor(1, 2, pwm=False) as device: device.close() assert device.closed assert device.forward_device.pin is None assert device.backward_device.pin is None def test_motor_value(): - f = Device.pin_factory.pin(1, pin_class=MockPWMPin) - b = Device.pin_factory.pin(2, pin_class=MockPWMPin) - with Motor(f, b) as device: + f = Device.pin_factory.pin(1) + b = Device.pin_factory.pin(2) + with Motor(1, 2) as device: device.value = -1 assert device.is_active assert device.value == -1 @@ -868,7 +914,7 @@ def test_motor_value(): def test_motor_value_nonpwm(): f = Device.pin_factory.pin(1) b = Device.pin_factory.pin(2) - with Motor(f, b, pwm=False) as device: + with Motor(1, 2, pwm=False) as device: device.value = -1 assert device.is_active assert device.value == -1 @@ -883,9 +929,9 @@ def test_motor_value_nonpwm(): assert b.state == 0 and f.state == 0 def test_motor_bad_value(): - f = Device.pin_factory.pin(1, pin_class=MockPWMPin) - b = Device.pin_factory.pin(2, pin_class=MockPWMPin) - with Motor(f, b) as device: + f = Device.pin_factory.pin(1) + b = Device.pin_factory.pin(2) + with Motor(1, 2) as device: with pytest.raises(ValueError): device.value = -2 with pytest.raises(ValueError): @@ -898,7 +944,7 @@ def test_motor_bad_value(): def test_motor_bad_value_nonpwm(): f = Device.pin_factory.pin(1) b = Device.pin_factory.pin(2) - with Motor(f, b, pwm=False) as device: + with Motor(1, 2, pwm=False) as device: with pytest.raises(ValueError): device.value = -2 with pytest.raises(ValueError): @@ -909,9 +955,9 @@ def test_motor_bad_value_nonpwm(): device.value = -0.5 def test_motor_reverse(): - f = Device.pin_factory.pin(1, pin_class=MockPWMPin) - b = Device.pin_factory.pin(2, pin_class=MockPWMPin) - with Motor(f, b) as device: + f = Device.pin_factory.pin(1) + b = Device.pin_factory.pin(2) + with Motor(1, 2) as device: device.forward() assert device.value == 1 assert b.state == 0 and f.state == 1 @@ -928,7 +974,7 @@ def test_motor_reverse(): def test_motor_reverse_nonpwm(): f = Device.pin_factory.pin(1) b = Device.pin_factory.pin(2) - with Motor(f, b, pwm=False) as device: + with Motor(1, 2, pwm=False) as device: device.forward() assert device.value == 1 assert b.state == 0 and f.state == 1 @@ -937,28 +983,28 @@ def test_motor_reverse_nonpwm(): assert b.state == 1 and f.state == 0 def test_servo_pins(): - p = Device.pin_factory.pin(1, pin_class=MockPWMPin) - with Servo(p) as device: + p = Device.pin_factory.pin(1) + with Servo(1) as device: assert device.pwm_device.pin is p assert isinstance(device.pwm_device, PWMOutputDevice) def test_servo_bad_value(): - p = Device.pin_factory.pin(1, pin_class=MockPWMPin) + p = Device.pin_factory.pin(1) with pytest.raises(ValueError): - Servo(p, initial_value=2) + Servo(1, initial_value=2) with pytest.raises(ValueError): - Servo(p, min_pulse_width=30/1000) + Servo(1, min_pulse_width=30/1000) with pytest.raises(ValueError): - Servo(p, max_pulse_width=30/1000) + Servo(1, max_pulse_width=30/1000) def test_servo_pins_nonpwm(): p = Device.pin_factory.pin(2) with pytest.raises(PinPWMUnsupported): - Servo(p) + Servo(1) def test_servo_close(): - p = Device.pin_factory.pin(2, pin_class=MockPWMPin) - with Servo(p) as device: + p = Device.pin_factory.pin(2) + with Servo(1) as device: device.close() assert device.closed assert device.pwm_device.pin is None @@ -966,8 +1012,8 @@ def test_servo_close(): assert device.closed def test_servo_pulse_width(): - p = Device.pin_factory.pin(2, pin_class=MockPWMPin) - with Servo(p, min_pulse_width=5/10000, max_pulse_width=25/10000) as device: + p = Device.pin_factory.pin(2) + with Servo(1, min_pulse_width=5/10000, max_pulse_width=25/10000) as device: assert isclose(device.min_pulse_width, 5/10000) assert isclose(device.max_pulse_width, 25/10000) assert isclose(device.frame_width, 20/1000) @@ -980,8 +1026,8 @@ def test_servo_pulse_width(): assert device.pulse_width is None def test_servo_values(): - p = Device.pin_factory.pin(1, pin_class=MockPWMPin) - with Servo(p) as device: + p = Device.pin_factory.pin(1) + with Servo(1) as device: device.min() assert device.is_active assert device.value == -1 @@ -1007,14 +1053,14 @@ def test_servo_values(): assert device.value is None def test_angular_servo_range(): - p = Device.pin_factory.pin(1, pin_class=MockPWMPin) - with AngularServo(p, initial_angle=15, min_angle=0, max_angle=90) as device: + p = Device.pin_factory.pin(1) + with AngularServo(1, initial_angle=15, min_angle=0, max_angle=90) as device: assert device.min_angle == 0 assert device.max_angle == 90 def test_angular_servo_angles(): - p = Device.pin_factory.pin(1, pin_class=MockPWMPin) - with AngularServo(p) as device: + p = Device.pin_factory.pin(1) + with AngularServo(1) as device: device.angle = 0 assert device.angle == 0 assert isclose(device.value, 0) @@ -1026,7 +1072,7 @@ def test_angular_servo_angles(): assert isclose(device.value, -1) device.detach() assert device.angle is None - with AngularServo(p, initial_angle=15, min_angle=0, max_angle=90) as device: + with AngularServo(1, initial_angle=15, min_angle=0, max_angle=90) as device: assert device.angle == 15 assert isclose(device.value, -2/3) device.angle = 0 @@ -1037,7 +1083,7 @@ def test_angular_servo_angles(): assert isclose(device.value, 1) device.angle = None assert device.angle is None - with AngularServo(p, min_angle=45, max_angle=-45) as device: + with AngularServo(1, min_angle=45, max_angle=-45) as device: assert device.angle == 0 assert isclose(device.value, 0) device.angle = -45 diff --git a/tests/test_spi.py b/tests/test_spi.py index 6ae6967..f9dcf9f 100644 --- a/tests/test_spi.py +++ b/tests/test_spi.py @@ -33,50 +33,50 @@ def test_spi_hardware_params(): with patch('os.open'), patch('mmap.mmap') as mmap_mmap, patch('io.open') as io_open: mmap_mmap.return_value = array(nstr('B'), (0,) * 4096) io_open.return_value.__enter__.return_value = ['Revision: a21042'] - with patch('gpiozero.devices.Device.pin_factory', NativeFactory()), \ - patch('gpiozero.pins.local.SpiDev'): - with Device.pin_factory.spi() as device: + factory = NativeFactory() + with patch('gpiozero.pins.local.SpiDev'): + with factory.spi() as device: assert isinstance(device, LocalPiHardwareSPI) - with Device.pin_factory.spi(port=0, device=0) as device: + with factory.spi(port=0, device=0) as device: assert isinstance(device, LocalPiHardwareSPI) - with Device.pin_factory.spi(port=0, device=1) as device: + with factory.spi(port=0, device=1) as device: assert isinstance(device, LocalPiHardwareSPI) - with Device.pin_factory.spi(clock_pin=11) as device: + with factory.spi(clock_pin=11) as device: assert isinstance(device, LocalPiHardwareSPI) - with Device.pin_factory.spi(clock_pin=11, mosi_pin=10, select_pin=8) as device: + with factory.spi(clock_pin=11, mosi_pin=10, select_pin=8) as device: assert isinstance(device, LocalPiHardwareSPI) - with Device.pin_factory.spi(clock_pin=11, mosi_pin=10, select_pin=7) as device: + with factory.spi(clock_pin=11, mosi_pin=10, select_pin=7) as device: assert isinstance(device, LocalPiHardwareSPI) - with Device.pin_factory.spi(shared=True) as device: + with factory.spi(shared=True) as device: assert isinstance(device, LocalPiHardwareSPIShared) with pytest.raises(ValueError): - Device.pin_factory.spi(port=1) + factory.spi(port=1) with pytest.raises(ValueError): - Device.pin_factory.spi(device=2) + factory.spi(device=2) with pytest.raises(ValueError): - Device.pin_factory.spi(port=0, clock_pin=12) + factory.spi(port=0, clock_pin=12) with pytest.raises(ValueError): - Device.pin_factory.spi(foo='bar') + factory.spi(foo='bar') def test_spi_software_params(): with patch('os.open'), patch('mmap.mmap') as mmap_mmap, patch('io.open') as io_open: mmap_mmap.return_value = array(nstr('B'), (0,) * 4096) io_open.return_value.__enter__.return_value = ['Revision: a21042'] - with patch('gpiozero.devices.Device.pin_factory', NativeFactory()), \ - patch('gpiozero.pins.local.SpiDev'): - with Device.pin_factory.spi(select_pin=6) as device: + factory = NativeFactory() + with patch('gpiozero.pins.local.SpiDev'): + with factory.spi(select_pin=6) as device: assert isinstance(device, LocalPiSoftwareSPI) - with Device.pin_factory.spi(clock_pin=11, mosi_pin=9, miso_pin=10) as device: + with factory.spi(clock_pin=11, mosi_pin=9, miso_pin=10) as device: assert isinstance(device, LocalPiSoftwareSPI) - with Device.pin_factory.spi(select_pin=6, shared=True) as device: + with factory.spi(select_pin=6, shared=True) as device: assert isinstance(device, LocalPiSoftwareSPIShared) - with patch('gpiozero.devices.Device.pin_factory', NativeFactory()), \ - patch('gpiozero.pins.local.SpiDev', None): - # Clear out the old factory's pins cache (this is only necessary - # because we're being very naughty switching out pin factories) - Device.pin_factory.pins.clear() + with patch('gpiozero.pins.local.SpiDev', None): + # Clear out the old factory's caches (this is only necessary because + # we're being naughty switching out patches) + factory.pins.clear() + factory._reservations.clear() # Ensure software fallback works when SpiDev isn't present - with Device.pin_factory.spi() as device: + with factory.spi() as device: assert isinstance(device, LocalPiSoftwareSPI) def test_spi_hardware_conflict(): From 1ca017fc6de436d460dbc76beceac1e00f262609 Mon Sep 17 00:00:00 2001 From: Dave Jones Date: Wed, 5 Jul 2017 20:17:57 +0100 Subject: [PATCH 2/4] Make mock constructor consistent --- gpiozero/pins/mock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpiozero/pins/mock.py b/gpiozero/pins/mock.py index 23a0e4f..742e01a 100644 --- a/gpiozero/pins/mock.py +++ b/gpiozero/pins/mock.py @@ -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(repr(self)) else 'floating' + self._pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating' self._state = self._pull == 'up' self._bounce = None self._edges = 'both' From 8958874a77b09c72cd395526362a87641d299fee Mon Sep 17 00:00:00 2001 From: Dave Jones Date: Fri, 14 Jul 2017 10:44:24 +0100 Subject: [PATCH 3/4] Add pin_factory param to all devices And some docs ... --- docs/api_input.rst | 16 +- docs/api_pins.rst | 62 +++++-- docs/images/device_pin_flowchart.dot | 19 ++ docs/images/device_pin_flowchart.pdf | Bin 0 -> 8134 bytes docs/images/device_pin_flowchart.png | Bin 0 -> 29123 bytes docs/images/device_pin_flowchart.svg | 66 +++++++ docs/images/raspi-config.png | Bin 33392 -> 37065 bytes docs/installing.rst | 72 +++++--- docs/notes.rst | 2 +- docs/recipes_advanced.rst | 8 +- docs/remote_gpio.rst | 134 +++++++++----- gpiozero/boards.py | 257 ++++++++++++++++++++------- gpiozero/devices.py | 9 +- gpiozero/input_devices.py | 75 ++++++-- gpiozero/output_devices.py | 91 ++++++++-- gpiozero/spi_devices.py | 4 +- 16 files changed, 619 insertions(+), 196 deletions(-) create mode 100644 docs/images/device_pin_flowchart.dot create mode 100644 docs/images/device_pin_flowchart.pdf create mode 100644 docs/images/device_pin_flowchart.png create mode 100644 docs/images/device_pin_flowchart.svg diff --git a/docs/api_input.rst b/docs/api_input.rst index 4791c1c..8151836 100644 --- a/docs/api_input.rst +++ b/docs/api_input.rst @@ -16,35 +16,35 @@ everyday components. Components must be wired up correctly before use in code. Button ====== -.. autoclass:: Button(pin, pull_up=True, bounce_time=None, hold_time=1, hold_repeat=False) +.. autoclass:: Button(pin, pull_up=True, bounce_time=None, hold_time=1, hold_repeat=False, pin_factory=None) :members: wait_for_press, wait_for_release, pin, is_pressed, is_held, hold_time, held_time, hold_repeat, pull_up, when_pressed, when_released, when_held Line Sensor (TRCT5000) ====================== -.. autoclass:: LineSensor(pin, queue_len=5, sample_rate=100, threshold=0.5, partial=False) +.. autoclass:: LineSensor(pin, queue_len=5, sample_rate=100, threshold=0.5, partial=False, pin_factory=None) :members: wait_for_line, wait_for_no_line, pin, line_detected, when_line, when_no_line Motion Sensor (D-SUN PIR) ========================= -.. autoclass:: MotionSensor(pin, queue_len=1, sample_rate=10, threshold=0.5, partial=False) +.. autoclass:: MotionSensor(pin, queue_len=1, sample_rate=10, threshold=0.5, partial=False, pin_factory=None) :members: wait_for_motion, wait_for_no_motion, pin, motion_detected, when_motion, when_no_motion Light Sensor (LDR) ================== -.. autoclass:: LightSensor(pin, queue_len=5, charge_time_limit=0.01, threshold=0.1, partial=False) +.. autoclass:: LightSensor(pin, queue_len=5, charge_time_limit=0.01, threshold=0.1, partial=False, pin_factory=None) :members: wait_for_light, wait_for_dark, pin, light_detected, when_light, when_dark Distance Sensor (HC-SR04) ========================= -.. autoclass:: DistanceSensor(echo, trigger, queue_len=30, max_distance=1, threshold_distance=0.3, partial=False) +.. autoclass:: DistanceSensor(echo, trigger, queue_len=30, max_distance=1, threshold_distance=0.3, partial=False, pin_factory=None) :members: wait_for_in_range, wait_for_out_of_range, trigger, echo, when_in_range, when_out_of_range, max_distance, distance, threshold_distance Base Classes @@ -63,7 +63,7 @@ to construct classes for their own devices. DigitalInputDevice ================== -.. autoclass:: DigitalInputDevice(pin, pull_up=False, bounce_time=None) +.. autoclass:: DigitalInputDevice(pin, pull_up=False, bounce_time=None, pin_factory=None) :members: SmoothedInputDevice @@ -75,12 +75,12 @@ SmoothedInputDevice InputDevice =========== -.. autoclass:: InputDevice(pin, pull_up=False) +.. autoclass:: InputDevice(pin, pull_up=False, pin_factory=None) :members: GPIODevice ========== -.. autoclass:: GPIODevice(pin) +.. autoclass:: GPIODevice(pin, pin_factory=None) :members: diff --git a/docs/api_pins.rst b/docs/api_pins.rst index 6a34a72..340d8b9 100644 --- a/docs/api_pins.rst +++ b/docs/api_pins.rst @@ -11,10 +11,23 @@ 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 pin specification. However, what the -library actually expects is a :class:`Pin` implementation. If it finds anything -else, it uses the existing ``Device.pin_factory`` to construct a :class:`Pin` -implementation based on the specification. +When you construct a device, you pass in a pin specification. This is passed to +a pin :class:`Factory` which turns it into a :class:`Pin` implementation. The +default factory can be queried (and changed) with ``Device.pin_factory``, i.e. +the ``pin_factory`` attribute of the :class:`Device` class. However, all +classes accept a ``pin_factory`` keyword argument to their constructors +permitting the factory to be overridden on a per-device basis (the reason for +allowing per-device factories is made apparent later in the :doc:`remote_gpio` +chapter). + +This is illustrated in the following flow-chart: + +.. image:: images/device_pin_flowchart.* + +The default factory is constructed when GPIO Zero is first imported; if no +default factory can be constructed (e.g. because no GPIO implementations are +installed, or all of them fail to load for whatever reason), an +:exc:`ImportError` will be raised. Changing the pin factory ======================== @@ -24,7 +37,7 @@ The default pin factory can be replaced by specifying a value for the .. code-block:: console - pi@raspberrypi $ GPIOZERO_PIN_FACTORY=native python + $ GPIOZERO_PIN_FACTORY=native python Python 3.4.2 (default, Oct 19 2014, 13:31:11) [GCC 4.9.1] on linux Type "help", "copyright", "credits" or "license" for more information. @@ -37,8 +50,8 @@ export this value: .. code-block:: console - pi@raspberrypi $ export GPIOZERO_PIN_FACTORY=native - pi@raspberrypi $ python + $ export GPIOZERO_PIN_FACTORY=native + $ python Python 3.4.2 (default, Oct 19 2014, 13:31:11) [GCC 4.9.1] on linux Type "help", "copyright", "credits" or "license" for more information. @@ -46,7 +59,7 @@ export this value: >>> gpiozero.Device.pin_factory >>> quit() - pi@raspberrypi $ python + $ python Python 3.4.2 (default, Oct 19 2014, 13:31:11) [GCC 4.9.1] on linux Type "help", "copyright", "credits" or "license" for more information. @@ -73,17 +86,30 @@ they are tried by default. | native | :class:`gpiozero.pins.native.NativeFactory` | :class:`gpiozero.pins.native.NativePin` | +---------+-----------------------------------------------+-------------------------------------------+ -If you need to change the default pin factory from within a script, set +If you need to change the default pin factory from within a script, either set ``Device.pin_factory`` to the new factory instance to use:: from gpiozero.pins.native import NativeFactory - from gpiozero import * + from gpiozero import Device, LED + Device.pin_factory = NativeFactory() + # These will now implicitly use NativePin instead of + # RPiGPIOPin + led1 = LED(16) + led2 = LED(17) + +Or use the ``pin_factory`` keyword parameter mentioned above:: + + from gpiozero.pins.native import NativeFactory from gpiozero import LED - # This will now use NativePin instead of RPiGPIOPin - led = LED(16) + my_factory = NativeFactory() + + # This will use NativePin instead of RPiGPIOPin for led1 + # but led2 will continue to use RPiGPIOPin + led1 = LED(16, pin_factory=my_factory) + led2 = LED(17) Certain factories may take default information from additional sources. For example, to default to creating pins with @@ -100,11 +126,13 @@ Like the ``GPIOZERO_PIN_FACTORY`` value, these can be exported from your .. warning:: - The astute and mischievous reader may note that it is possible to mix pin - implementations, e.g. using ``RPiGPIOPin`` for one pin, and ``NativePin`` - for another. This is unsupported, and if it results in your script - crashing, your components failing, or your Raspberry Pi turning into an - actual raspberry pie, you have only yourself to blame. + The astute and mischievous reader may note that it is possible to mix + strictly local pin implementations, e.g. using ``RPiGPIOPin`` for one pin, + and ``NativePin`` for another. This is unsupported, and if it results in + your script crashing, your components failing, or your Raspberry Pi turning + into an actual raspberry pie, you have only yourself to blame. + + Sensible uses of multiple pin factories are given in :doc:`remote_gpio`. RPi.GPIO diff --git a/docs/images/device_pin_flowchart.dot b/docs/images/device_pin_flowchart.dot new file mode 100644 index 0000000..1470b08 --- /dev/null +++ b/docs/images/device_pin_flowchart.dot @@ -0,0 +1,19 @@ +/* vim: set et sw=4 sts=4: */ + +digraph device_pins { + graph [rankdir=TB]; + node [shape=rect, shape=filled, fontname=Sans, fontsize=10]; + edge [fontname=Sans, fontsize=10]; + + constructor [label="LED(pin_spec, ...,\npin_factory=None)"]; + pin_factory_kwarg [shape=diamond,label="pin_factory == None?"]; + default_factory [label="self.pin_factory = Device.pin_factory"]; + override_factory [label="self.pin_factory = pin_factory"]; + factory_pin [label="self.pin = self.pin_factory.pin(pin_spec)"]; + + constructor->pin_factory_kwarg; + pin_factory_kwarg->default_factory [label="yes"]; + pin_factory_kwarg->override_factory [label="no"]; + default_factory->factory_pin; + override_factory->factory_pin; +} diff --git a/docs/images/device_pin_flowchart.pdf b/docs/images/device_pin_flowchart.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4a9d4a31d58d3b415470d1f77bb7ce7793e59b01 GIT binary patch literal 8134 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 E0R2oPz5oCK literal 0 HcmV?d00001 diff --git a/docs/images/device_pin_flowchart.png b/docs/images/device_pin_flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..4909f7851b3eef0ba4b28e9a716d879d1677c221 GIT binary patch literal 29123 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 literal 0 HcmV?d00001 diff --git a/docs/images/device_pin_flowchart.svg b/docs/images/device_pin_flowchart.svg new file mode 100644 index 0000000..075dcc9 --- /dev/null +++ b/docs/images/device_pin_flowchart.svg @@ -0,0 +1,66 @@ + + + + + + +device_pins + + +constructor + +LED(pin_spec, ..., +pin_factory=None) + + +pin_factory_kwarg + +pin_factory == None? + + +constructor->pin_factory_kwarg + + + + +default_factory + +self.pin_factory = Device.pin_factory + + +pin_factory_kwarg->default_factory + + +yes + + +override_factory + +self.pin_factory = pin_factory + + +pin_factory_kwarg->override_factory + + +no + + +factory_pin + +self.pin = self.pin_factory.pin(pin_spec) + + +default_factory->factory_pin + + + + +override_factory->factory_pin + + + + + diff --git a/docs/images/raspi-config.png b/docs/images/raspi-config.png index 7880adb81a39b1a9e92592a924eebe47567cac6e..bf34fcf5963309f18ecea10edcb24e638c32eada 100644 GIT binary patch literal 37065 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 Of8xS2LZt%QKK~1omOgL* literal 33392 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 diff --git a/docs/installing.rst b/docs/installing.rst index 30c80a5..168f869 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -9,42 +9,58 @@ operating systems, including for PCs using the :doc:`remote_gpio` feature. Raspberry Pi ============ -First, update your repositories list:: +First, update your repositories list: - sudo apt update +.. code-block:: console -Then install the package for Python 3:: + pi@raspberrypi:~$ sudo apt update - sudo apt install python3-gpiozero +Then install the package for Python 3: -or Python 2:: +.. code-block:: console - sudo apt install python-gpiozero + pi@raspberrypi:~$ sudo apt install python3-gpiozero + +or Python 2: + +.. code-block:: console + + pi@raspberrypi:~$ sudo apt install python-gpiozero Linux ===== -First, update your distribution's repositories list. For example:: +First, update your distribution's repositories list. For example: - sudo apt update +.. code-block:: console -Then install pip for Python 3:: + $ sudo apt update - sudo apt install python3-pip +Then install pip for Python 3: -or Python 3:: +.. code-block:: console - sudo apt install python-pip + $ sudo apt install python3-pip + +or Python 3: + +.. code-block:: console + + $ sudo apt install python-pip (Alternatively, install pip with `get-pip`_.) -Next, install gpiozero for Python 3:: +Next, install gpiozero for Python 3: - sudo pip3 install gpiozero +.. code-block:: console -or Python 2:: + $ sudo pip3 install gpiozero - sudo pip install gpiozero +or Python 2: + +.. code-block:: console + + $ sudo pip install gpiozero .. note:: @@ -55,24 +71,32 @@ or Python 2:: Mac OS ====== -First, install pip:: +First, install pip: - ??? +.. code-block:: console -Next, install gpiozero with pip:: + $ ??? - pip install gpiozero +Next, install gpiozero with pip: + +.. code-block:: console + + $ pip install gpiozero Windows ======= -First, install pip:: +First, install pip: - ??? +.. code-block:: doscon -Next, install gpiozero with pip:: + C:\Users\user1> ??? - pip install gpiozero +Next, install gpiozero with pip: + +.. code-block:: doscon + + C:\Users\user1> pip install gpiozero .. _Raspbian Jessie: https://www.raspberrypi.org/downloads/raspbian/ diff --git a/docs/notes.rst b/docs/notes.rst index 5889f92..b704a8d 100644 --- a/docs/notes.rst +++ b/docs/notes.rst @@ -102,4 +102,4 @@ the ``pip`` utility. This can be done with the following command in Raspbian: Alternatively, install pip with `get-pip`_. -.. _get_pip: https://pip.pypa.io/en/stable/installing/ +.. _get-pip: https://pip.pypa.io/en/stable/installing/ diff --git a/docs/recipes_advanced.rst b/docs/recipes_advanced.rst index bb99583..39db647 100644 --- a/docs/recipes_advanced.rst +++ b/docs/recipes_advanced.rst @@ -36,7 +36,9 @@ functionality without the need to wire up your own LEDs (also useful because the power and activity LEDs are "known good"). Firstly you need to disable the usual triggers for the built-in LEDs. This can -be done from the terminal with the following commands:: +be done from the terminal with the following commands: + +.. code-block:: console $ echo none | sudo tee /sys/class/leds/led0/trigger $ echo gpio | sudo tee /sys/class/leds/led1/trigger @@ -46,7 +48,9 @@ Now you can control the LEDs with gpiozero like so: .. literalinclude:: examples/led_builtin.py To revert the LEDs to their usual purpose you can either reboot your Pi or -run the following commands:: +run the following commands: + +.. code-block:: console $ echo mmc0 | sudo tee /sys/class/leds/led0/trigger $ echo input | sudo tee /sys/class/leds/led1/trigger diff --git a/docs/remote_gpio.rst b/docs/remote_gpio.rst index ef19b88..dabcee3 100644 --- a/docs/remote_gpio.rst +++ b/docs/remote_gpio.rst @@ -7,7 +7,7 @@ Remote GPIO GPIO Zero supports a number of different pin implementations (low-level pin libraries which deal with the GPIO pins directly). By default, the `RPi.GPIO`_ library is used (assuming it is installed on your system), but you can -optionally specify one to use. For more information, see the :doc:`pins` +optionally specify one to use. For more information, see the :doc:`api_pins` documentation page. One of the pin libraries supported, `pigpio`_, provides the ability to control @@ -23,9 +23,11 @@ Preparing the Raspberry Pi If you're using Raspbian Jessie (desktop - not Jessie Lite) then you have everything you need to use the remote GPIO feature. If you're using Jessie Lite, -or another distribution, you'll need to install pigpio:: +or another distribution, you'll need to install pigpio: - sudo apt install pigpio +.. code-block:: console + + $ sudo apt install pigpio Then you just need to enable **Remote GPIO** in the Raspberry Pi configuration tool: @@ -34,21 +36,25 @@ tool: (Alternatively, use ``sudo raspi-config`` on the command line) -Then launch the pigpio daemon:: +Then launch the pigpio daemon: - sudo pigpiod +.. code-block:: console + + $ sudo pigpiod To only allow connections from a specific IP address, use the ``-n`` flag. For -example:: +example: - sudo pigpiod -n localhost # allow localhost only - sudo pigpiod -n 192.168.1.65 # allow 192.168.1.65 only - sudo pigpiod -n localhost -n 192.168.1.65 # allow localhost and 192.168.1.65 only +.. code-block:: console + + $ sudo pigpiod -n localhost # allow localhost only + $ sudo pigpiod -n 192.168.1.65 # allow 192.168.1.65 only + $ sudo pigpiod -n localhost -n 192.168.1.65 # allow localhost and 192.168.1.65 only You will need to launch the pigpio daemon every time you wish to use this feature. To automate running the daemon at boot time:: - sudo systemctl enable pigpiod + $ sudo systemctl enable pigpiod Preparing the host computer =========================== @@ -61,72 +67,100 @@ Python library on the PC. Raspberry Pi ------------ -First, update your repositories list:: +First, update your repositories list: - sudo apt update +.. code-block:: console -Then install the pigpio library for Python 3:: + $ sudo apt update - sudo apt install python3-pigpio +Then install the pigpio library for Python 3: -or Python 2:: +.. code-block:: console - sudo apt install python-pigpio + $ sudo apt install python3-pigpio -Alternatively, install with pip:: +or Python 2: - sudo pip3 install pigpio +.. code-block:: console -or:: + $ sudo apt install python-pigpio - sudo pip install pigpio +Alternatively, install with pip: + +.. code-block:: console + + $ sudo pip3 install pigpio + +or: + +.. code-block:: console + + $ sudo pip install pigpio Linux ----- -First, update your distribution's repositories list. For example:: +First, update your distribution's repositories list. For example: - sudo apt update +.. code-block:: console -Then install pip for Python 3:: + $ sudo apt update - sudo apt install python3-pip +Then install pip for Python 3: -or Python 2:: +.. code-block:: console - sudo apt install python-pip + $ sudo apt install python3-pip + +or Python 2: + +.. code-block:: console + + $ sudo apt install python-pip (Alternatively, install pip with `get-pip`_.) -Next, install pigpio for Python 3:: +Next, install pigpio for Python 3: - sudo pip3 install pigpio +.. code-block:: console -or Python 2:: + $ sudo pip3 install pigpio - sudo pip install pigpio +or Python 2: + +.. code-block:: console + + $ sudo pip install pigpio Mac OS ------ -First, install pip:: +First, install pip: - ??? +.. code-block:: console -Next, install pigpio with pip:: + $ ??? - pip install pigpio +Next, install pigpio with pip: + +.. code-block:: console + + $ pip install pigpio Windows ------- -First install pip:: +First install pip: - ??? +.. code-block:: doscon -Next, install pigpio with pip:: + C:\Users\user1> ??? - pip install pigpio +Next, install pigpio with pip: + +.. code-block:: doscon + + C:\Users\user1> pip install pigpio Environment variables ===================== @@ -135,7 +169,9 @@ The simplest way to use devices with remote pins is to set the ``PIGPIO_ADDR`` environment variable to the IP address of the desired Raspberry Pi. You must run your Python script or launch your development environment with the environment variable set using the command line. For example, one of the -following:: +following: + +.. code-block:: console $ PIGPIO_ADDR=192.168.1.3 python3 hello.py $ PIGPIO_ADDR=192.168.1.3 python3 @@ -147,7 +183,9 @@ pigpio Python library installed, this will work with no further configuration. However, if you are running this from a Raspberry Pi, you will also need to ensure the default pin factory is set to ``PiGPIOPin``. If ``RPi.GPIO`` is installed, this will be selected as the default pin factory, so either uninstall -it, or use another environment variable to set it to ``PiGPIOPin``:: +it, or use another environment variable to set it to ``PiGPIOPin``: + +.. code-block:: console $ GPIOZERO_PIN_FACTORY=pigpio PIGPIO_ADDR=192.168.1.3 python3 hello.py @@ -160,12 +198,16 @@ with no modifications needed. For example: .. literalinclude:: examples/led_1.py -When run with:: +When run with: + +.. code-block:: console $ PIGPIO_ADDR=192.168.1.3 python3 led.py will flash the LED connected to pin 17 of the Raspberry Pi with the IP address -``192.168.1.3``. And:: +``192.168.1.3``. And: + +.. code-block:: console $ PIGPIO_ADDR=192.168.1.4 python3 led.py @@ -236,11 +278,13 @@ computer using remote pins. First, configure the boot partition of the SD card: 1. Edit ``config.txt`` and add ``dtoverlay=dwc2`` on a new line, then save the -file. + file. + 2. Create an empty file called ``ssh`` (no file extension) and save it in the -boot partition. + boot partition. + 3. Edit ``cmdline.txt`` and insert ``modules-load=dwc2,g_ether`` after -``rootwait``. + ``rootwait``. (See `blog.gbaman.info`_ for more information) diff --git a/gpiozero/boards.py b/gpiozero/boards.py index 95ff3bd..1651ab6 100644 --- a/gpiozero/boards.py +++ b/gpiozero/boards.py @@ -47,6 +47,10 @@ class CompositeOutputDevice(SourceMixin, CompositeDevice): specific order). All keyword arguments *must* be included in the collection. If omitted, an alphabetically sorted order will be selected for keyword arguments. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ def on(self): @@ -124,6 +128,10 @@ class ButtonBoard(HoldMixin, CompositeDevice): executed once per hold. This parameter can only be specified as a keyword parameter. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + :param \*\*named_pins: Specify GPIO pins that buttons of the board are attached to, associating each button with a property name. You can designate as @@ -135,6 +143,7 @@ class ButtonBoard(HoldMixin, CompositeDevice): bounce_time = kwargs.pop('bounce_time', None) hold_time = kwargs.pop('hold_time', 1) hold_repeat = kwargs.pop('hold_repeat', False) + pin_factory = kwargs.pop('pin_factory', None) order = kwargs.pop('_order', None) super(ButtonBoard, self).__init__( *( @@ -142,6 +151,7 @@ class ButtonBoard(HoldMixin, CompositeDevice): for pin in args ), _order=order, + pin_factory=pin_factory, **{ name: Button(pin, pull_up, bounce_time, hold_time, hold_repeat) for name, pin in kwargs.items() @@ -209,20 +219,28 @@ class LEDCollection(CompositeOutputDevice): pwm = kwargs.pop('pwm', False) active_high = kwargs.pop('active_high', True) initial_value = kwargs.pop('initial_value', False) + pin_factory = kwargs.pop('pin_factory', None) order = kwargs.pop('_order', None) LEDClass = PWMLED if pwm else LED super(LEDCollection, self).__init__( *( pin_or_collection if isinstance(pin_or_collection, LEDCollection) else - LEDClass(pin_or_collection, active_high, initial_value) + LEDClass( + pin_or_collection, active_high, initial_value, + pin_factory=pin_factory + ) for pin_or_collection in args ), _order=order, + pin_factory=pin_factory, **{ name: pin_or_collection if isinstance(pin_or_collection, LEDCollection) else - LEDClass(pin_or_collection, active_high, initial_value) + LEDClass( + pin_or_collection, active_high, initial_value, + pin_factory=pin_factory + ) for name, pin_or_collection in kwargs.items() }) leds = [] @@ -283,6 +301,10 @@ class LEDBoard(LEDCollection): the device will be switched on initially. This parameter can only be specified as a keyword parameter. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + :param \*\*named_pins: Specify GPIO pins that LEDs of the board are attached to, associating each LED with a property name. You can designate as many pins as @@ -486,6 +508,10 @@ class LEDBarGraph(LEDCollection): The initial :attr:`value` of the graph given as a float between -1 and +1. Defaults to ``0.0``. This parameter can only be specified as a keyword parameter. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ def __init__(self, *pins, **kwargs): @@ -495,9 +521,12 @@ class LEDBarGraph(LEDCollection): pwm = kwargs.pop('pwm', False) active_high = kwargs.pop('active_high', True) initial_value = kwargs.pop('initial_value', 0.0) + pin_factory = kwargs.pop('pin_factory', None) if kwargs: raise TypeError('unexpected keyword argument: %s' % kwargs.popitem()[0]) - super(LEDBarGraph, self).__init__(*pins, pwm=pwm, active_high=active_high) + super(LEDBarGraph, self).__init__( + *pins, pwm=pwm, active_high=active_high, pin_factory=pin_factory + ) try: self.value = initial_value except: @@ -572,12 +601,17 @@ class LedBorg(RGBLED): each component of the LedBorg. If ``False``, construct regular :class:`LED` instances, which prevents smooth color graduations. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _PiBorg LedBorg: https://www.piborg.org/ledborg """ - def __init__(self, initial_value=(0, 0, 0), pwm=True): + def __init__(self, initial_value=(0, 0, 0), pwm=True, pin_factory=None): super(LedBorg, self).__init__(red=17, green=27, blue=22, - pwm=pwm, initial_value=initial_value) + pwm=pwm, initial_value=initial_value, + pin_factory=pin_factory) class PiLiter(LEDBoard): @@ -604,12 +638,17 @@ class PiLiter(LEDBoard): in when configured for output (warning: this can be on). If ``True``, the device will be switched on initially. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/ """ - def __init__(self, pwm=False, initial_value=False): + def __init__(self, pwm=False, initial_value=False, pin_factory=None): super(PiLiter, self).__init__(4, 17, 27, 18, 22, 23, 24, 25, - pwm=pwm, initial_value=initial_value) + pwm=pwm, initial_value=initial_value, + pin_factory=pin_factory) class PiLiterBarGraph(LEDBarGraph): @@ -634,13 +673,18 @@ class PiLiterBarGraph(LEDBarGraph): The initial :attr:`value` of the graph given as a float between -1 and +1. Defaults to ``0.0``. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/ """ - def __init__(self, pwm=False, initial_value=0.0): + def __init__(self, pwm=False, initial_value=0.0, pin_factory=None): pins = (4, 17, 27, 18, 22, 23, 24, 25) - super(PiLiterBarGraph, self).__init__(*pins, - pwm=pwm, initial_value=initial_value) + super(PiLiterBarGraph, self).__init__( + *pins, pwm=pwm, initial_value=initial_value, pin_factory=pin_factory + ) class TrafficLights(LEDBoard): @@ -680,9 +724,14 @@ class TrafficLights(LEDBoard): The GPIO pin that the yellow LED is attached to. This is merely an alias for the ``amber`` parameter - you can't specify both ``amber`` and ``yellow``. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ def __init__(self, red=None, amber=None, green=None, - pwm=False, initial_value=False, yellow=None): + pwm=False, initial_value=False, yellow=None, + pin_factory=None): if amber is not None and yellow is not None: raise OutputDeviceBadValue( 'Only one of amber or yellow can be specified' @@ -700,7 +749,7 @@ class TrafficLights(LEDBoard): ) super(TrafficLights, self).__init__( pwm=pwm, initial_value=initial_value, - _order=devices.keys(), + _order=devices.keys(), pin_factory=pin_factory, **devices) def __getattr__(self, name): @@ -739,11 +788,16 @@ class PiTraffic(TrafficLights): in when configured for output (warning: this can be on). If ``True``, the device will be switched on initially. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _Low Voltage Labs PI-TRAFFIC: http://lowvoltagelabs.com/products/pi-traffic/ """ - def __init__(self, pwm=False, initial_value=False): + def __init__(self, pwm=False, initial_value=False, pin_factory=None): super(PiTraffic, self).__init__(9, 10, 11, - pwm=pwm, initial_value=initial_value) + pwm=pwm, initial_value=initial_value, + pin_factory=pin_factory) class PiStop(TrafficLights): @@ -774,6 +828,10 @@ class PiStop(TrafficLights): in when configured for output (warning: this can be on). If ``True``, the device will be switched on initially. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _PiHardware Pi-Stop: https://pihw.wordpress.com/meltwaters-pi-hardware-kits/pi-stop/ .. _location: https://github.com/PiHw/Pi-Stop/blob/master/markdown_source/markdown/Discover-PiStop.md """ @@ -786,13 +844,17 @@ class PiStop(TrafficLights): 'D': (2, 3, 4), } - def __init__(self, location=None, pwm=False, initial_value=False): + def __init__( + self, location=None, pwm=False, initial_value=False, + pin_factory=None): gpios = self.LOCATIONS.get(location, None) if gpios is None: raise ValueError('location must be one of: %s' % ', '.join(sorted(self.LOCATIONS.keys()))) - super(PiStop, self).__init__(*gpios, - pwm=pwm, initial_value=initial_value) + super(PiStop, self).__init__( + *gpios, pwm=pwm, initial_value=initial_value, + pin_factory=pin_factory + ) class StatusZero(LEDBoard): @@ -817,6 +879,10 @@ class StatusZero(LEDBoard): not all strips are given labels, any remaining strips will not be initialised. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _STATUS Zero: https://thepihut.com/statuszero """ default_labels = ('one', 'two', 'three') @@ -827,6 +893,7 @@ class StatusZero(LEDBoard): (22, 27), (9, 10), ) + pin_factory = kwargs.pop('pin_factory', None) if len(labels) == 0: labels = self.default_labels elif len(labels) > len(pins): @@ -834,10 +901,15 @@ class StatusZero(LEDBoard): dup, count = Counter(labels).most_common(1)[0] if count > 1: raise ValueError("Duplicate label %s" % dup) - super(StatusZero, self).__init__(_order=labels, **{ - label: LEDBoard(red=red, green=green, _order=('red', 'green'), **kwargs) - for (green, red), label in zip(pins, labels) - }) + super(StatusZero, self).__init__( + _order=labels, pin_factory=pin_factory, **{ + label: LEDBoard( + red=red, green=green, _order=('red', 'green'), + pin_factory=pin_factory, **kwargs + ) + for (green, red), label in zip(pins, labels) + } + ) class StatusBoard(CompositeOutputDevice): @@ -862,6 +934,10 @@ class StatusBoard(CompositeOutputDevice): will be initialised with names 'one' to 'five'. If some, but not all strips are given labels, any remaining strips will not be initialised. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _STATUS: https://thepihut.com/status """ default_labels = ('one', 'two', 'three', 'four', 'five') @@ -874,6 +950,7 @@ class StatusBoard(CompositeOutputDevice): (5, 11, 26), (13, 6, 18), ) + pin_factory = kwargs.pop('pin_factory', None) if len(labels) == 0: labels = self.default_labels elif len(labels) > len(pins): @@ -881,14 +958,18 @@ class StatusBoard(CompositeOutputDevice): dup, count = Counter(labels).most_common(1)[0] if count > 1: raise ValueError("Duplicate label %s" % dup) - super(StatusBoard, self).__init__(_order=labels, **{ - label: CompositeOutputDevice( - button=Button(button), - lights=LEDBoard( - red=red, green=green, _order=('red', 'green'), **kwargs - ), _order=('button', 'lights')) - for (green, red, button), label in zip(pins, labels) - }) + super(StatusBoard, self).__init__( + _order=labels, pin_factory=pin_factory, **{ + label: CompositeOutputDevice( + button=Button(button), + lights=LEDBoard( + red=red, green=green, _order=('red', 'green'), + pin_factory=pin_factory, **kwargs + ), _order=('button', 'lights'), pin_factory=pin_factory + ) + for (green, red, button), label in zip(pins, labels) + } + ) class SnowPi(LEDBoard): @@ -917,30 +998,39 @@ class SnowPi(LEDBoard): in when configured for output (warning: this can be on). If ``True``, the device will be switched on initially. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _Ryanteck SnowPi: https://ryanteck.uk/raspberry-pi/114-snowpi-the-gpio-snowman-for-raspberry-pi-0635648608303.html """ - def __init__(self, pwm=False, initial_value=False): + def __init__(self, pwm=False, initial_value=False, pin_factory=None): super(SnowPi, self).__init__( arms=LEDBoard( left=LEDBoard( top=17, middle=18, bottom=22, pwm=pwm, initial_value=initial_value, - _order=('top', 'middle', 'bottom')), + _order=('top', 'middle', 'bottom'), + pin_factory=pin_factory), right=LEDBoard( top=7, middle=8, bottom=9, pwm=pwm, initial_value=initial_value, - _order=('top', 'middle', 'bottom')), - _order=('left', 'right') + _order=('top', 'middle', 'bottom'), + pin_factory=pin_factory), + _order=('left', 'right'), + pin_factory=pin_factory ), eyes=LEDBoard( left=23, right=24, pwm=pwm, initial_value=initial_value, - _order=('left', 'right') + _order=('left', 'right'), + pin_factory=pin_factory ), nose=25, pwm=pwm, initial_value=initial_value, - _order=('eyes', 'nose', 'arms') - ) + _order=('eyes', 'nose', 'arms'), + pin_factory=pin_factory + ) class TrafficLightsBuzzer(CompositeOutputDevice): @@ -957,12 +1047,18 @@ class TrafficLightsBuzzer(CompositeOutputDevice): :param Button button: An instance of :class:`Button` representing the button on the HAT. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ - def __init__(self, lights, buzzer, button): + def __init__(self, lights, buzzer, button, pin_factory=None): super(TrafficLightsBuzzer, self).__init__( lights=lights, buzzer=buzzer, button=button, - _order=('lights', 'buzzer', 'button')) + _order=('lights', 'buzzer', 'button'), + pin_factory=pin_factory + ) class FishDish(TrafficLightsBuzzer): @@ -985,14 +1081,19 @@ class FishDish(TrafficLightsBuzzer): LED. If ``False`` (the default), construct regular :class:`LED` instances. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _Pi Supply FishDish: https://www.pi-supply.com/product/fish-dish-raspberry-pi-led-buzzer-board/ """ - def __init__(self, pwm=False): + def __init__(self, pwm=False, pin_factory=None): super(FishDish, self).__init__( - TrafficLights(9, 22, 4, pwm=pwm), - Buzzer(8), - Button(7, pull_up=False), + TrafficLights(9, 22, 4, pwm=pwm, pin_factory=pin_factory), + Buzzer(8, pin_factory=pin_factory), + Button(7, pull_up=False, pin_factory=pin_factory), + pin_factory=pin_factory ) @@ -1016,14 +1117,19 @@ class TrafficHat(TrafficLightsBuzzer): LED. If ``False`` (the default), construct regular :class:`LED` instances. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _Ryanteck Traffic HAT: https://ryanteck.uk/hats/1-traffichat-0635648607122.html """ - def __init__(self, pwm=False): + def __init__(self, pwm=False, pin_factory=None): super(TrafficHat, self).__init__( - TrafficLights(24, 23, 22, pwm=pwm), - Buzzer(5), - Button(25), + TrafficLights(24, 23, 22, pwm=pwm, pin_factory=pin_factory), + Buzzer(5, pin_factory=pin_factory), + Button(25, pin_factory=pin_factory), + pin_factory=pin_factory ) @@ -1049,13 +1155,19 @@ class Robot(SourceMixin, CompositeDevice): :param tuple right: A tuple of two GPIO pins representing the forward and backward inputs of the right motor's controller. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ - def __init__(self, left=None, right=None): + def __init__(self, left=None, right=None, pin_factory=None): super(Robot, self).__init__( - left_motor=Motor(*left), - right_motor=Motor(*right), - _order=('left_motor', 'right_motor')) + left_motor=Motor(*left, pin_factory=pin_factory), + right_motor=Motor(*right, pin_factory=pin_factory), + _order=('left_motor', 'right_motor'), + pin_factory=pin_factory + ) @property def value(self): @@ -1148,11 +1260,17 @@ class RyanteckRobot(Robot): robot = RyanteckRobot() robot.forward() + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _Ryanteck MCB: https://ryanteck.uk/add-ons/6-ryanteck-rpi-motor-controller-board-0635648607160.html """ - def __init__(self): - super(RyanteckRobot, self).__init__((17, 18), (22, 23)) + def __init__(self, pin_factory=None): + super(RyanteckRobot, self).__init__( + (17, 18), (22, 23), pin_factory=pin_factory + ) class CamJamKitRobot(Robot): @@ -1168,20 +1286,31 @@ class CamJamKitRobot(Robot): robot = CamJamKitRobot() robot.forward() + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _CamJam #3 EduKit: http://camjam.me/?page_id=1035 """ - def __init__(self): - super(CamJamKitRobot, self).__init__((9, 10), (7, 8)) + def __init__(self, pin_factory=None): + super(CamJamKitRobot, self).__init__( + (9, 10), (7, 8), pin_factory=pin_factory + ) class _EnergenieMaster(SharedMixin, CompositeOutputDevice): - def __init__(self): + def __init__(self, pin_factory=None): self._lock = Lock() super(_EnergenieMaster, self).__init__( - *(OutputDevice(pin) for pin in (17, 22, 23, 27)), - mode=OutputDevice(24), enable=OutputDevice(25), - _order=('mode', 'enable')) + *( + OutputDevice(pin, pin_factory=pin_factory) + for pin in (17, 22, 23, 27) + ), + mode=OutputDevice(24, pin_factory=pin_factory), + enable=OutputDevice(25, pin_factory=pin_factory), + _order=('mode', 'enable'), pin_factory=pin_factory + ) def close(self): if self._lock: @@ -1190,7 +1319,7 @@ class _EnergenieMaster(SharedMixin, CompositeOutputDevice): self._lock = None @classmethod - def _shared_key(cls): + def _shared_key(cls, pin_factory): # There's only one Energenie master return None @@ -1231,18 +1360,22 @@ class Energenie(SourceMixin, Device): the socket, which will be set upon construction. This defaults to ``False`` which will switch the socket off. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _Energenie socket: https://energenie4u.co.uk/index.php/catalogue/product/ENER002-2PI """ - def __init__(self, socket=None, initial_value=False): + def __init__(self, socket=None, initial_value=False, pin_factory=None): if socket is None: raise EnergenieSocketMissing('socket number must be provided') if not (1 <= socket <= 4): raise EnergenieBadSocket('socket number must be between 1 and 4') self._value = None - super(Energenie, self).__init__() + super(Energenie, self).__init__(pin_factory=pin_factory) self._socket = socket - self._master = _EnergenieMaster() + self._master = _EnergenieMaster(pin_factory=pin_factory) if initial_value: self.on() else: diff --git a/gpiozero/devices.py b/gpiozero/devices.py index 1a1c687..9cbc9c8 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -197,7 +197,11 @@ class Device(ValuesMixin, GPIOBase): def __init__(self, **kwargs): # Force pin_factory to be keyword-only, even in Python 2 - self.pin_factory = kwargs.pop('pin_factory', Device.pin_factory) + pin_factory = kwargs.pop('pin_factory', None) + if pin_factory is None: + self.pin_factory = Device.pin_factory + else: + self.pin_factory = pin_factory if kwargs: raise TypeError("Device.__init__() got unexpected keyword " "argument '%s'" % kwargs.popitem()[0]) @@ -281,8 +285,7 @@ class CompositeDevice(Device): dev.close() raise self._all = args + tuple(kwargs[v] for v in self._order) - kwargs = {'pin_factory': pin_factory} if pin_factory is not None else {} - super(CompositeDevice, self).__init__(**kwargs) + super(CompositeDevice, self).__init__(pin_factory=pin_factory) def __getattr__(self, name): # if _named doesn't exist yet, pretend it's an empty dict diff --git a/gpiozero/input_devices.py b/gpiozero/input_devices.py index e65b769..4570d75 100644 --- a/gpiozero/input_devices.py +++ b/gpiozero/input_devices.py @@ -32,9 +32,13 @@ class InputDevice(GPIODevice): :param bool pull_up: If ``True``, the pin will be pulled high with an internal resistor. If ``False`` (the default), the pin will be pulled low. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ - def __init__(self, pin=None, pull_up=False): - super(InputDevice, self).__init__(pin) + def __init__(self, pin=None, pull_up=False, pin_factory=None): + super(InputDevice, self).__init__(pin, pin_factory=pin_factory) try: self.pin.function = 'input' pull = 'up' if pull_up else 'down' @@ -75,9 +79,16 @@ class DigitalInputDevice(EventsMixin, InputDevice): Specifies the length of time (in seconds) that the component will ignore changes in state after an initial change. This defaults to ``None`` which indicates that no bounce compensation will be performed. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ - def __init__(self, pin=None, pull_up=False, bounce_time=None): - super(DigitalInputDevice, self).__init__(pin, pull_up) + def __init__( + self, pin=None, pull_up=False, bounce_time=None, pin_factory=None): + super(DigitalInputDevice, self).__init__( + pin, pull_up, pin_factory=pin_factory + ) try: self.pin.bounce = bounce_time self.pin.edges = 'both' @@ -127,12 +138,18 @@ class SmoothedInputDevice(EventsMixin, InputDevice): (from the :attr:`is_active` property) will block until the queue has filled. If ``True``, a value will be returned immediately, but be aware that this value is likely to fluctuate excessively. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ def __init__( self, pin=None, pull_up=False, threshold=0.5, - queue_len=5, sample_wait=0.0, partial=False): + queue_len=5, sample_wait=0.0, partial=False, pin_factory=None): self._queue = None - super(SmoothedInputDevice, self).__init__(pin, pull_up) + super(SmoothedInputDevice, self).__init__( + pin, pull_up, pin_factory=pin_factory + ) try: self._queue = GPIOQueue(self, queue_len, sample_wait, partial) self.threshold = float(threshold) @@ -263,11 +280,17 @@ class Button(HoldMixin, DigitalInputDevice): as long as the device remains active, every *hold_time* seconds. If ``False`` (the default) the :attr:`when_held` handler will be only be executed once per hold. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ def __init__( self, pin=None, pull_up=True, bounce_time=None, - hold_time=1, hold_repeat=False): - super(Button, self).__init__(pin, pull_up, bounce_time) + hold_time=1, hold_repeat=False, pin_factory=None): + super(Button, self).__init__( + pin, pull_up, bounce_time, pin_factory=pin_factory + ) self.hold_time = hold_time self.hold_repeat = hold_repeat @@ -325,14 +348,19 @@ class LineSensor(SmoothedInputDevice): filled with values. Only set this to ``True`` if you require values immediately after object construction. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _CamJam #3 EduKit: http://camjam.me/?page_id=1035 """ def __init__( self, pin=None, queue_len=5, sample_rate=100, threshold=0.5, - partial=False): + partial=False, pin_factory=None): super(LineSensor, self).__init__( pin, pull_up=False, threshold=threshold, - queue_len=queue_len, sample_wait=1 / sample_rate, partial=partial + queue_len=queue_len, sample_wait=1 / sample_rate, partial=partial, + pin_factory=pin_factory ) try: self._queue.start() @@ -394,13 +422,18 @@ class MotionSensor(SmoothedInputDevice): :attr:`~SmoothedInputDevice.is_active` until the internal queue has filled with values. Only set this to ``True`` if you require values immediately after object construction. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ def __init__( self, pin=None, queue_len=1, sample_rate=10, threshold=0.5, - partial=False): + partial=False, pin_factory=None): super(MotionSensor, self).__init__( pin, pull_up=False, threshold=threshold, - queue_len=queue_len, sample_wait=1 / sample_rate, partial=partial + queue_len=queue_len, sample_wait=1 / sample_rate, partial=partial, + pin_factory=pin_factory ) try: self._queue.start() @@ -460,14 +493,19 @@ class LightSensor(SmoothedInputDevice): filled with values. Only set this to ``True`` if you require values immediately after object construction. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _CamJam #2 EduKit: http://camjam.me/?page_id=623 """ def __init__( self, pin=None, queue_len=5, charge_time_limit=0.01, - threshold=0.1, partial=False): + threshold=0.1, partial=False, pin_factory=None): super(LightSensor, self).__init__( pin, pull_up=False, threshold=threshold, - queue_len=queue_len, sample_wait=0.0, partial=partial + queue_len=queue_len, sample_wait=0.0, partial=partial, + pin_factory=pin_factory ) try: self._charge_time_limit = charge_time_limit @@ -568,17 +606,22 @@ class DistanceSensor(SmoothedInputDevice): filled with values. Only set this to ``True`` if you require values immediately after object construction. + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + .. _CamJam #3 EduKit: http://camjam.me/?page_id=1035 """ def __init__( self, echo=None, trigger=None, queue_len=30, max_distance=1, - threshold_distance=0.3, partial=False): + threshold_distance=0.3, partial=False, pin_factory=None): if max_distance <= 0: raise ValueError('invalid maximum distance (must be positive)') self._trigger = None super(DistanceSensor, self).__init__( echo, pull_up=False, threshold=threshold_distance / max_distance, - queue_len=queue_len, sample_wait=0.0, partial=partial + queue_len=queue_len, sample_wait=0.0, partial=partial, + pin_factory=pin_factory ) try: self.speed_of_sound = 343.26 # m/s diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index b570f3b..b715bfb 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -36,9 +36,15 @@ class OutputDevice(SourceMixin, GPIODevice): ``None``, the device will be left in whatever state the pin is found in when configured for output (warning: this can be on). If ``True``, the device will be switched on initially. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ - def __init__(self, pin=None, active_high=True, initial_value=False): - super(OutputDevice, self).__init__(pin) + def __init__( + self, pin=None, active_high=True, initial_value=False, + pin_factory=None): + super(OutputDevice, self).__init__(pin, pin_factory=pin_factory) self._lock = Lock() self.active_high = active_high if initial_value is None: @@ -126,10 +132,14 @@ class DigitalOutputDevice(OutputDevice): uses an optional background thread to handle toggling the device state without further interaction. """ - def __init__(self, pin=None, active_high=True, initial_value=False): + def __init__( + self, pin=None, active_high=True, initial_value=False, + pin_factory=None): self._blink_thread = None self._controller = None - super(DigitalOutputDevice, self).__init__(pin, active_high, initial_value) + super(DigitalOutputDevice, self).__init__( + pin, active_high, initial_value, pin_factory=pin_factory + ) @property def value(self): @@ -230,6 +240,10 @@ class LED(DigitalOutputDevice): ``None``, the LED will be left in whatever state the pin is found in when configured for output (warning: this can be on). If ``True``, the LED will be switched on initially. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ pass @@ -265,6 +279,10 @@ class Buzzer(DigitalOutputDevice): ``None``, the buzzer will be left in whatever state the pin is found in when configured for output (warning: this can be on). If ``True``, the buzzer will be switched on initially. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ pass @@ -293,13 +311,21 @@ class PWMOutputDevice(OutputDevice): :param int frequency: The frequency (in Hz) of pulses emitted to drive the device. Defaults to 100Hz. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ - def __init__(self, pin=None, active_high=True, initial_value=0, frequency=100): + def __init__( + self, pin=None, active_high=True, initial_value=0, frequency=100, + pin_factory=None): self._blink_thread = None self._controller = None if not 0 <= initial_value <= 1: raise OutputDeviceBadValue("initial_value must be between 0 and 1") - super(PWMOutputDevice, self).__init__(pin, active_high, initial_value=None) + super(PWMOutputDevice, self).__init__( + pin, active_high, initial_value=None, pin_factory=pin_factory + ) try: # XXX need a way of setting these together self.pin.frequency = frequency @@ -500,6 +526,10 @@ class PWMLED(PWMOutputDevice): :param int frequency: The frequency (in Hz) of pulses emitted to drive the LED. Defaults to 100Hz. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ pass @@ -552,17 +582,24 @@ class RGBLED(SourceMixin, Device): If ``True`` (the default), construct :class:`PWMLED` instances for each component of the RGBLED. If ``False``, construct regular :class:`LED` instances, which prevents smooth color graduations. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ def __init__( self, red=None, green=None, blue=None, active_high=True, - initial_value=(0, 0, 0), pwm=True): + initial_value=(0, 0, 0), pwm=True, pin_factory=None): self._leds = () self._blink_thread = None if not all(p is not None for p in [red, green, blue]): raise GPIOPinMissing('red, green, and blue pins must be provided') LEDClass = PWMLED if pwm else LED - super(RGBLED, self).__init__() - self._leds = tuple(LEDClass(pin, active_high) for pin in (red, green, blue)) + super(RGBLED, self).__init__(pin_factory=pin_factory) + self._leds = tuple( + LEDClass(pin, active_high, pin_factory=pin_factory) + for pin in (red, green, blue) + ) self.value = initial_value red = _led_property(0) @@ -803,17 +840,23 @@ class Motor(SourceMixin, CompositeDevice): variable speed control. If ``False``, construct :class:`DigitalOutputDevice` instances, allowing only direction control. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ - def __init__(self, forward=None, backward=None, pwm=True): + def __init__(self, forward=None, backward=None, pwm=True, pin_factory=None): if not all(p is not None for p in [forward, backward]): raise GPIOPinMissing( 'forward and backward pins must be provided' ) PinClass = PWMOutputDevice if pwm else DigitalOutputDevice super(Motor, self).__init__( - forward_device=PinClass(forward), - backward_device=PinClass(backward), - _order=('forward_device', 'backward_device')) + forward_device=PinClass(forward, pin_factory=pin_factory), + backward_device=PinClass(backward, pin_factory=pin_factory), + _order=('forward_device', 'backward_device'), + pin_factory=pin_factory + ) @property def value(self): @@ -946,11 +989,15 @@ class Servo(SourceMixin, CompositeDevice): :param float frame_width: The length of time between servo control pulses measured in seconds. This defaults to 20ms which is a common value for servos. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ def __init__( self, pin=None, initial_value=0.0, min_pulse_width=1/1000, max_pulse_width=2/1000, - frame_width=20/1000): + frame_width=20/1000, pin_factory=None): if min_pulse_width >= max_pulse_width: raise ValueError('min_pulse_width must be less than max_pulse_width') if max_pulse_width >= frame_width: @@ -961,7 +1008,11 @@ class Servo(SourceMixin, CompositeDevice): self._min_value = -1 self._value_range = 2 super(Servo, self).__init__( - pwm_device=PWMOutputDevice(pin, frequency=int(1 / frame_width))) + pwm_device=PWMOutputDevice( + pin, frequency=int(1 / frame_width), pin_factory=pin_factory + ), + pin_factory=pin_factory + ) try: self.value = initial_value except: @@ -1146,17 +1197,23 @@ class AngularServo(Servo): :param float frame_width: The length of time between servo control pulses measured in seconds. This defaults to 20ms which is a common value for servos. + + :param Factory pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). """ def __init__( self, pin=None, initial_angle=0.0, min_angle=-90, max_angle=90, min_pulse_width=1/1000, max_pulse_width=2/1000, - frame_width=20/1000): + frame_width=20/1000, pin_factory=None): self._min_angle = min_angle self._angular_range = max_angle - min_angle initial_value = 2 * ((initial_angle - min_angle) / self._angular_range) - 1 super(AngularServo, self).__init__( - pin, initial_value, min_pulse_width, max_pulse_width, frame_width) + pin, initial_value, min_pulse_width, max_pulse_width, frame_width, + pin_factory=pin_factory + ) @property def min_angle(self): diff --git a/gpiozero/spi_devices.py b/gpiozero/spi_devices.py index 621828a..f02be55 100644 --- a/gpiozero/spi_devices.py +++ b/gpiozero/spi_devices.py @@ -28,7 +28,9 @@ class SPIDevice(Device): """ def __init__(self, **spi_args): self._spi = None - super(SPIDevice, self).__init__() + super(SPIDevice, self).__init__( + pin_factory=spi_args.pop('pin_factory', None) + ) self._spi = self.pin_factory.spi(**spi_args) def close(self): From 9fdc0938373b3f8785b21bfab580da56a7e944e1 Mon Sep 17 00:00:00 2001 From: Dave Jones Date: Thu, 13 Jul 2017 21:01:42 +0100 Subject: [PATCH 4/4] Add pin_factory to all relevant class docs Also tell a whole load of lies about what's a keyword-only arg. In the vast majority of cases, pin_factory (and other args) *aren't* keyword-only ... but we'd really like them to be, it's just difficult while maintaining py2 compatibility. --- docs/api_boards.rst | 16 ++++++++-------- docs/api_generic.rst | 2 +- docs/api_input.rst | 16 ++++++++-------- docs/api_output.rst | 22 +++++++++++----------- docs/api_spi.rst | 4 ++++ gpiozero/boards.py | 2 +- 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/docs/api_boards.rst b/docs/api_boards.rst index 49919fb..23c4730 100644 --- a/docs/api_boards.rst +++ b/docs/api_boards.rst @@ -18,21 +18,21 @@ individually. LEDBoard ======== -.. autoclass:: LEDBoard(\*pins, pwm=False, active_high=True, initial_value=False, \*\*named_pins) +.. autoclass:: LEDBoard(\*pins, pwm=False, active_high=True, initial_value=False, pin_factory=None, \*\*named_pins) :inherited-members: :members: LEDBarGraph =========== -.. autoclass:: LEDBarGraph(\*pins, pwm=False, active_high=True, initial_value=0) +.. autoclass:: LEDBarGraph(\*pins, pwm=False, active_high=True, initial_value=0, pin_factory=None) :inherited-members: :members: ButtonBoard =========== -.. autoclass:: ButtonBoard(\*pins, pull_up=True, bounce_time=None, hold_time=1, hold_repeat=False, \*\*named_pins) +.. autoclass:: ButtonBoard(\*pins, pull_up=True, bounce_time=None, hold_time=1, hold_repeat=False, pin_factory=None, \*\*named_pins) :inherited-members: :members: @@ -130,14 +130,14 @@ Energenie StatusZero ========== -.. autoclass:: StatusZero +.. autoclass:: StatusZero(\*labels, pwm=False, active_high=True, initial_value=False, pin_factory=None) :inherited-members: :members: StatusBoard =========== -.. autoclass:: StatusBoard +.. autoclass:: StatusBoard(\*labels, pwm=False, active_high=True, initial_value=False, pin_factory=None) :inherited-members: :members: @@ -168,17 +168,17 @@ to construct classes for their own devices. LEDCollection ============= -.. autoclass:: LEDCollection +.. autoclass:: LEDCollection(\*pins, pwm=False, active_high=True, initial_value=False, pin_factory=None, \*\*named_pins) :members: CompositeOutputDevice ===================== -.. autoclass:: CompositeOutputDevice(\*args, _order=None, \*\*kwargs) +.. autoclass:: CompositeOutputDevice(\*args, _order=None, pin_factory=None, \*\*kwargs) :members: CompositeDevice =============== -.. autoclass:: CompositeDevice(\*args, _order=None, \*\*kwargs) +.. autoclass:: CompositeDevice(\*args, _order=None, pin_factory=None, \*\*kwargs) :members: diff --git a/docs/api_generic.rst b/docs/api_generic.rst index 5b3c6ae..8e1eb76 100644 --- a/docs/api_generic.rst +++ b/docs/api_generic.rst @@ -33,7 +33,7 @@ are represented in purple, while abstract classes are shaded lighter): Device ====== -.. autoclass:: Device +.. autoclass:: Device(\*, pin_factory=None) :members: close, closed, value, is_active ValuesMixin diff --git a/docs/api_input.rst b/docs/api_input.rst index 8151836..2629213 100644 --- a/docs/api_input.rst +++ b/docs/api_input.rst @@ -16,35 +16,35 @@ everyday components. Components must be wired up correctly before use in code. Button ====== -.. autoclass:: Button(pin, pull_up=True, bounce_time=None, hold_time=1, hold_repeat=False, pin_factory=None) +.. autoclass:: Button(pin, \*, pull_up=True, bounce_time=None, hold_time=1, hold_repeat=False, pin_factory=None) :members: wait_for_press, wait_for_release, pin, is_pressed, is_held, hold_time, held_time, hold_repeat, pull_up, when_pressed, when_released, when_held Line Sensor (TRCT5000) ====================== -.. autoclass:: LineSensor(pin, queue_len=5, sample_rate=100, threshold=0.5, partial=False, pin_factory=None) +.. autoclass:: LineSensor(pin, \*, queue_len=5, sample_rate=100, threshold=0.5, partial=False, pin_factory=None) :members: wait_for_line, wait_for_no_line, pin, line_detected, when_line, when_no_line Motion Sensor (D-SUN PIR) ========================= -.. autoclass:: MotionSensor(pin, queue_len=1, sample_rate=10, threshold=0.5, partial=False, pin_factory=None) +.. autoclass:: MotionSensor(pin, \*, queue_len=1, sample_rate=10, threshold=0.5, partial=False, pin_factory=None) :members: wait_for_motion, wait_for_no_motion, pin, motion_detected, when_motion, when_no_motion Light Sensor (LDR) ================== -.. autoclass:: LightSensor(pin, queue_len=5, charge_time_limit=0.01, threshold=0.1, partial=False, pin_factory=None) +.. autoclass:: LightSensor(pin, \*, queue_len=5, charge_time_limit=0.01, threshold=0.1, partial=False, pin_factory=None) :members: wait_for_light, wait_for_dark, pin, light_detected, when_light, when_dark Distance Sensor (HC-SR04) ========================= -.. autoclass:: DistanceSensor(echo, trigger, queue_len=30, max_distance=1, threshold_distance=0.3, partial=False, pin_factory=None) +.. autoclass:: DistanceSensor(echo, trigger, \*, queue_len=30, max_distance=1, threshold_distance=0.3, partial=False, pin_factory=None) :members: wait_for_in_range, wait_for_out_of_range, trigger, echo, when_in_range, when_out_of_range, max_distance, distance, threshold_distance Base Classes @@ -63,19 +63,19 @@ to construct classes for their own devices. DigitalInputDevice ================== -.. autoclass:: DigitalInputDevice(pin, pull_up=False, bounce_time=None, pin_factory=None) +.. autoclass:: DigitalInputDevice(pin, \*, pull_up=False, bounce_time=None, pin_factory=None) :members: SmoothedInputDevice =================== -.. autoclass:: SmoothedInputDevice +.. autoclass:: SmoothedInputDevice(pin, \*, pull_up=False, threshold=0.5, queue_len=5, sample_wait=0.0, partial=False, pin_factory=None) :members: InputDevice =========== -.. autoclass:: InputDevice(pin, pull_up=False, pin_factory=None) +.. autoclass:: InputDevice(pin, \*, pull_up=False, pin_factory=None) :members: GPIODevice diff --git a/docs/api_output.rst b/docs/api_output.rst index 4a2a178..b800773 100644 --- a/docs/api_output.rst +++ b/docs/api_output.rst @@ -16,44 +16,44 @@ everyday components. Components must be wired up correctly before use in code. LED === -.. autoclass:: LED(pin, active_high=True, initial_value=False) +.. autoclass:: LED(pin, \*, active_high=True, initial_value=False, pin_factory=None) :members: on, off, toggle, blink, pin, is_lit PWMLED ====== -.. autoclass:: PWMLED(pin, active_high=True, initial_value=0, frequency=100) +.. autoclass:: PWMLED(pin, \*, active_high=True, initial_value=0, frequency=100, pin_factory=None) :members: on, off, toggle, blink, pulse, pin, is_lit, value RGBLED ====== -.. autoclass:: RGBLED(red, green, blue, active_high=True, initial_value=(0, 0, 0), pwm=True) +.. autoclass:: RGBLED(red, green, blue, \*, active_high=True, initial_value=(0, 0, 0), pwm=True, pin_factory=None) :members: on, off, toggle, blink, pulse, red, green, blue, is_lit, color Buzzer ====== -.. autoclass:: Buzzer(pin, active_high=True, initial_value=False) +.. autoclass:: Buzzer(pin, \*, active_high=True, initial_value=False, pin_factory=None) :members: on, off, toggle, beep, pin, is_active Motor ===== -.. autoclass:: Motor(forward, backward, pwm=True) +.. autoclass:: Motor(forward, backward, \*, pwm=True, pin_factory=None) :members: forward, backward, stop Servo ===== -.. autoclass:: Servo(pin, initial_value=0, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000) +.. autoclass:: Servo(pin, \*, initial_value=0, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000, pin_factory=None) :inherited-members: :members: AngularServo ============ -.. autoclass:: AngularServo(pin, initial_angle=0, min_angle=-90, max_angle=90, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000) +.. autoclass:: AngularServo(pin, \*, initial_angle=0, min_angle=-90, max_angle=90, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000, pin_factory=None) :inherited-members: :members: @@ -73,25 +73,25 @@ to construct classes for their own devices. DigitalOutputDevice =================== -.. autoclass:: DigitalOutputDevice(pin, active_high=True, initial_value=False) +.. autoclass:: DigitalOutputDevice(pin, \*, active_high=True, initial_value=False, pin_factory=None) :members: PWMOutputDevice =============== -.. autoclass:: PWMOutputDevice(pin, active_high=True, initial_value=0, frequency=100) +.. autoclass:: PWMOutputDevice(pin, \*, active_high=True, initial_value=0, frequency=100, pin_factory=None) :members: OutputDevice ============ -.. autoclass:: OutputDevice(pin, active_high=True, initial_value=False) +.. autoclass:: OutputDevice(pin, \*, active_high=True, initial_value=False, pin_factory=None) :members: GPIODevice ========== -.. autoclass:: GPIODevice(pin) +.. autoclass:: GPIODevice(pin, \*, pin_factory=None) :members: :noindex: diff --git a/docs/api_spi.rst b/docs/api_spi.rst index 2d40f4e..85ddcd5 100644 --- a/docs/api_spi.rst +++ b/docs/api_spi.rst @@ -64,6 +64,10 @@ omit any arguments from either scheme. The defaults are: * *clock_pin* defaults to 11, *mosi_pin* defaults to 10, *miso_pin* defaults to 9, and *select_pin* defaults to 8. +* As with other GPIO based devices you can optionally specify a *pin_factory* + argument overriding the default pin factory (see :doc:`api_pins` for more + information). + Hence the following constructors are all equivalent:: from gpiozero import MCP3008 diff --git a/gpiozero/boards.py b/gpiozero/boards.py index 1651ab6..20d819c 100644 --- a/gpiozero/boards.py +++ b/gpiozero/boards.py @@ -961,7 +961,7 @@ class StatusBoard(CompositeOutputDevice): super(StatusBoard, self).__init__( _order=labels, pin_factory=pin_factory, **{ label: CompositeOutputDevice( - button=Button(button), + button=Button(button, pin_factory=pin_factory), lights=LEDBoard( red=red, green=green, _order=('red', 'green'), pin_factory=pin_factory, **kwargs