From ce6217c14fddf5a39edbc94c920f0fcd1e425620 Mon Sep 17 00:00:00 2001 From: Dave Jones Date: Tue, 27 Sep 2016 00:30:57 +0100 Subject: [PATCH] Fix #459 - properly support remote SPI with pigpio Sorry! Dave's messing around with the pin implementations again. Hopefully the last time. The pin_factory is now really a factory object which can be asked to produce individual pins or pin-based interfaces like SPI (which can be supported properly via pigpio). --- docs/api_exc.rst | 28 +- docs/api_pins.rst | 125 ++++---- docs/images/composed_devices.pdf | Bin 12980 -> 12979 bytes docs/images/composed_devices.png | Bin 34694 -> 34875 bytes docs/notes.rst | 11 +- docs/recipes.rst | 10 +- gpiozero/__init__.py | 47 +-- gpiozero/devices.py | 205 ++++++++---- gpiozero/exc.py | 32 +- gpiozero/input_devices.py | 14 +- gpiozero/mixins.py | 2 +- gpiozero/output_devices.py | 22 +- gpiozero/pins/__init__.py | 514 +++++++++++++++++++++++++++---- gpiozero/pins/data.py | 62 ++-- gpiozero/pins/local.py | 241 +++++++++++++++ gpiozero/pins/mock.py | 117 +++---- gpiozero/pins/native.py | 140 ++++----- gpiozero/pins/pi.py | 214 +++++++++++++ gpiozero/pins/pigpiod.py | 417 +++++++++++++++++++------ gpiozero/pins/rpigpio.py | 94 +++--- gpiozero/pins/rpio.py | 108 +++---- gpiozero/pins/spi.py | 86 ++++++ gpiozero/spi.py | 419 ------------------------- gpiozero/spi_devices.py | 6 +- setup.py | 12 +- tests/conftest.py | 10 + tests/test_boards.py | 255 +++++++-------- tests/test_devices.py | 75 ++--- tests/test_inputs.py | 57 ++-- tests/test_mock_pin.py | 62 ++-- tests/test_outputs.py | 190 ++++++------ tests/test_pins_data.py | 58 ++-- tests/test_spi.py | 126 ++++---- tests/test_spi_devices.py | 8 +- 34 files changed, 2311 insertions(+), 1456 deletions(-) create mode 100644 gpiozero/pins/local.py create mode 100644 gpiozero/pins/pi.py create mode 100644 gpiozero/pins/spi.py delete mode 100644 gpiozero/spi.py create mode 100644 tests/conftest.py diff --git a/docs/api_exc.rst b/docs/api_exc.rst index 3a8b932..f68ed72 100644 --- a/docs/api_exc.rst +++ b/docs/api_exc.rst @@ -59,6 +59,20 @@ Errors .. autoexception:: SPIBadArgs +.. autoexception:: SPIBadChannel + +.. autoexception:: SPIFixedClockMode + +.. autoexception:: SPIInvalidClockMode + +.. autoexception:: SPIFixedBitOrder + +.. autoexception:: SPIFixedSelect + +.. autoexception:: SPIFixedWordSize + +.. autoexception:: SPIInvalidWordSize + .. autoexception:: GPIODeviceError .. autoexception:: GPIODeviceClosed @@ -83,23 +97,31 @@ Errors .. autoexception:: PinInvalidEdges +.. autoexception:: PinInvalidBounce + .. autoexception:: PinSetInput .. autoexception:: PinFixedPull .. autoexception:: PinEdgeDetectUnsupported +.. autoexception:: PinGPIOUnsupported + +.. autoexception:: PinSPIUnsupported + .. autoexception:: PinPWMError .. autoexception:: PinPWMUnsupported .. autoexception:: PinPWMFixedValue +.. autoexception:: PinUnknownPi + .. autoexception:: PinMultiplePins .. autoexception:: PinNoPins -.. autoexception:: PinUnknownPi +.. autoexception:: PinInvalidPin Warnings ======== @@ -110,3 +132,7 @@ Warnings .. autoexception:: SPISoftwareFallback +.. autoexception:: PinFactoryFallback + +.. autoexception:: PinNonPhysical + diff --git a/docs/api_pins.rst b/docs/api_pins.rst index 97723ca..fd8e0e6 100644 --- a/docs/api_pins.rst +++ b/docs/api_pins.rst @@ -11,68 +11,68 @@ 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 GPIO pin number. However, what the -library actually expects is a :class:`Pin` implementation. If it finds a simple -integer number instead, it uses one of the following classes to provide the -:class:`Pin` implementation (classes are listed in favoured order): +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. -1. :class:`gpiozero.pins.rpigpio.RPiGPIOPin` +Changing the pin factory +======================== -2. :class:`gpiozero.pins.rpio.RPIOPin` +The default pin factory can be replaced by specifying a value for the +``GPIOZERO_PIN_FACTORY`` environment variable. For example: -3. :class:`gpiozero.pins.pigpiod.PiGPIOPin` +.. code-block:: console -4. :class:`gpiozero.pins.native.NativePin` + pi@raspberrypi $ 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. + >>> import gpiozero + >>> gpiozero.Device._pin_factory + -You can change the default pin implementation by over-writing the -``pin_factory`` global in the ``devices`` module like so:: +The following values, and the corresponding :class:`Factory` and :class:`Pin` +classes are listed in the table below. Factories are listed in the order that +they are tried by default. - from gpiozero.pins.native import NativePin - import gpiozero.devices - # Force the default pin implementation to be NativePin - gpiozero.devices.pin_factory = NativePin ++---------+-----------------------------------------------+-------------------------------------------+ +| Name | Factory class | Pin class | ++=========+===============================================+===========================================+ +| rpigpio | :class:`gpiozero.pins.rpigpio.RPiGPIOFactory` | :class:`gpiozero.pins.rpigpio.RPiGPIOPin` | ++---------+-----------------------------------------------+-------------------------------------------+ +| rpio | :class:`gpiozero.pins.rpio.RPIOFactory` | :class:`gpiozero.pins.rpio.RPIOPin` | ++---------+-----------------------------------------------+-------------------------------------------+ +| pigpio | :class:`gpiozero.pins.pigpiod.PiGPIOFactory` | :class:`gpiozero.pins.pigpiod.PiGPIOPin` | ++---------+-----------------------------------------------+-------------------------------------------+ +| native | :class:`gpiozero.pins.native.NativeFactory` | :class:`gpiozero.pins.native.NativePin` | ++---------+-----------------------------------------------+-------------------------------------------+ + +If you need to change the default pin factory from within a script, use the +``Device._set_pin_factory`` class method, passing in the instance of the new +factory to use. This is only supported at script startup (replacing the factory +closes all existing pin instances which can have interesting consequences for +any devices using them):: + + from gpiozero.pins.native import NativeFactory + from gpiozero import * + Device._set_pin_factory(NativeFactory()) from gpiozero import LED # This will now use NativePin instead of RPiGPIOPin led = LED(16) -``pin_factory`` is a concrete descendent of the abstract :class:`Pin` class. -The descendent may take additional parameters in its constructor provided they -are optional; GPIO Zero will expect to be able to construct instances with -nothing more than an integer pin number. - -However, the descendent may take default information from additional sources. +Certain factories may take default information from additional sources. For example, to default to creating pins with :class:`gpiozero.pins.pigpiod.PiGPIOPin` on a remote pi called ``remote-pi`` you can set the :envvar:`PIGPIO_ADDR` environment variable when running your -script:: +script: - $ PIGPIO_ADDR=remote-pi python my_script.py +.. code-block:: console -It is worth noting that instead of passing an integer to device constructors, -you can pass an object derived from :class:`Pin` itself:: - - from gpiozero.pins.native import NativePin - from gpiozero import LED - - led = LED(NativePin(16)) - -In future, this separation of pins and devices should also permit the library -to utilize pins that are part of IO extender chips. For example:: - - from gpiozero import IOExtender, LED - - ext = IOExtender() - led = LED(ext.pins[0]) - led.on() - -.. warning:: - - While the devices API is now considered stable and won't change in - backwards incompatible ways, the pins API is *not* yet considered stable. - It is potentially subject to change in future versions. We welcome any - comments from testers! + $ export GPIOZERO_PIN_FACTORY=pigpio + $ PIGPIO_ADDR=remote-pi python3 my_script.py .. warning:: @@ -83,41 +83,48 @@ to utilize pins that are part of IO extender chips. For example:: actual raspberry pie, you have only yourself to blame. -RPiGPIOPin -========== +RPi.GPIO +======== + +.. autoclass:: gpiozero.pins.rpigpio.RPiGPIOFactory .. autoclass:: gpiozero.pins.rpigpio.RPiGPIOPin -RPIOPin -======= +RPIO +==== + +.. autoclass:: gpiozero.pins.rpio.RPIOFactory .. autoclass:: gpiozero.pins.rpio.RPIOPin -PiGPIOPin -========= +PiGPIO +====== + +.. autoclass:: gpiozero.pins.pigpiod.PiGPIOFactory .. autoclass:: gpiozero.pins.pigpiod.PiGPIOPin -NativePin -========= +Native +====== + +.. autoclass:: gpiozero.pins.native.NativeFactory .. autoclass:: gpiozero.pins.native.NativePin -Abstract Pin +Base classes ============ +.. autoclass:: Factory + :members: + .. autoclass:: Pin :members: - -Local Pin -========= - -.. autoclass:: LocalPin +.. autoclass:: SPI :members: diff --git a/docs/images/composed_devices.pdf b/docs/images/composed_devices.pdf index 387e179ef6a7d42e38ded90c51285360a37ab9fb..6bdefbafe7b5c35e14591023a97c18df99aa53ac 100644 GIT binary patch delta 8335 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 j55~w(!VdgTH9x=D|Kil*4 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 diff --git a/docs/images/composed_devices.png b/docs/images/composed_devices.png index 692c1b17f633b280d86da90b38fdf2c7bf85e321..d076f4d5a28bb2897b9579debb7e90bfbea3942f 100644 GIT binary patch literal 34875 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�Q+9uol#q(IdCz?WI`cUGvrzjOzX)LPlAsxc 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 diff --git a/docs/notes.rst b/docs/notes.rst index 7c4ba69..859647f 100644 --- a/docs/notes.rst +++ b/docs/notes.rst @@ -4,6 +4,7 @@ Notes .. currentmodule:: gpiozero + .. _keep-your-script-running: Keep your script running @@ -46,6 +47,7 @@ events to be detected:: button.when_pressed = hello pause() + Importing from GPIO Zero ======================== @@ -70,12 +72,15 @@ In this case, all references to items within GPIO Zero must be prefixed:: button = gpiozero.Button(2) + How can I tell what version of gpiozero I have installed? ========================================================= The gpiozero library relies on the setuptools package for installation services. You can use the setuptools ``pkg_resources`` API to query which -version of gpiozero is available in your Python environment like so:: +version of gpiozero is available in your Python environment like so: + +.. code-block:: pycon >>> from pkg_resources import require >>> require('gpiozero') @@ -89,7 +94,9 @@ the first entry in the list will be the version that ``import gpiozero`` will import. If you receive the error "No module named pkg_resources", you need to install -the ``pip`` utility. This can be done with the following command in Raspbian:: +the ``pip`` utility. This can be done with the following command in Raspbian: + +.. code-block:: console $ sudo apt-get install python-pip diff --git a/docs/recipes.rst b/docs/recipes.rst index 5c64dd3..93bb0bd 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -9,7 +9,7 @@ library. Please note that all recipes are written assuming Python 3. Recipes *may* work under Python 2, but no guarantees! -.. _pin_numbering: +.. _pin-numbering: Pin Numbering ============= @@ -429,7 +429,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 @@ -439,7 +441,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/gpiozero/__init__.py b/gpiozero/__init__.py index e147943..31e8dc3 100644 --- a/gpiozero/__init__.py +++ b/gpiozero/__init__.py @@ -6,8 +6,9 @@ from __future__ import ( ) from .pins import ( + Factory, Pin, - LocalPin, + SPI, ) from .pins.data import ( PiBoardInfo, @@ -15,47 +16,9 @@ from .pins.data import ( PinInfo, pi_info, ) -from .exc import ( - GPIOZeroError, - DeviceClosed, - BadEventHandler, - BadWaitTime, - BadQueueLen, - CompositeDeviceError, - CompositeDeviceBadName, - CompositeDeviceBadOrder, - CompositeDeviceBadDevice, - SPIError, - SPIBadArgs, - EnergenieSocketMissing, - EnergenieBadSocket, - GPIODeviceError, - GPIODeviceClosed, - GPIOPinInUse, - GPIOPinMissing, - InputDeviceError, - OutputDeviceError, - OutputDeviceBadValue, - PinError, - PinInvalidFunction, - PinInvalidState, - PinInvalidPull, - PinInvalidEdges, - PinSetInput, - PinFixedPull, - PinEdgeDetectUnsupported, - PinPWMError, - PinPWMUnsupported, - PinPWMFixedValue, - PinUnknownPi, - PinMultiplePins, - PinNoPins, - GPIOZeroWarning, - SPIWarning, - SPISoftwareFallback, - PinWarning, - PinNonPhysical, -) +# Yes, import * is naughty, but exc imports nothing else so there's no cross +# contamination here ... and besides, have you *seen* the list lately?! +from .exc import * from .devices import ( Device, GPIODevice, diff --git a/gpiozero/devices.py b/gpiozero/devices.py index 001f1d0..c8c5b49 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -10,15 +10,16 @@ str = type('') import os import atexit import weakref +import warnings from collections import namedtuple from itertools import chain from types import FunctionType -from threading import RLock +from threading import Lock import pkg_resources +from .pins import Pin from .threads import _threads_shutdown -from .pins import _pins_shutdown from .mixins import ( ValuesMixin, SharedMixin, @@ -32,52 +33,11 @@ from .exc import ( GPIOPinMissing, GPIOPinInUse, GPIODeviceClosed, + PinFactoryFallback, ) from .compat import frozendict -def _default_pin_factory(name=os.getenv('GPIOZERO_PIN_FACTORY', None)): - group = 'gpiozero_pin_factories' - if name is None: - # If no factory is explicitly specified, try various names in - # "preferred" order. Note that in this case we only select from - # gpiozero distribution so without explicitly specifying a name (via - # the environment) it's impossible to auto-select a factory from - # outside the base distribution - # - # We prefer RPi.GPIO here as it supports PWM, and all Pi revisions. If - # no third-party libraries are available, however, we fall back to a - # pure Python implementation which supports platforms like PyPy - dist = pkg_resources.get_distribution('gpiozero') - for name in ('RPiGPIOPin', 'RPIOPin', 'PiGPIOPin', 'NativePin'): - try: - return pkg_resources.load_entry_point(dist, group, name) - except ImportError: - pass - raise BadPinFactory('Unable to locate any default pin factory!') - else: - for factory in pkg_resources.iter_entry_points(group, name): - return factory.load() - raise BadPinFactory('Unable to locate pin factory "%s"' % name) - -pin_factory = _default_pin_factory() - - -_PINS = set() -_PINS_LOCK = RLock() # Yes, this needs to be re-entrant - -def _shutdown(): - _threads_shutdown() - with _PINS_LOCK: - while _PINS: - _PINS.pop().close() - # Any cleanup routines registered by pins libraries must be called *after* - # cleanup of pin objects used by devices - _pins_shutdown() - -atexit.register(_shutdown) - - class GPIOMeta(type): # NOTE Yes, this is a metaclass. Don't be scared - it's a simple one. @@ -106,7 +66,7 @@ class GPIOMeta(type): # already exists. Only construct the instance if the key's new. key = cls._shared_key(*args, **kwargs) try: - self = cls._INSTANCES[key] + self = cls._instances[key] self._refs += 1 except (KeyError, ReferenceError) as e: self = super(GPIOMeta, cls).__call__(*args, **kwargs) @@ -122,14 +82,14 @@ class GPIOMeta(type): old_close() finally: try: - del cls._INSTANCES[key] + del cls._instances[key] except KeyError: # If the _refs go negative (too many closes) # just ignore the resulting KeyError here - # it's already gone pass self.close = close - cls._INSTANCES[key] = weakref.proxy(self) + cls._instances[key] = weakref.proxy(self) else: # Construct the instance as normal self = super(GPIOMeta, cls).__call__(*args, **kwargs) @@ -229,13 +189,100 @@ class GPIOBase(GPIOMeta(nstr('GPIOBase'), (), {})): class Device(ValuesMixin, GPIOBase): """ Represents a single device of any type; GPIO-based, SPI-based, I2C-based, - etc. This is the base class of the device hierarchy. It defines the - basic services applicable to all devices (specifically the :attr:`is_active` + etc. This is the base class of the device hierarchy. It defines the basic + services applicable to all devices (specifically the :attr:`is_active` property, the :attr:`value` property, and the :meth:`close` method). """ + _pin_factory = None # instance of a Factory sub-class + _reservations = {} # maps pin addresses to lists of devices + _res_lock = Lock() + def __repr__(self): return "" % (self.__class__.__name__) + @classmethod + def _set_pin_factory(cls, new_factory): + if cls._pin_factory is not None: + cls._pin_factory.close() + cls._pin_factory = new_factory + + def _reserve_pins(self, *pins_or_addresses): + """ + Called to indicate that the device reserves the right to use the + specified *pins_or_addresses*. This should be done during device + construction. If pins are reserved, you must ensure that the + reservation is released by eventually called :meth:`_release_pins`. + + The *pins_or_addresses* can be actual :class:`Pin` instances or the + addresses of pin instances (each address is a tuple of strings). The + latter form is permitted to ensure that devices do not have to + construct :class:`Pin` objects to reserve pins. This is important as + constructing a pin often configures it (e.g. as an input) which + conflicts with alternate pin functions like SPI. + """ + addresses = ( + p.address if isinstance(p, Pin) else p + for p in pins_or_addresses + ) + with self._res_lock: + for address in addresses: + try: + conflictors = self._reservations[address] + except KeyError: + conflictors = [] + self._reservations[address] = conflictors + for device_ref in conflictors: + device = device_ref() + if device is not None and self._conflicts_with(device): + raise GPIOPinInUse( + 'pin %s is already in use by %r' % ( + '/'.join(address), device) + ) + conflictors.append(weakref.ref(self)) + + def _release_pins(self, *pins_or_addresses): + """ + Releases the reservation of this device against *pins_or_addresses*. + This is typically called during :meth:`close` to clean up reservations + taken during construction. Releasing a reservation that is not + currently held will be silently ignored (to permit clean-up after + failed / partial construction). + """ + addresses = ( + p.address if isinstance(p, Pin) else p + for p in pins_or_addresses + ) + with self._res_lock: + for address in addresses: + self._reservations[address] = [ + ref for ref in self._reservations[address] + if ref() not in (self, None) # may as well clean up dead refs + ] + + def _release_all(self): + """ + Releases all pin reservations taken out by this device. See + :meth:`_release_pins` for further information). + """ + with self._res_lock: + Device._reservations = { + address: [ + ref for ref in conflictors + if ref() not in (self, None) + ] + for address, conflictors in self._reservations.items() + } + + def _conflicts_with(self, other): + """ + Called by :meth:`_reserve_pin` to test whether the *other* + :class:`Device` using a common pin conflicts with this device's intent + to use it. The default is ``True`` indicating that all devices conflict + with common pins. Sub-classes may override this to permit more nuanced + replies. + """ + return True + @property def value(self): """ @@ -378,14 +425,12 @@ class GPIODevice(Device): self._pin = None if pin is None: raise GPIOPinMissing('No pin given') - if isinstance(pin, int): - pin = pin_factory(pin) - with _PINS_LOCK: - if pin in _PINS: - raise GPIOPinInUse( - 'pin %r is already in use by another gpiozero object' % pin - ) - _PINS.add(pin) + if isinstance(pin, Pin): + self._reserve_pins(pin) + else: + # Check you can reserve *before* constructing the pin + self._reserve_pins(self._pin_factory.pin_address(pin)) + pin = self._pin_factory.pin(pin) self._pin = pin self._active_state = True self._inactive_state = False @@ -402,12 +447,10 @@ class GPIODevice(Device): def close(self): super(GPIODevice, self).close() - with _PINS_LOCK: - pin = self._pin + if self._pin is not None: + self._release_pins(self._pin) + self._pin.close() self._pin = None - if pin in _PINS: - _PINS.remove(pin) - pin.close() @property def closed(self): @@ -441,3 +484,41 @@ class GPIODevice(Device): except DeviceClosed: return "" % self.__class__.__name__ + +# Defined last to ensure Device is defined before attempting to load any pin +# factory; pin factories want to load spi which in turn relies on devices (for +# the soft-SPI implementation) +def _default_pin_factory(name=os.getenv('GPIOZERO_PIN_FACTORY', None)): + group = 'gpiozero_pin_factories' + if name is None: + # If no factory is explicitly specified, try various names in + # "preferred" order. Note that in this case we only select from + # gpiozero distribution so without explicitly specifying a name (via + # the environment) it's impossible to auto-select a factory from + # outside the base distribution + # + # We prefer RPi.GPIO here as it supports PWM, and all Pi revisions. If + # no third-party libraries are available, however, we fall back to a + # pure Python implementation which supports platforms like PyPy + dist = pkg_resources.get_distribution('gpiozero') + for name in ('rpigpio', 'rpio', 'pigpio', 'native'): + try: + return pkg_resources.load_entry_point(dist, group, name)() + except Exception as e: + warnings.warn( + PinFactoryFallback( + 'Failed to load factory %s: %s' % (name, str(e)))) + raise BadPinFactory('Unable to load any default pin factory!') + else: + for factory in pkg_resources.iter_entry_points(group, name.lower()): + return factory.load()() + raise BadPinFactory('Unable to find pin factory "%s"' % name) + +Device._set_pin_factory(_default_pin_factory()) + +def _shutdown(): + _threads_shutdown() + Device._set_pin_factory(None) + +atexit.register(_shutdown) + diff --git a/gpiozero/exc.py b/gpiozero/exc.py index aedc96f..9ed9361 100644 --- a/gpiozero/exc.py +++ b/gpiozero/exc.py @@ -52,6 +52,24 @@ class SPIBadArgs(SPIError, ValueError): class SPIBadChannel(SPIError, ValueError): "Error raised when an invalid channel is given to an :class:`AnalogInputDevice`" +class SPIFixedClockMode(SPIError, AttributeError): + "Error raised when the SPI clock mode cannot be changed" + +class SPIInvalidClockMode(SPIError, ValueError): + "Error raised when an invalid clock mode is given to an SPI implementation" + +class SPIFixedBitOrder(SPIError, AttributeError): + "Error raised when the SPI bit-endianness cannot be changed" + +class SPIFixedSelect(SPIError, AttributeError): + "Error raised when the SPI select polarity cannot be changed" + +class SPIFixedWordSize(SPIError, AttributeError): + "Error raised when the number of bits per word cannot be changed" + +class SPIInvalidWordSize(SPIError, ValueError): + "Error raised when an invalid (out of range) number of bits per word is specified" + class GPIODeviceError(GPIOZeroError): "Base class for errors specific to the GPIODevice hierarchy" @@ -62,7 +80,7 @@ class GPIOPinInUse(GPIODeviceError): "Error raised when attempting to use a pin already in use by another device" class GPIOPinMissing(GPIODeviceError, ValueError): - "Error raised when a pin number is not specified" + "Error raised when a pin specification is not given" class InputDeviceError(GPIODeviceError): "Base class for errors specific to the InputDevice hierarchy" @@ -100,6 +118,12 @@ class PinFixedPull(PinError, AttributeError): class PinEdgeDetectUnsupported(PinError, AttributeError): "Error raised when attempting to use edge detection on unsupported pins" +class PinGPIOUnsupported(PinError, NotImplementedError): + "Error raised when attempting to obtain a GPIO interface on unsupported pins" + +class PinSPIUnsupported(PinError, NotImplementedError): + "Error raised when attempting to obtain an SPI interface on unsupported pins" + class PinPWMError(PinError): "Base class for errors related to PWM implementations" @@ -118,6 +142,9 @@ class PinMultiplePins(PinError, RuntimeError): class PinNoPins(PinError, RuntimeError): "Error raised when no pins support the requested function" +class PinInvalidPin(PinError, ValueError): + "Error raised when an invalid pin specification is provided" + class GPIOZeroWarning(Warning): "Base class for all warnings in GPIO Zero" @@ -130,6 +157,9 @@ class SPISoftwareFallback(SPIWarning): class PinWarning(GPIOZeroWarning): "Base class for warnings related to pin implementations" +class PinFactoryFallback(PinWarning): + "Warning raised when a default pin factory fails to load and a fallback is tried" + class PinNonPhysical(PinWarning): "Warning raised when a non-physical pin is specified in a constructor" diff --git a/gpiozero/input_devices.py b/gpiozero/input_devices.py index 8a4e592..e65b769 100644 --- a/gpiozero/input_devices.py +++ b/gpiozero/input_devices.py @@ -165,7 +165,7 @@ class SmoothedInputDevice(EventsMixin, InputDevice): if self.partial or self._queue.full.is_set(): return super(SmoothedInputDevice, self).__repr__() else: - return "" % ( + return "" % ( self.__class__.__name__, self.pin, self.pull_up) @property @@ -240,7 +240,7 @@ class Button(HoldMixin, DigitalInputDevice): print("The button was pressed!") :param int pin: - The GPIO pin which the button is attached to. See :ref:`pin_numbering` + The GPIO pin which the button is attached to. See :ref:`pin-numbering` for valid pin numbers. :param bool pull_up: @@ -302,7 +302,7 @@ class LineSensor(SmoothedInputDevice): pause() :param int pin: - The GPIO pin which the sensor is attached to. See :ref:`pin_numbering` + The GPIO pin which the sensor is attached to. See :ref:`pin-numbering` for valid pin numbers. :param int queue_len: @@ -371,7 +371,7 @@ class MotionSensor(SmoothedInputDevice): print("Motion detected!") :param int pin: - The GPIO pin which the sensor is attached to. See :ref:`pin_numbering` + The GPIO pin which the sensor is attached to. See :ref:`pin-numbering` for valid pin numbers. :param int queue_len: @@ -435,7 +435,7 @@ class LightSensor(SmoothedInputDevice): print("Light detected!") :param int pin: - The GPIO pin which the sensor is attached to. See :ref:`pin_numbering` + The GPIO pin which the sensor is attached to. See :ref:`pin-numbering` for valid pin numbers. :param int queue_len: @@ -543,11 +543,11 @@ class DistanceSensor(SmoothedInputDevice): :param int echo: The GPIO pin which the ECHO pin is attached to. See - :ref:`pin_numbering` for valid pin numbers. + :ref:`pin-numbering` for valid pin numbers. :param int trigger: The GPIO pin which the TRIG pin is attached to. See - :ref:`pin_numbering` for valid pin numbers. + :ref:`pin-numbering` for valid pin numbers. :param int queue_len: The length of the queue used to store values read from the sensor. diff --git a/gpiozero/mixins.py b/gpiozero/mixins.py index a2e390e..669617f 100644 --- a/gpiozero/mixins.py +++ b/gpiozero/mixins.py @@ -127,7 +127,7 @@ class SharedMixin(object): When :meth:`close` is called, an internal reference counter will be decremented and the instance will only close when it reaches zero. """ - _INSTANCES = {} + _instances = {} def __del__(self): self._refs = 0 diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index 1b3c2e2..9c24ba6 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -128,8 +128,8 @@ class DigitalOutputDevice(OutputDevice): """ def __init__(self, pin=None, active_high=True, initial_value=False): self._blink_thread = None - super(DigitalOutputDevice, self).__init__(pin, active_high, initial_value) self._controller = None + super(DigitalOutputDevice, self).__init__(pin, active_high, initial_value) @property def value(self): @@ -217,7 +217,7 @@ class LED(DigitalOutputDevice): led.on() :param int pin: - The GPIO pin which the LED is attached to. See :ref:`pin_numbering` for + The GPIO pin which the LED is attached to. See :ref:`pin-numbering` for valid pin numbers. :param bool active_high: @@ -252,7 +252,7 @@ class Buzzer(DigitalOutputDevice): bz.on() :param int pin: - The GPIO pin which the buzzer is attached to. See :ref:`pin_numbering` + The GPIO pin which the buzzer is attached to. See :ref:`pin-numbering` for valid pin numbers. :param bool active_high: @@ -276,7 +276,7 @@ class PWMOutputDevice(OutputDevice): Generic output device configured for pulse-width modulation (PWM). :param int pin: - The GPIO pin which the device is attached to. See :ref:`pin_numbering` + The GPIO pin which the device is attached to. See :ref:`pin-numbering` for valid pin numbers. :param bool active_high: @@ -483,7 +483,7 @@ class PWMLED(PWMOutputDevice): an optional resistor to prevent the LED from burning out. :param int pin: - The GPIO pin which the LED is attached to. See :ref:`pin_numbering` for + The GPIO pin which the LED is attached to. See :ref:`pin-numbering` for valid pin numbers. :param bool active_high: @@ -562,8 +562,12 @@ class RGBLED(SourceMixin, Device): 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)) - self.value = initial_value + try: + self._leds = tuple(LEDClass(pin, active_high) for pin in (red, green, blue)) + self.value = initial_value + except: + self.close() + raise red = _led_property(0) green = _led_property(1) @@ -926,7 +930,7 @@ class Servo(SourceMixin, CompositeDevice): sleep(1) :param int pin: - The GPIO pin which the device is attached to. See :ref:`pin_numbering` + The GPIO pin which the device is attached to. See :ref:`pin-numbering` for valid pin numbers. :param float initial_value: @@ -1116,7 +1120,7 @@ class AngularServo(Servo): expectations of minimum and maximum. :param int pin: - The GPIO pin which the device is attached to. See :ref:`pin_numbering` + The GPIO pin which the device is attached to. See :ref:`pin-numbering` for valid pin numbers. :param float initial_angle: diff --git a/gpiozero/pins/__init__.py b/gpiozero/pins/__init__.py index 3503145..f88cb11 100644 --- a/gpiozero/pins/__init__.py +++ b/gpiozero/pins/__init__.py @@ -1,3 +1,5 @@ +# vim: set fileencoding=utf-8: + from __future__ import ( unicode_literals, absolute_import, @@ -6,32 +8,124 @@ from __future__ import ( ) str = type('') -import io - -from .data import pi_info from ..exc import ( PinInvalidFunction, PinSetInput, PinFixedPull, + PinSPIUnsupported, PinPWMUnsupported, PinEdgeDetectUnsupported, + SPIFixedClockMode, + SPIFixedBitOrder, + SPIFixedSelect, + SPIFixedWordSize, ) -PINS_CLEANUP = [] -def _pins_shutdown(): - for routine in PINS_CLEANUP: - routine() +class Factory(object): + """ + Generates pins, SPI, and I2C interfaces for devices. This is an abstract + base class for pin factories. Descendents must override: + + * :meth:`_get_address` + * :meth:`pin_address` + + Descendents may override: + + * :meth:`close` + * :meth:`pin` + * :meth:`spi` + * :meth:`_get_pi_info` + """ + + def close(self): + """ + Closes the pin factory. This is expected to clean up all resources + manipulated by the factory. It it typically called at script + termination. + """ + pass + + def pin(self, spec): + """ + Creates an instance of a :class:`Pin` descendent representing the + specified pin. + + .. warning:: + + Descendents must ensure that pin instances representing the same + hardware are identical; i.e. two separate invocations of + :meth:`pin` for the same pin specification must return the same + object. + """ + raise PinGPIOUnsupported("GPIO 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 + *port* and *device*, or for the specified pins (*clock_pin*, + *mosi_pin*, *miso_pin*, and *select_pin*). Only one of the schemes can + be used; attempting to mix *port* and *device* with pin numbers will + raise :exc:`SPIBadArgs`. + """ + raise PinSPIUnsupported('SPI not supported by this pin factory') + + 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 + + pi_info = property( + lambda self: self._get_pi_info(), + doc="""\ + Returns a :class:`PiBoardInfo` instance representing the Pi that + instances generated by this factory will be attached to. + + If the pins represented by this class are not *directly* attached to a + Pi (e.g. the pin is attached to a board attached to the Pi, or the pins + are not on a Pi at all), this may return ``None``. + """) class Pin(object): """ - Abstract base class representing a GPIO pin or a pin from an IO extender. + Abstract base class representing a pin attached to some form of controller, + be it GPIO, SPI, ADC, etc. Descendents should override property getters and setters to accurately represent the capabilities of pins. The following functions *must* be overridden: + * :meth:`_get_address` * :meth:`_get_function` * :meth:`_set_function` * :meth:`_get_state` @@ -39,6 +133,8 @@ class Pin(object): The following functions *may* be overridden if applicable: * :meth:`close` + * :meth:`output_with_state` + * :meth:`input_with_pull` * :meth:`_set_state` * :meth:`_get_frequency` * :meth:`_set_frequency` @@ -50,20 +146,10 @@ class Pin(object): * :meth:`_set_edges` * :meth:`_get_when_changed` * :meth:`_set_when_changed` - * :meth:`pi_info` - * :meth:`output_with_state` - * :meth:`input_with_pull` - - .. warning:: - - Descendents must ensure that pin instances representing the same - physical hardware are identical, right down to object identity. The - framework relies on this to correctly clean up resources at interpreter - shutdown. """ def __repr__(self): - return "Abstract pin" + return self.address[-1] def close(self): """ @@ -105,6 +191,18 @@ 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" @@ -140,10 +238,19 @@ class Pin(object): doc="""\ The state of the pin. This is 0 for low, and 1 for high. As a low level view of the pin, no swapping is performed in the case of pull ups (see - :attr:`pull` for more information). + :attr:`pull` for more information): - If PWM is currently active (when :attr:`frequency` is not ``None``), - this represents the PWM duty cycle as a value between 0.0 and 1.0. + .. code-block:: text + + HIGH - - - - > ,---------------------- + | + | + LOW ----------------' + + Descendents which implement analog, or analog-like capabilities can + return values between 0 and 1. For example, pins implementing PWM + (where :attr:`frequency` is not ``None``) return a value between 0.0 + and 1.0 representing the current PWM duty cycle. If a pin is currently configured for input, and an attempt is made to set this attribute, :exc:`PinSetInput` will be raised. If an invalid @@ -205,6 +312,26 @@ class Pin(object): detection, measured in seconds. If bounce detection is not currently in use, this is ``None``. + For example, if :attr:`edge` is currently "rising", :attr:`bounce` is + currently 5/1000 (5ms), then the waveform below will only fire + :attr:`when_changed` on two occasions despite there being three rising + edges: + + .. code-block:: text + + TIME 0...1...2...3...4...5...6...7...8...9...10..11..12 ms + + bounce elimination |===================| |============== + + HIGH - - - - > ,--. ,--------------. ,--. + | | | | | | + | | | | | | + LOW ----------------' `-' `-' `----------- + : : + : : + when_changed when_changed + fires fires + If the pin does not support edge detection, attempts to set this property will raise :exc:`PinEdgeDetectUnsupported`. If the pin supports edge detection, the class must implement bounce detection, @@ -223,7 +350,18 @@ class Pin(object): doc="""\ The edge that will trigger execution of the function or bound method assigned to :attr:`when_changed`. This can be one of the strings - "both" (the default), "rising", "falling", or "none". + "both" (the default), "rising", "falling", or "none": + + .. code-block:: text + + HIGH - - - - > ,--------------. + | | + | | + LOW --------------------' `-------------- + : : + : : + Fires when_changed "both" "both" + when edges is ... "rising" "falling" If the pin does not support edge detection, attempts to set this property will raise :exc:`PinEdgeDetectUnsupported`. @@ -247,48 +385,300 @@ class Pin(object): property will raise :exc:`PinEdgeDetectUnsupported`. """) - @classmethod - def pi_info(cls): - """ - Returns a :class:`PiBoardInfo` instance representing the Pi that - instances of this pin class will be attached to. - If the pins represented by this class are not *directly* attached to a - Pi (e.g. the pin is attached to a board attached to the Pi, or the pins - are not on a Pi at all), this may return ``None``. - """ - return None - - -class LocalPin(Pin): +class SPI(object): """ - Abstract base class representing pins attached locally to a Pi. This forms - the base class for local-only pin interfaces (:class:`RPiGPIOPin`, - :class:`RPIOPin`, and :class:`NativePin`). + Abstract interface for `Serial Peripheral Interface`_ (SPI) implementations. + Descendents *must* override the following: + + * :meth:`transfer` + * :meth:`_get_clock_mode` + + Descendents *may* override the following methods: + + * :meth:`read` + * :meth:`write` + * :meth:`_set_clock_mode` + * :meth:`_get_lsb_first` + * :meth:`_set_lsb_first` + * :meth:`_get_select_high` + * :meth:`_set_select_high` + * :meth:`_get_bits_per_word` + * :meth:`_set_bits_per_word` + + .. _Serial Peripheral Interface: https://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus """ - _PI_REVISION = None - @classmethod - def pi_info(cls): + def read(self, n): """ - Returns a :class:`PiBoardInfo` instance representing the local Pi. - The Pi's revision is determined by reading :file:`/proc/cpuinfo`. If - no valid revision is found, returns ``None``. - """ - # Cache the result as we can reasonably assume it won't change during - # runtime (this is LocalPin after all; descendents that deal with - # remote Pis should inherit from Pin instead) - if cls._PI_REVISION is None: - with io.open('/proc/cpuinfo', 'r') as f: - for line in f: - if line.startswith('Revision'): - revision = line.split(':')[1].strip().lower() - overvolted = revision.startswith('100') - if overvolted: - revision = revision[-4:] - cls._PI_REVISION = revision - break - if cls._PI_REVISION is None: - return None # something weird going on - return pi_info(cls._PI_REVISION) + Read *n* words of data from the SPI interface, returning them as a + sequence of unsigned ints, each no larger than the configured + :attr:`bits_per_word` of the interface. + + This method is typically used with read-only devices that feature + half-duplex communication. See :meth:`transfer` for full duplex + communication. + """ + return self.transfer((0,) * n) + + def write(self, data): + """ + Write *data* to the SPI interface. *data* must be a sequence of + unsigned integer words each of which will fit within the configured + :attr:`bits_per_word` of the interface. The method returns the number + of words written to the interface (which may be less than or equal to + the length of *data*). + + This method is typically used with write-only devices that feature + half-duplex communication. See :meth:`transfer` for full duplex + communication. + """ + return len(self.transfer(data)) + + def transfer(self, data): + """ + Write *data* to the SPI interface. *data* must be a sequence of + unsigned integer words each of which will fit within the configured + :attr:`bits_per_word` of the interface. The method returns the sequence + of words read from the interface while writing occurred (full duplex + communication). + + The length of the sequence returned dictates the number of words of + *data* written to the interface. Each word in the returned sequence + will be an unsigned integer no larger than the configured + :attr:`bits_per_word` of the interface. + """ + raise NotImplementedError + + @property + def clock_polarity(self): + """ + The polarity of the SPI clock pin. If this is ``False`` (the default), + the clock pin will idle low, and pulse high. Setting this to ``True`` + will cause the clock pin to idle high, and pulse low. On many data + sheets this is documented as the CPOL value. + + The following diagram illustrates the waveform when + :attr:`clock_polarity` is ``False`` (the default), equivalent to CPOL + 0: + + .. code-block:: text + + on on on on on on on + ,---. ,---. ,---. ,---. ,---. ,---. ,---. + CLK | | | | | | | | | | | | | | + | | | | | | | | | | | | | | + ------' `---' `---' `---' `---' `---' `---' `------ + idle off off off off off off idle + + The following diagram illustrates the waveform when + :attr:`clock_polarity` is ``True``, equivalent to CPOL 1: + + .. code-block:: text + + idle off off off off off off idle + ------. ,---. ,---. ,---. ,---. ,---. ,---. ,------ + | | | | | | | | | | | | | | + CLK | | | | | | | | | | | | | | + `---' `---' `---' `---' `---' `---' `---' + on on on on on on on + """ + return bool(self.clock_mode & 2) + + @clock_polarity.setter + def clock_polarity(self, value): + self.clock_mode = self.clock_mode & (~2) | (bool(value) << 1) + + @property + def clock_phase(self): + """ + The phase of the SPI clock pin. If this is ``False`` (the default), + data will be read from the MISO pin when the clock pin activates. + Setting this to ``True`` will cause data to be read from the MISO pin + when the clock pin deactivates. On many data sheets this is documented + as the CPHA value. Whether the clock edge is rising or falling when the + clock is considered activated is controlled by the + :attr:`clock_polarity` attribute (corresponding to CPOL). + + The following diagram indicates when data is read when + :attr:`clock_polarity` is ``False``, and :attr:`clock_phase` is + ``False`` (the default), equivalent to CPHA 0: + + .. code-block:: text + + ,---. ,---. ,---. ,---. ,---. ,---. ,---. + CLK | | | | | | | | | | | | | | + | | | | | | | | | | | | | | + ----' `---' `---' `---' `---' `---' `---' `------- + : : : : : : : + MISO---. ,---. ,---. ,---. ,---. ,---. ,---. + / \ / \ / \ / \ / \ / \ / \\ + -{ Bit X Bit X Bit X Bit X Bit X Bit X Bit }------ + \ / \ / \ / \ / \ / \ / \ / + `---' `---' `---' `---' `---' `---' `---' + + The following diagram indicates when data is read when + :attr:`clock_polarity` is ``False``, but :attr:`clock_phase` is + ``True``, equivalent to CPHA 1: + + .. code-block:: text + + ,---. ,---. ,---. ,---. ,---. ,---. ,---. + CLK | | | | | | | | | | | | | | + | | | | | | | | | | | | | | + ----' `---' `---' `---' `---' `---' `---' `------- + : : : : : : : + MISO ,---. ,---. ,---. ,---. ,---. ,---. ,---. + / \ / \ / \ / \ / \ / \ / \\ + -----{ Bit X Bit X Bit X Bit X Bit X Bit X Bit }-- + \ / \ / \ / \ / \ / \ / \ / + `---' `---' `---' `---' `---' `---' `---' + """ + return bool(self.clock_mode & 1) + + @clock_phase.setter + def clock_phase(self, value): + self.clock_mode = self.clock_mode & (~1) | bool(value) + + def _get_clock_mode(self): + raise NotImplementedError + + def _set_clock_mode(self, value): + raise SPIFixedClockMode("clock_mode cannot be changed on %r" % self) + + clock_mode = property( + lambda self: self._get_clock_mode(), + lambda self, value: self._set_clock_mode(value), + doc="""\ + Presents a value representing the :attr:`clock_polarity` and + :attr:`clock_phase` attributes combined according to the following + table: + + +------+-----------------+--------------+ + | mode | polarity (CPOL) | phase (CPHA) | + +======+=================+==============+ + | 0 | False | False | + | 1 | False | True | + | 2 | True | False | + | 3 | True | True | + +------+-----------------+--------------+ + + Adjusting this value adjusts both the :attr:`clock_polarity` and + :attr:`clock_phase` attributes simultaneously. + """) + + def _get_lsb_first(self): + return False + + def _set_lsb_first(self, value): + raise SPIFixedBitOrder("lsb_first cannot be changed on %r" % self) + + lsb_first = property( + lambda self: self._get_lsb_first(), + lambda self, value: self._set_lsb_first(value), + doc="""\ + Controls whether words are read and written LSB in (Least Significant + Bit first) order. The default is ``False`` indicating that words are + read and written in MSB (Most Significant Bit first) order. + Effectively, this controls the `Bit endianness`_ of the connection. + + The following diagram shows the a word containing the number 5 (binary + 0101) transmitted on MISO with :attr:`bits_per_word` set to 4, and + :attr:`clock_mode` set to 0, when :attr:`lsb_first` is ``False`` (the + default): + + .. code-block:: text + + ,---. ,---. ,---. ,---. + CLK | | | | | | | | + | | | | | | | | + ----' `---' `---' `---' `----- + : ,-------. : ,-------. + MISO: | : | : | : | + : | : | : | : | + ----------' : `-------' : `---- + : : : : + MSB LSB + + And now with :attr:`lsb_first` set to ``True`` (and all other + parameters the same): + + .. code-block:: text + + ,---. ,---. ,---. ,---. + CLK | | | | | | | | + | | | | | | | | + ----' `---' `---' `---' `----- + ,-------. : ,-------. : + MISO: | : | : | : + | : | : | : | : + --' : `-------' : `----------- + : : : : + LSB MSB + + .. _Bit endianness: https://en.wikipedia.org/wiki/Endianness#Bit_endianness + """) + + def _get_select_high(self): + return False + + def _set_select_high(self, value): + raise SPIFixedSelect("select_high cannot be changed on %r" % self) + + select_high = property( + lambda self: self._get_select_high(), + lambda self, value: self._set_select_high(value), + doc="""\ + If ``False`` (the default), the chip select line is considered active + when it is pulled low. When set to ``True``, the chip select line is + considered active when it is driven high. + + The following diagram shows the waveform of the chip select line, and + the clock when :attr:`clock_polarity` is ``False``, and + :attr:`select_high` is ``False`` (the default): + + .. code-block:: text + + ---. ,------ + __ | | + CS | chip is selected, and will react to clock | idle + `-----------------------------------------------------' + + ,---. ,---. ,---. ,---. ,---. ,---. ,---. + CLK | | | | | | | | | | | | | | + | | | | | | | | | | | | | | + ----' `---' `---' `---' `---' `---' `---' `------- + + And when :attr:`select_high` is ``True``: + + .. code-block:: text + + ,-----------------------------------------------------. + CS | chip is selected, and will react to clock | idle + | | + ---' `------ + + ,---. ,---. ,---. ,---. ,---. ,---. ,---. + CLK | | | | | | | | | | | | | | + | | | | | | | | | | | | | | + ----' `---' `---' `---' `---' `---' `---' `------- + """) + + def _get_bits_per_word(self): + return 8 + + def _set_bits_per_word(self, value): + raise SPIFixedWordSize("bits_per_word cannot be changed on %r" % self) + + bits_per_word = property( + lambda self: self._get_bits_per_word(), + lambda self, value: self._set_bits_per_word(value), + doc="""\ + Controls the number of bits that make up a word, and thus where the + word boundaries appear in the data stream, and the maximum value of a + word. Defaults to 8 meaning that words are effectively bytes. + + Several implementations do not support non-byte-sized words. + """) + diff --git a/gpiozero/pins/data.py b/gpiozero/pins/data.py index 03d046e..4dbbda3 100644 --- a/gpiozero/pins/data.py +++ b/gpiozero/pins/data.py @@ -13,7 +13,7 @@ from itertools import cycle from operator import attrgetter from collections import namedtuple -from ..exc import PinUnknownPi, PinMultiplePins, PinNoPins +from ..exc import PinUnknownPi, PinMultiplePins, PinNoPins, PinInvalidPin # Some useful constants for describing pins @@ -119,8 +119,8 @@ A_BOARD = """\ BPLUS_BOARD = """\ {style:white on green},--------------------------------.{style:reset} -{style:white on green}| {P1:{style} col2}{style:white on green} P1 {style:black on white}+===={style:reset} -{style:white on green}| {P1:{style} col1}{style:white on green} {style:black on white}| USB{style:reset} +{style:white on green}| {J8:{style} col2}{style:white on green} J8 {style:black on white}+===={style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} {style:black on white}| USB{style:reset} {style:white on green}| {style:black on white}+===={style:reset} {style:white on green}| {style:bold}Pi Model {model:2s} V{pcb_revision:3s}{style:normal} |{style:reset} {style:white on green}| {style:on black}+----+{style:on green} {style:black on white}+===={style:reset} @@ -134,8 +134,8 @@ BPLUS_BOARD = """\ APLUS_BOARD = """\ {style:white on green},--------------------------.{style:reset} -{style:white on green}| {P1:{style} col2}{style:white on green} P1 |{style:reset} -{style:white on green}| {P1:{style} col1}{style:white on green} |{style:reset} +{style:white on green}| {J8:{style} col2}{style:white on green} J8 |{style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} |{style:reset} {style:white on green}| |{style:reset} {style:white on green}| {style:bold}Pi Model {model:2s} V{pcb_revision:3s}{style:normal} |{style:reset} {style:white on green}| {style:on black}+----+{style:on green} {style:black on white}+===={style:reset} @@ -149,8 +149,8 @@ APLUS_BOARD = """\ ZERO12_BOARD = """\ {style:white on green},-------------------------.{style:reset} -{style:white on green}| {P1:{style} col2}{style:white on green} P1 |{style:reset} -{style:white on green}| {P1:{style} col1}{style:white on green} |{style:reset} +{style:white on green}| {J8:{style} col2}{style:white on green} J8 |{style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} |{style:reset} {style:black on white}---+{style:white on green} {style:on black}+---+{style:on green} {style:bold}PiZero{style:normal} |{style:reset} {style:black on white} sd|{style:white on green} {style:on black}|SoC|{style:on green} {style:bold}V{pcb_revision:3s}{style:normal} |{style:reset} {style:black on white}---+|hdmi|{style:white on green} {style:on black}+---+{style:on green} {style:black on white}usb{style:on green} {style:black on white}pwr{style:white on green} |{style:reset} @@ -158,8 +158,8 @@ ZERO12_BOARD = """\ ZERO13_BOARD = """\ {style:white on green}.-------------------------.{style:reset} -{style:white on green}| {P1:{style} col2}{style:white on green} P1 |{style:reset} -{style:white on green}| {P1:{style} col1}{style:white on green} {style:black on white}|c{style:reset} +{style:white on green}| {J8:{style} col2}{style:white on green} J8 |{style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} {style:black on white}|c{style:reset} {style:black on white}---+{style:white on green} {style:on black}+---+{style:on green} {style:bold}Pi{model:6s}{style:normal}{style:black on white}|s{style:reset} {style:black on white} sd|{style:white on green} {style:on black}|SoC|{style:on green} {style:bold}V{pcb_revision:3s}{style:normal} {style:black on white}|i{style:reset} {style:black on white}---+|hdmi|{style:white on green} {style:on black}+---+{style:on green} {style:black on white}usb{style:on green} {style:on white}pwr{style:white on green} |{style:reset} @@ -216,7 +216,7 @@ REV2_P5 = { 7: (GND, False), 8: (GND, False), } -PLUS_P1 = { +PLUS_J8 = { 1: (V3_3, False), 2: (V5, False), 3: (GPIO2, True), 4: (V5, False), 5: (GPIO3, True), 6: (GND, False), @@ -379,12 +379,12 @@ PI_REVISIONS = { 0xd: ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, REV2_BOARD, ), 0xe: ('B', '2.0', '2012Q4', 'BCM2835', 'Sony', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, REV2_BOARD, ), 0xf: ('B', '2.0', '2012Q4', 'BCM2835', 'Qisda', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, REV2_BOARD, ), - 0x10: ('B+', '1.2', '2014Q3', 'BCM2835', 'Sony', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'P1': PLUS_P1}, BPLUS_BOARD, ), + 0x10: ('B+', '1.2', '2014Q3', 'BCM2835', 'Sony', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'J8': PLUS_J8}, BPLUS_BOARD, ), 0x11: ('CM', '1.1', '2014Q2', 'BCM2835', 'Sony', 512, 'eMMC', 1, 0, False, False, 2, 2, {'SODIMM': CM_SODIMM}, CM_BOARD, ), - 0x12: ('A+', '1.1', '2014Q4', 'BCM2835', 'Sony', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'P1': PLUS_P1}, APLUS_BOARD, ), - 0x13: ('B+', '1.2', '2015Q1', 'BCM2835', 'Egoman', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'P1': PLUS_P1}, BPLUS_BOARD, ), + 0x12: ('A+', '1.1', '2014Q4', 'BCM2835', 'Sony', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'J8': PLUS_J8}, APLUS_BOARD, ), + 0x13: ('B+', '1.2', '2015Q1', 'BCM2835', 'Egoman', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'J8': PLUS_J8}, BPLUS_BOARD, ), 0x14: ('CM', '1.1', '2014Q2', 'BCM2835', 'Embest', 512, 'eMMC', 1, 0, False, False, 2, 2, {'SODIMM': CM_SODIMM}, CM_BOARD, ), - 0x15: ('A+', '1.1', '2014Q4', 'BCM2835', 'Embest', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'P1': PLUS_P1}, APLUS_BOARD, ), + 0x15: ('A+', '1.1', '2014Q4', 'BCM2835', 'Embest', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'J8': PLUS_J8}, APLUS_BOARD, ), } @@ -529,7 +529,8 @@ class HeaderInfo(namedtuple('HeaderInfo', ( from gpiozero import * - print('{0:full}'.format(pi_info().headers['P1'])) + print('{0}'.format(pi_info().headers['J8'])) + print('{0:full}'.format(pi_info().headers['J8'])) print('{0:col2}'.format(pi_info().headers['P1'])) print('{0:row1}'.format(pi_info().headers['P1'])) @@ -537,10 +538,9 @@ class HeaderInfo(namedtuple('HeaderInfo', ( the use of `ANSI color codes`_. If neither is specified, ANSI codes will only be used if stdout is detected to be a tty:: - print('{0:color row2}'.format(pi_info().headers['P1'])) # force use of ANSI codes + print('{0:color row2}'.format(pi_info().headers['J8'])) # force use of ANSI codes print('{0:mono row2}'.format(pi_info().headers['P1'])) # force plain ASCII - .. _ANSI color codes: https://en.wikipedia.org/wiki/ANSI_escape_code The following attributes are defined: .. automethod:: pprint @@ -548,7 +548,7 @@ class HeaderInfo(namedtuple('HeaderInfo', ( .. attribute:: name The name of the header, typically as it appears silk-screened on the - board (e.g. "P1"). + board (e.g. "P1" or "J8"). .. attribute:: rows @@ -561,6 +561,8 @@ class HeaderInfo(namedtuple('HeaderInfo', ( .. attribute:: pins A dictionary mapping physical pin numbers to :class:`PinInfo` tuples. + + .. _ANSI color codes: https://en.wikipedia.org/wiki/ANSI_escape_code """ __slots__ = () # workaround python issue #24931 @@ -685,6 +687,7 @@ class PiBoardInfo(namedtuple('PiBoardInfo', ( from gpiozero import * + print('{0}'.format(pi_info())) print('{0:full}'.format(pi_info())) print('{0:board}'.format(pi_info())) print('{0:specs}'.format(pi_info())) @@ -801,8 +804,8 @@ class PiBoardInfo(namedtuple('PiBoardInfo', ( A dictionary which maps header labels to :class:`HeaderInfo` tuples. For example, to obtain information about header P1 you would query - ``headers['P1']``. To obtain information about pin 12 on header P1 you - would query ``headers['P1'].pins[12]``. + ``headers['P1']``. To obtain information about pin 12 on header J8 you + would query ``headers['J8'].pins[12]``. A rendered version of this data can be obtained by using the :class:`PiBoardInfo` object in a format string:: @@ -937,10 +940,10 @@ class PiBoardInfo(namedtuple('PiBoardInfo', ( }.get(model, csi) headers = { 'A': {'P1': REV2_P1, 'P5': REV2_P5}, - 'B': {'P1': REV2_P1, 'P5': REV2_P5} if pcb_revision == '2.0' else {'P1': REV1_P1}, + 'B': {'P1': REV1_P1} if pcb_revision == '1.0' else {'P1': REV2_P1, 'P5': REV2_P5}, 'CM': {'SODIMM': CM_SODIMM}, 'CM3': {'SODIMM': CM3_SODIMM}, - }.get(model, {'P1': PLUS_P1}) + }.get(model, {'J8': PLUS_J8}) board = { 'A': A_BOARD, 'B': REV1_BOARD if pcb_revision == '1.0' else REV2_BOARD, @@ -1115,8 +1118,8 @@ class PiBoardInfo(namedtuple('PiBoardInfo', ( """ Pretty-print a representation of the board along with header diagrams. - If *color* is ``None`` (the default, the diagram will include ANSI - color codes if stdout is a color-capable terminal). Otherwise *color* + If *color* is ``None`` (the default), the diagram will include ANSI + color codes if stdout is a color-capable terminal. Otherwise *color* can be set to ``True`` or ``False`` to force color or monochrome output. """ @@ -1134,13 +1137,10 @@ def pi_info(revision=None): the model of Pi it is running on and return information about that. """ if revision is None: - # NOTE: This import is declared locally for two reasons. Firstly it - # avoids a circular dependency (devices->pins->pins.data->devices). - # Secondly, pin_factory is one global which might potentially be - # re-written by a user's script at runtime hence we should re-import - # here in case it's changed since initialization - from ..devices import pin_factory - result = pin_factory.pi_info() + # The reason this import is located here is to avoid a circular + # dependency; devices->pins.local->pins.data->devices + from ..devices import Device + result = Device._pin_factory.pi_info if result is None: raise PinUnknownPi('The default pin_factory is not attached to a Pi') else: diff --git a/gpiozero/pins/local.py b/gpiozero/pins/local.py new file mode 100644 index 0000000..f61f4eb --- /dev/null +++ b/gpiozero/pins/local.py @@ -0,0 +1,241 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + +import io +import weakref +import warnings + +try: + from spidev import SpiDev +except ImportError: + SpiDev = None + +from . import SPI +from .pi import PiFactory, PiPin +from .spi import SPISoftwareBus +from ..devices import Device, SharedMixin +from ..output_devices import OutputDevice +from ..exc import DeviceClosed, PinUnknownPi, SPIInvalidClockMode + + +class LocalPiFactory(PiFactory): + """ + Abstract base class representing pins attached locally to a Pi. This forms + the base class for local-only pin interfaces (:class:`RPiGPIOPin`, + :class:`RPIOPin`, and :class:`NativePin`). + """ + pins = {} + + def __init__(self): + super(LocalPiFactory, self).__init__() + self.spi_hardware_class = LocalPiHardwareSPI + self.spi_software_class = LocalPiSoftwareSPI + self.shared_spi_hardware_class = LocalPiHardwareSPIShared + self.shared_spi_software_class = 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 + self.pins = LocalPiFactory.pins + + def _get_address(self): + return ('localhost',) + + def _get_revision(self): + # Cache the result as we can reasonably assume it won't change during + # runtime (this is LocalPin after all; descendents that deal with + # remote Pis should inherit from Pin instead) + with io.open('/proc/cpuinfo', 'r') as f: + for line in f: + if line.startswith('Revision'): + revision = line.split(':')[1].strip().lower() + overvolted = revision.startswith('100') + if overvolted: + revision = revision[-4:] + return revision + raise PinUnknownPi('unable to locate Pi revision in /proc/cpuinfo') + + +class LocalPiPin(PiPin): + """ + Abstract base class representing a multi-function GPIO pin attached to the + local Raspberry Pi. + """ + pass + + +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._intf = None + self._address = factory.address + ('SPI(port=%d, device=%d)' % (port, device),) + super(LocalPiHardwareSPI, self).__init__() + self._reserve_pins( + factory.pin_address(11), + factory.pin_address(10), + factory.pin_address(9), + factory.pin_address((8, 7)[device]) + ) + self._intf = SpiDev() + self._intf.open(port, device) + self._intf.max_speed_hz = 500000 + + def _conflicts_with(self, other): + return not ( + isinstance(other, LocalPiHardwareSPI) and + (self._port, self._device) != (other._port, other._device) + ) + + def close(self): + if self._intf: + try: + self._intf.close() + finally: + self._intf = None + self._release_all() + super(LocalPiHardwareSPI, self).close() + + @property + def closed(self): + return self._intf is None + + def __repr__(self): + try: + self._check_open() + return 'SPI(port=%d, device=%d)' % (self._port, self._device) + except DeviceClosed: + return 'SPI(closed)' + + def transfer(self, data): + """ + Writes data (a list of integer words where each word is assumed to have + :attr:`bits_per_word` bits or less) to the SPI interface, and reads an + equivalent number of words, returning them as a list of integers. + """ + return self._intf.xfer2(data) + + def _get_clock_mode(self): + return self._intf.mode + + def _set_clock_mode(self, value): + self._intf.mode = value + + def _get_lsb_first(self): + return self._intf.lsbfirst + + def _set_lsb_first(self, value): + self._intf.lsbfirst = bool(value) + + def _get_select_high(self): + return self._intf.cshigh + + def _set_select_high(self, value): + self._intf.cshigh = bool(value) + + def _get_bits_per_word(self): + return self._intf.bits_per_word + + def _set_bits_per_word(self, value): + self._intf.bits_per_word = value + + +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 + self._lsb_first = False + self._bits_per_word = 8 + self._bus = SPISoftwareBus(clock_pin, mosi_pin, miso_pin) + except: + self.close() + raise + + def close(self): + if self._bus: + self._bus.close() + self._bus = None + super(LocalPiSoftwareSPI, self).close() + + @property + def closed(self): + return self._bus is None + + def __repr__(self): + try: + self._check_open() + return 'SPI(clock_pin=%d, mosi_pin=%d, miso_pin=%d, select_pin=%d)' % ( + self._bus.clock.pin.number, + self._bus.mosi.pin.number, + self._bus.miso.pin.number, + self.pin.number) + except DeviceClosed: + return 'SPI(closed)' + + def transfer(self, data): + with self._bus.lock: + self.on() + try: + return self._bus.transfer( + data, self._clock_phase, self._lsb_first, self._bits_per_word) + finally: + self.off() + + def _get_clock_mode(self): + with self._bus.lock: + return (not self._bus.clock.active_high) << 1 | self._clock_phase + + def _set_clock_mode(self, value): + if not (0 <= value < 4): + raise SPIInvalidClockMode("%d is not a valid clock mode" % value) + with self._bus.lock: + self._bus.clock.active_high = not (value & 2) + self._clock_phase = bool(value & 1) + + def _get_lsb_first(self): + return self._lsb_first + + def _set_lsb_first(self, value): + self._lsb_first = bool(value) + + def _get_bits_per_word(self): + return self._bits_per_word + + def _set_bits_per_word(self, value): + if value < 1: + raise ValueError('bits_per_word must be positive') + self._bits_per_word = int(value) + + def _get_select_high(self): + return self.active_high + + def _set_select_high(self, value): + with self._bus.lock: + self.active_high = value + self.off() + + +class LocalPiHardwareSPIShared(SharedMixin, LocalPiHardwareSPI): + @classmethod + def _shared_key(cls, factory, port, device): + return (port, device) + + +class LocalPiSoftwareSPIShared(SharedMixin, LocalPiSoftwareSPI): + @classmethod + def _shared_key(cls, factory, clock_pin, mosi_pin, miso_pin, select_pin): + return (select_pin,) + diff --git a/gpiozero/pins/mock.py b/gpiozero/pins/mock.py index f12c8cc..8cced75 100644 --- a/gpiozero/pins/mock.py +++ b/gpiozero/pins/mock.py @@ -15,56 +15,28 @@ try: except ImportError: from ..compat import isclose -from . import Pin -from .data import pi_info -from ..exc import PinSetInput, PinPWMUnsupported, PinFixedPull +from ..exc import PinPWMUnsupported, PinSetInput, PinFixedPull +from ..devices import Device +from .pi import PiPin +from .local import LocalPiFactory PinState = namedtuple('PinState', ('timestamp', 'state')) -class MockPin(Pin): +class MockPin(PiPin): """ A mock pin used primarily for testing. This class does *not* support PWM. """ - _PINS = {} - - @classmethod - def clear_pins(cls): - cls._PINS.clear() - - @classmethod - def pi_info(cls): - return pi_info('a21041') # Pretend we're a Pi 2B - - def __new__(cls, number): - if not (0 <= number < 54): - raise ValueError('invalid pin %d specified (must be 0..53)' % number) - try: - old_pin = cls._PINS[number] - except KeyError: - self = super(MockPin, cls).__new__(cls) - cls._PINS[number] = self - self._number = number - self._function = 'input' - self._state = False - self._pull = 'floating' - self._bounce = None - self._edges = 'both' - self._when_changed = None - self.clear_states() - return self - # Ensure the pin class expected supports PWM (or not) - if issubclass(cls, MockPWMPin) != isinstance(old_pin, MockPWMPin): - raise ValueError('pin %d is already in use as a %s' % (number, old_pin.__class__.__name__)) - return old_pin - - def __repr__(self): - return 'MOCK%d' % self._number - - @property - def number(self): - return self._number + def __init__(self, factory, number): + super(MockPin, self).__init__(factory, number) + self._function = 'input' + self._state = False + self._pull = 'floating' + self._bounce = None + self._edges = 'both' + self._when_changed = None + self.clear_states() def close(self): self.when_changed = None @@ -186,8 +158,8 @@ class MockChargingPin(MockPin): (as if attached to, e.g. a typical circuit using an LDR and a capacitor to time the charging rate). """ - def __init__(self, number): - super(MockChargingPin, self).__init__() + def __init__(self, factory, number): + super(MockChargingPin, self).__init__(factory, number) self.charge_time = 0.01 # dark charging time self._charge_stop = Event() self._charge_thread = None @@ -225,8 +197,8 @@ class MockTriggerPin(MockPin): corresponding pin instance. When this pin is driven high it will trigger the echo pin to drive high for the echo time. """ - def __init__(self, number): - super(MockTriggerPin, self).__init__() + def __init__(self, factory, number): + super(MockTriggerPin, self).__init__(factory, number) self.echo_pin = None self.echo_time = 0.04 # longest echo time self._echo_thread = None @@ -250,8 +222,8 @@ class MockPWMPin(MockPin): """ This derivative of :class:`MockPin` adds PWM support. """ - def __init__(self, number): - super(MockPWMPin, self).__init__() + def __init__(self, factory, number): + super(MockPWMPin, self).__init__(factory, number) self._frequency = None def close(self): @@ -283,8 +255,8 @@ class MockSPIClockPin(MockPin): rather, construct a :class:`MockSPIDevice` with various pin numbers, and this class will be used for the clock pin. """ - def __init__(self, number): - super(MockSPIClockPin, self).__init__() + def __init__(self, factory, number): + super(MockSPIClockPin, self).__init__(factory, number) if not hasattr(self, 'spi_devices'): self.spi_devices = [] @@ -301,8 +273,8 @@ class MockSPISelectPin(MockPin): tests; rather, construct a :class:`MockSPIDevice` with various pin numbers, and this class will be used for the select pin. """ - def __init__(self, number): - super(MockSPISelectPin, self).__init__() + def __init__(self, factory, number): + super(MockSPISelectPin, self).__init__(factory, number) if not hasattr(self, 'spi_device'): self.spi_device = None @@ -314,13 +286,13 @@ class MockSPISelectPin(MockPin): class MockSPIDevice(object): def __init__( - self, clock_pin, mosi_pin, miso_pin, select_pin=None, + self, clock_pin, mosi_pin=None, miso_pin=None, select_pin=None, clock_polarity=False, clock_phase=False, lsb_first=False, bits_per_word=8, select_high=False): - self.clock_pin = MockSPIClockPin(clock_pin) - self.mosi_pin = None if mosi_pin is None else MockPin(mosi_pin) - self.miso_pin = None if miso_pin is None else MockPin(miso_pin) - self.select_pin = None if select_pin is None else MockSPISelectPin(select_pin) + self.clock_pin = Device._pin_factory.pin(clock_pin, pin_class=MockSPIClockPin) + self.mosi_pin = None if mosi_pin is None else Device._pin_factory.pin(mosi_pin) + self.miso_pin = None if miso_pin is None else Device._pin_factory.pin(miso_pin) + self.select_pin = None if select_pin is None else Device._pin_factory.pin(select_pin, pin_class=MockSPISelectPin) self.clock_polarity = clock_polarity self.clock_phase = clock_phase self.lsb_first = lsb_first @@ -413,3 +385,34 @@ class MockSPIDevice(object): bits = reversed(bits) self.tx_buf.extend(bits) + +class MockFactory(LocalPiFactory): + def __init__(self, revision='a21041', pin_class=MockPin): + super(MockFactory, self).__init__() + self._revision = revision + self.pin_class = pin_class + + def _get_address(self): + return ('mock',) + + def _get_revision(self): + return self._revision + + def reset(self): + self.pins.clear() + + def pin(self, spec, pin_class=None): + if pin_class is None: + pin_class = self.pin_class + n = self._to_gpio(spec) + try: + pin = self.pins[n] + except KeyError: + pin = pin_class(self, n) + self.pins[n] = pin + else: + # Ensure the pin class expected supports PWM (or not) + if issubclass(pin_class, MockPWMPin) != isinstance(pin, MockPWMPin): + raise ValueError('pin %d is already in use as a %s' % (n, pin.__class__.__name__)) + return pin + diff --git a/gpiozero/pins/native.py b/gpiozero/pins/native.py index 290ea9a..12bc5a9 100644 --- a/gpiozero/pins/native.py +++ b/gpiozero/pins/native.py @@ -13,20 +13,18 @@ import mmap import errno import struct import warnings +import weakref from time import sleep from threading import Thread, Event, Lock from collections import Counter -from . import LocalPin, PINS_CLEANUP -from .data import pi_info +from .local import LocalPiPin, LocalPiFactory from ..exc import ( PinInvalidPull, PinInvalidEdges, PinInvalidFunction, PinFixedPull, PinSetInput, - PinNonPhysical, - PinNoPins, ) @@ -149,7 +147,7 @@ class GPIOFS(object): f.write(str(pin).encode('ascii')) -class NativePin(LocalPin): +class NativeFactory(LocalPiFactory): """ Uses a built-in pure Python implementation to interface to the Pi's GPIO pins. This is the default pin implementation if no third-party libraries @@ -169,10 +167,17 @@ class NativePin(LocalPin): led = LED(NativePin(12)) """ + def __init__(self): + super(NativeFactory, self).__init__() + self.mem = GPIOMemory() + self.pin_class = NativePin - _MEM = None - _PINS = {} + def close(self): + super(NativeFactory, self).close() + self.mem.close() + +class NativePin(LocalPiPin): GPIO_FUNCTIONS = { 'input': 0b000, 'output': 0b001, @@ -202,89 +207,62 @@ class NativePin(LocalPin): GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()} GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()} - PI_INFO = None - - def __new__(cls, number): - if not cls._PINS: - cls._MEM = GPIOMemory() - PINS_CLEANUP.append(cls._MEM.close) - if cls.PI_INFO is None: - cls.PI_INFO = pi_info() - if not (0 <= number < 54): - raise ValueError('invalid pin %d specified (must be 0..53)' % number) - try: - return cls._PINS[number] - except KeyError: - self = super(NativePin, cls).__new__(cls) - try: - cls.PI_INFO.physical_pin('GPIO%d' % number) - except PinNoPins: - warnings.warn( - PinNonPhysical( - 'no physical pins exist for GPIO%d' % number)) - self._number = number - self._func_offset = self._MEM.GPFSEL_OFFSET + (number // 10) - self._func_shift = (number % 10) * 3 - self._set_offset = self._MEM.GPSET_OFFSET + (number // 32) - self._set_shift = number % 32 - self._clear_offset = self._MEM.GPCLR_OFFSET + (number // 32) - self._clear_shift = number % 32 - self._level_offset = self._MEM.GPLEV_OFFSET + (number // 32) - self._level_shift = number % 32 - self._pull_offset = self._MEM.GPPUDCLK_OFFSET + (number // 32) - self._pull_shift = number % 32 - self._edge_offset = self._MEM.GPEDS_OFFSET + (number // 32) - self._edge_shift = number % 32 - self._rising_offset = self._MEM.GPREN_OFFSET + (number // 32) - self._rising_shift = number % 32 - self._falling_offset = self._MEM.GPFEN_OFFSET + (number // 32) - self._falling_shift = number % 32 - self._when_changed = None - self._change_thread = None - self._change_event = Event() - self.function = 'input' - self.pull = 'up' if cls.PI_INFO.pulled_up('GPIO%d' % number) else 'floating' - self.bounce = None - self.edges = 'both' - cls._PINS[number] = self - return self - - def __repr__(self): - return "GPIO%d" % self._number - - @property - def number(self): - return self._number + def __init__(self, factory, number): + super(NativePin, self).__init__(factory, number) + self._func_offset = self.factory.mem.GPFSEL_OFFSET + (number // 10) + self._func_shift = (number % 10) * 3 + self._set_offset = self.factory.mem.GPSET_OFFSET + (number // 32) + self._set_shift = number % 32 + self._clear_offset = self.factory.mem.GPCLR_OFFSET + (number // 32) + self._clear_shift = number % 32 + self._level_offset = self.factory.mem.GPLEV_OFFSET + (number // 32) + self._level_shift = number % 32 + self._pull_offset = self.factory.mem.GPPUDCLK_OFFSET + (number // 32) + self._pull_shift = number % 32 + self._edge_offset = self.factory.mem.GPEDS_OFFSET + (number // 32) + self._edge_shift = number % 32 + self._rising_offset = self.factory.mem.GPREN_OFFSET + (number // 32) + self._rising_shift = number % 32 + self._falling_offset = self.factory.mem.GPFEN_OFFSET + (number // 32) + self._falling_shift = number % 32 + self._when_changed = None + self._change_thread = None + self._change_event = Event() + self.function = 'input' + self.pull = 'up' if factory.pi_info.pulled_up('GPIO%d' % number) else 'floating' + self.bounce = None + self.edges = 'both' def close(self): + self.frequency = None self.when_changed = None self.function = 'input' - self.pull = 'up' if self.PI_INFO.pulled_up('GPIO%d' % self.number) else 'floating' + self.pull = 'up' if self.factory.pi_info.pulled_up('GPIO%d' % self.number) else 'floating' def _get_function(self): - return self.GPIO_FUNCTION_NAMES[(self._MEM[self._func_offset] >> self._func_shift) & 7] + return self.GPIO_FUNCTION_NAMES[(self.factory.mem[self._func_offset] >> self._func_shift) & 7] def _set_function(self, value): try: value = self.GPIO_FUNCTIONS[value] except KeyError: raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self)) - self._MEM[self._func_offset] = ( - self._MEM[self._func_offset] + self.factory.mem[self._func_offset] = ( + self.factory.mem[self._func_offset] & ~(7 << self._func_shift) | (value << self._func_shift) ) def _get_state(self): - return bool(self._MEM[self._level_offset] & (1 << self._level_shift)) + return bool(self.factory.mem[self._level_offset] & (1 << self._level_shift)) def _set_state(self, value): if self.function == 'input': raise PinSetInput('cannot set state of pin %r' % self) if value: - self._MEM[self._set_offset] = 1 << self._set_shift + self.factory.mem[self._set_offset] = 1 << self._set_shift else: - self._MEM[self._clear_offset] = 1 << self._clear_shift + self.factory.mem[self._clear_offset] = 1 << self._clear_shift def _get_pull(self): return self.GPIO_PULL_UP_NAMES[self._pull] @@ -292,23 +270,23 @@ class NativePin(LocalPin): 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.PI_INFO.pulled_up('GPIO%d' % self.number): + if value != 'up' and self.factory.pi_info.pulled_up('GPIO%d' % self.number): raise PinFixedPull('%r has a physical pull-up resistor' % self) try: value = self.GPIO_PULL_UPS[value] except KeyError: raise PinInvalidPull('invalid pull direction "%s" for pin %r' % (value, self)) - self._MEM[self._MEM.GPPUD_OFFSET] = value + self.factory.mem[self.factory.mem.GPPUD_OFFSET] = value sleep(0.000000214) - self._MEM[self._pull_offset] = 1 << self._pull_shift + self.factory.mem[self._pull_offset] = 1 << self._pull_shift sleep(0.000000214) - self._MEM[self._MEM.GPPUD_OFFSET] = 0 - self._MEM[self._pull_offset] = 0 + self.factory.mem[self.factory.mem.GPPUD_OFFSET] = 0 + self.factory.mem[self._pull_offset] = 0 self._pull = value def _get_edges(self): - rising = bool(self._MEM[self._rising_offset] & (1 << self._rising_shift)) - falling = bool(self._MEM[self._falling_offset] & (1 << self._falling_shift)) + rising = bool(self.factory.mem[self._rising_offset] & (1 << self._rising_shift)) + falling = bool(self.factory.mem[self._falling_offset] & (1 << self._falling_shift)) return self.GPIO_EDGES_NAMES[(rising, falling)] def _set_edges(self, value): @@ -319,13 +297,13 @@ class NativePin(LocalPin): f = self.when_changed self.when_changed = None try: - self._MEM[self._rising_offset] = ( - self._MEM[self._rising_offset] + self.factory.mem[self._rising_offset] = ( + self.factory.mem[self._rising_offset] & ~(1 << self._rising_shift) | (rising << self._rising_shift) ) - self._MEM[self._falling_offset] = ( - self._MEM[self._falling_offset] + self.factory.mem[self._falling_offset] = ( + self.factory.mem[self._falling_offset] & ~(1 << self._falling_shift) | (falling << self._falling_shift) ) @@ -353,9 +331,9 @@ class NativePin(LocalPin): def _change_watch(self): offset = self._edge_offset mask = 1 << self._edge_shift - self._MEM[offset] = mask # clear any existing detection bit + self.factory.mem[offset] = mask # clear any existing detection bit while not self._change_event.wait(0.001): - if self._MEM[offset] & mask: - self._MEM[offset] = mask + if self.factory.mem[offset] & mask: + self.factory.mem[offset] = mask self._when_changed() diff --git a/gpiozero/pins/pi.py b/gpiozero/pins/pi.py new file mode 100644 index 0000000..622ba79 --- /dev/null +++ b/gpiozero/pins/pi.py @@ -0,0 +1,214 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + +import io +import weakref +import warnings + +try: + from spidev import SpiDev +except ImportError: + SpiDev = None + +from . import Factory, Pin +from .data import pi_info +from ..exc import ( + PinNoPins, + PinNonPhysical, + PinInvalidPin, + SPIBadArgs, + SPISoftwareFallback, + ) + + +class PiFactory(Factory): + """ + Abstract base class representing hardware attached to a Raspberry Pi. This + forms the base of :class:`LocalPiFactory`. + """ + def __init__(self): + self._info = None + self.pins = {} + self.pin_class = None + self.spi_hardware_class = None + self.spi_software_class = None + self.shared_spi_hardware_class = None + self.shared_spi_software_class = None + + def close(self): + for pin in self.pins.values(): + pin.close() + self.pins.clear() + + def pin(self, spec): + n = self._to_gpio(spec) + try: + pin = self.pins[n] + except KeyError: + pin = self.pin_class(self, n) + 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. + """ + if not 0 <= spec < 54: + raise PinInvalidPin('invalid GPIO port %d specified (range 0..53) ' % spec) + return spec + + def _get_revision(self): + raise NotImplementedError + + def _get_pi_info(self): + if self._info is None: + self._info = pi_info(self._get_revision()) + return self._info + + def spi(self, **spi_args): + """ + Returns an SPI interface, for the specified SPI *port* and *device*, or + for the specified pins (*clock_pin*, *mosi_pin*, *miso_pin*, and + *select_pin*). Only one of the schemes can be used; attempting to mix + *port* and *device* with pin numbers will raise :exc:`SPIBadArgs`. + + If the pins specified match the hardware SPI pins (clock on GPIO11, + MOSI on GPIO10, MISO on GPIO9, and chip select on GPIO8 or GPIO7), and + the spidev module can be imported, a :class:`SPIHardwareInterface` + instance will be returned. Otherwise, a :class:`SPISoftwareInterface` + will be returned which will use simple bit-banging to communicate. + + Both interfaces have the same API, support clock polarity and phase + attributes, and can handle half and full duplex communications, but the + hardware interface is significantly faster (though for many things this + doesn't matter). + """ + spi_args, kwargs = self._extract_spi_args(**spi_args) + shared = kwargs.pop('shared', False) + if kwargs: + raise SPIBadArgs( + 'unrecognized keyword argument %s' % kwargs.popitem()[0]) + if all(( + spi_args['clock_pin'] == 11, + spi_args['mosi_pin'] == 10, + spi_args['miso_pin'] == 9, + spi_args['select_pin'] in (7, 8), + )): + try: + hardware_spi_args = { + 'port': 0, + 'device': {8: 0, 7: 1}[spi_args['select_pin']], + } + if shared: + return self.shared_spi_hardware_class(self, **hardware_spi_args) + else: + return self.spi_hardware_class(self, **hardware_spi_args) + except Exception as e: + warnings.warn( + SPISoftwareFallback( + 'failed to initialize hardware SPI, falling back to ' + 'software (error was: %s)' % str(e))) + # Convert all pin arguments to integer GPIO numbers. This is necessary + # to ensure the shared-key for shared implementations get matched + # correctly, and is a bit of a hack for the pigpio bit-bang + # implementation which just wants the pin numbers too. + spi_args = { + key: pin.number if isinstance(pin, Pin) else pin + for key, pin in spi_args.items() + } + if shared: + return self.shared_spi_software_class(self, **spi_args) + else: + return self.spi_software_class(self, **spi_args) + + def _extract_spi_args(self, **kwargs): + """ + Given a set of keyword arguments, splits it into those relevant to SPI + implementations and all the rest. SPI arguments are augmented with + defaults and converted into the pin format (from the port/device + format) if necessary. + + Returns a tuple of ``(spi_args, other_args)``. + """ + pin_defaults = { + 'clock_pin': 11, + 'mosi_pin': 10, + 'miso_pin': 9, + 'select_pin': 8, + } + dev_defaults = { + 'port': 0, + 'device': 0, + } + spi_args = { + key: value for (key, value) in kwargs.items() + if key in pin_defaults or key in dev_defaults + } + kwargs = { + key: value for (key, value) in kwargs.items() + if key not in spi_args + } + if not spi_args: + spi_args = pin_defaults + elif set(spi_args) <= set(pin_defaults): + spi_args = { + key: self._to_gpio(spi_args.get(key, default)) + for key, default in pin_defaults.items() + } + elif set(spi_args) <= set(dev_defaults): + spi_args = { + key: spi_args.get(key, default) + for key, default in dev_defaults.items() + } + if spi_args['port'] != 0: + raise SPIBadArgs('port 0 is the only valid SPI port') + if spi_args['device'] not in (0, 1): + raise SPIBadArgs('device must be 0 or 1') + spi_args = { + key: value if key != 'select_pin' else (8, 7)[spi_args['device']] + for key, value in pin_defaults.items() + } + else: + raise SPIBadArgs( + 'you must either specify port and device, or clock_pin, ' + 'mosi_pin, miso_pin, and select_pin; combinations of the two ' + 'schemes (e.g. port and clock_pin) are not permitted') + return spi_args, kwargs + + +class PiPin(Pin): + """ + Abstract base class representing a multi-function GPIO pin attached to a + Raspberry Pi. + """ + def __init__(self, factory, number): + super(PiPin, self).__init__() + try: + factory.pi_info.physical_pin('GPIO%d' % number) + except PinNoPins: + warnings.warn( + PinNonPhysical( + 'no physical pins exist for GPIO%d' % number)) + self._factory = weakref.proxy(factory) + self._number = number + + @property + def number(self): + return self._number + + @property + def factory(self): + return self._factory + + def _get_address(self): + return self.factory.address + ('GPIO%d' % self.number,) + diff --git a/gpiozero/pins/pigpiod.py b/gpiozero/pins/pigpiod.py index 537eb48..d36b059 100644 --- a/gpiozero/pins/pigpiod.py +++ b/gpiozero/pins/pigpiod.py @@ -6,12 +6,15 @@ from __future__ import ( ) str = type('') -import warnings +import weakref import pigpio import os -from . import Pin +from . import SPI +from .pi import PiPin, PiFactory from .data import pi_info +from ..devices import Device +from ..mixins import SharedMixin from ..exc import ( PinInvalidFunction, PinSetInput, @@ -19,12 +22,12 @@ from ..exc import ( PinInvalidPull, PinInvalidBounce, PinInvalidState, - PinNonPhysical, - PinNoPins, + SPIBadArgs, + SPIInvalidClockMode, ) -class PiGPIOPin(Pin): +class PiGPIOFactory(PiFactory): """ Uses the `pigpio`_ library to interface to the Pi's GPIO pins. The pigpio library relies on a daemon (``pigpiod``) to be running as root to provide @@ -68,10 +71,65 @@ class PiGPIOPin(Pin): .. _pigpio: http://abyz.co.uk/rpi/pigpio/ """ + def __init__( + self, host=os.getenv('PIGPIO_ADDR', 'localhost'), + port=int(os.getenv('PIGPIO_PORT', 8888))): + super(PiGPIOFactory, self).__init__() + self.pin_class = PiGPIOPin + self.spi_hardware_class = PiGPIOHardwareSPI + self.spi_software_class = PiGPIOSoftwareSPI + self.shared_spi_hardware_class = PiGPIOHardwareSPIShared + self.shared_spi_software_class = PiGPIOSoftwareSPIShared + self._connection = pigpio.pi(host, port) + self._host = host + self._port = port + self._spis = [] + def close(self): + super(PiGPIOFactory, self).close() + # We *have* to keep track of SPI interfaces constructed with pigpio; + # if we fail to close them they prevent future interfaces from using + # the same pins + if self.connection: + while self._spis: + self._spis[0].close() + self.connection.stop() + self._connection = None + + @property + def connection(self): + # If we're shutting down, the connection may have disconnected itself + # already. Unfortunately, the connection's "connected" property is + # rather buggy - disconnecting doesn't set it to False! So we're + # naughty and check an internal variable instead... + try: + if self._connection.sl.s is not None: + return self._connection + except AttributeError: + pass + + @property + def host(self): + return self._host + + @property + def port(self): + return self._port + + 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) + return intf + + +class PiGPIOPin(PiPin): _CONNECTIONS = {} # maps (host, port) to (connection, pi_info) - _PINS = {} - GPIO_FUNCTIONS = { 'input': pigpio.INPUT, 'output': pigpio.OUTPUT, @@ -99,101 +157,64 @@ class PiGPIOPin(Pin): GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()} GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()} - def __new__( - cls, number, host=os.getenv('PIGPIO_ADDR', 'localhost'), - port=int(os.getenv('PIGPIO_PORT', 8888))): + def __init__(self, factory, number): + super(PiGPIOPin, self).__init__(factory, number) + self._pull = 'up' if factory.pi_info.pulled_up('GPIO%d' % number) else 'floating' + self._pwm = False + self._bounce = None + self._when_changed = None + self._callback = None + self._edges = pigpio.EITHER_EDGE try: - return cls._PINS[(host, port, number)] - except KeyError: - self = super(PiGPIOPin, cls).__new__(cls) - cls.pi_info(host, port) # implicitly creates connection - self._connection, self._pi_info = cls._CONNECTIONS[(host, port)] - try: - self._pi_info.physical_pin('GPIO%d' % number) - except PinNoPins: - warnings.warn( - PinNonPhysical( - 'no physical pins exist for GPIO%d' % number)) - self._host = host - self._port = port - self._number = number - self._pull = 'up' if self._pi_info.pulled_up('GPIO%d' % number) else 'floating' - self._pwm = False - self._bounce = None - self._when_changed = None - self._callback = None - self._edges = pigpio.EITHER_EDGE - try: - self._connection.set_mode(self._number, pigpio.INPUT) - except pigpio.error as e: - raise ValueError(e) - self._connection.set_pull_up_down(self._number, self.GPIO_PULL_UPS[self._pull]) - self._connection.set_glitch_filter(self._number, 0) - cls._PINS[(host, port, number)] = self - return self - - def __repr__(self): - if self._host == 'localhost': - return "GPIO%d" % self._number - else: - return "GPIO%d on %s:%d" % (self._number, self._host, self._port) - - @property - def host(self): - return self._host - - @property - def port(self): - return self._port - - @property - def number(self): - return self._number + self.factory.connection.set_mode(self.number, pigpio.INPUT) + except pigpio.error as e: + raise ValueError(e) + self.factory.connection.set_pull_up_down(self.number, self.GPIO_PULL_UPS[self._pull]) + self.factory.connection.set_glitch_filter(self.number, 0) def close(self): - # If we're shutting down, the connection may have disconnected itself - # already. Unfortunately, the connection's "connected" property is - # rather buggy - disconnecting doesn't set it to False! So we're - # naughty and check an internal variable instead... - if self._connection.sl.s is not None: + if self.factory.connection: self.frequency = None self.when_changed = None self.function = 'input' - self.pull = 'up' if self._pi_info.pulled_up('GPIO%d' % self.number) else 'floating' + self.pull = 'up' if self.factory.pi_info.pulled_up('GPIO%d' % self.number) else 'floating' + + def _get_address(self): + return self.factory.address + ('GPIO%d' % self.number,) def _get_function(self): - return self.GPIO_FUNCTION_NAMES[self._connection.get_mode(self._number)] + return self.GPIO_FUNCTION_NAMES[self.factory.connection.get_mode(self.number)] def _set_function(self, value): if value != 'input': self._pull = 'floating' try: - self._connection.set_mode(self._number, self.GPIO_FUNCTIONS[value]) + self.factory.connection.set_mode(self.number, self.GPIO_FUNCTIONS[value]) except KeyError: raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self)) def _get_state(self): if self._pwm: return ( - self._connection.get_PWM_dutycycle(self._number) / - self._connection.get_PWM_range(self._number) + self.factory.connection.get_PWM_dutycycle(self.number) / + self.factory.connection.get_PWM_range(self.number) ) else: - return bool(self._connection.read(self._number)) + return bool(self.factory.connection.read(self.number)) def _set_state(self, value): if self._pwm: try: - value = int(value * self._connection.get_PWM_range(self._number)) - if value != self._connection.get_PWM_dutycycle(self._number): - self._connection.set_PWM_dutycycle(self._number, value) + value = int(value * self.factory.connection.get_PWM_range(self.number)) + if value != self.factory.connection.get_PWM_dutycycle(self.number): + self.factory.connection.set_PWM_dutycycle(self.number, value) except pigpio.error: raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) elif self.function == 'input': raise PinSetInput('cannot set state of pin %r' % self) else: # write forces pin to OUTPUT, hence the check above - self._connection.write(self._number, bool(value)) + self.factory.connection.write(self.number, bool(value)) def _get_pull(self): return self._pull @@ -201,31 +222,31 @@ class PiGPIOPin(Pin): 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._pi_info.pulled_up('GPIO%d' % self._number): + if value != 'up' and self.factory.pi_info.pulled_up('GPIO%d' % self.number): raise PinFixedPull('%r has a physical pull-up resistor' % self) try: - self._connection.set_pull_up_down(self._number, self.GPIO_PULL_UPS[value]) + self.factory.connection.set_pull_up_down(self.number, self.GPIO_PULL_UPS[value]) self._pull = value except KeyError: raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self)) def _get_frequency(self): if self._pwm: - return self._connection.get_PWM_frequency(self._number) + return self.factory.connection.get_PWM_frequency(self.number) return None def _set_frequency(self, value): if not self._pwm and value is not None: - self._connection.set_PWM_frequency(self._number, value) - self._connection.set_PWM_range(self._number, 10000) - self._connection.set_PWM_dutycycle(self._number, 0) + self.factory.connection.set_PWM_frequency(self.number, value) + self.factory.connection.set_PWM_range(self.number, 10000) + self.factory.connection.set_PWM_dutycycle(self.number, 0) self._pwm = True elif self._pwm and value is not None: - if value != self._connection.get_PWM_frequency(self._number): - self._connection.set_PWM_frequency(self._number, value) - self._connection.set_PWM_range(self._number, 10000) + if value != self.factory.connection.get_PWM_frequency(self.number): + self.factory.connection.set_PWM_frequency(self.number, value) + self.factory.connection.set_PWM_range(self.number, 10000) elif self._pwm and value is None: - self._connection.write(self._number, 0) + self.factory.connection.write(self.number, 0) self._pwm = False def _get_bounce(self): @@ -236,7 +257,7 @@ class PiGPIOPin(Pin): value = 0 elif value < 0: raise PinInvalidBounce('bounce must be 0 or greater') - self._connection.set_glitch_filter(self._number, int(value * 1000000)) + self.factory.connection.set_glitch_filter(self.number, int(value * 1000000)) def _get_edges(self): return self.GPIO_EDGES_NAMES[self._edges] @@ -259,20 +280,224 @@ class PiGPIOPin(Pin): self._callback.cancel() self._callback = None if value is not None: - self._callback = self._connection.callback( - self._number, self._edges, + self._callback = self.factory.connection.callback( + self.number, self._edges, lambda gpio, level, tick: value()) - @classmethod - def pi_info( - cls, host=os.getenv('PIGPIO_ADDR', 'localhost'), - port=int(os.getenv('PIGPIO_PORT', 8888))): - try: - connection, info = cls._CONNECTIONS[(host, port)] - except KeyError: - connection = pigpio.pi(host, port) - revision = '%04x' % connection.get_hardware_revision() - info = pi_info(revision) - cls._CONNECTIONS[(host, port)] = (connection, info) - return info + +class PiGPIOHardwareSPI(SPI, Device): + def __init__(self, factory, port, device): + self._port = port + self._device = device + self._factory = weakref.proxy(factory) + super(PiGPIOHardwareSPI, self).__init__() + self._reserve_pins(*( + factory.address + ('GPIO%d' % pin,) + for pin in (11, 10, 9, (8, 7)[device]) + )) + self._mode = 0 + self._select_high = False + self._bits_per_word = 8 + self._baud = 500000 + self._handle = self._factory.connection.spi_open( + device, self._baud, self._spi_flags()) + + def close(self): + try: + self._factory._spis.remove(self) + except (ReferenceError, ValueError): + # If the factory has died already or we're not present in its + # internal list, ignore the error + pass + if not self.closed: + self._factory.connection.spi_close(self._handle) + self._handle = None + self._release_all() + super(PiGPIOHardwareSPI, self).close() + + @property + def closed(self): + return self._handle is None or self._factory.connection is None + + @property + def factory(self): + return self._factory + + def __repr__(self): + try: + self._check_open() + return 'SPI(port=%d, device=%d)' % (self._port, self._device) + except DeviceClosed: + return 'SPI(closed)' + + def _spi_flags(self): + return ( + self._mode << 0 | + self._select_high << (2 + self._device) | + self._bits_per_word << 16 + ) + + def _get_clock_mode(self): + return self._clock_mode + + def _set_clock_mode(self, value): + self._check_open() + if not 0 <= value < 4: + raise SPIInvalidClockmode("%d is not a valid SPI clock mode" % value) + self._factory.connection.spi_close(self._handle) + self._clock_mode = value + self._handle = self._factory.connection.spi_open( + self._device, self._baud, self._spi_flags()) + + def _get_select_high(self): + return self._select_high + + def _set_select_high(self, value): + self._check_open() + self._factory.connection.spi_close(self._handle) + self._select_high = bool(value) + self._handle = self._factory.connection.spi_open( + self._device, self._baud, self._spi_flags()) + + def _get_bits_per_word(self): + return self._bits_per_word + + def _set_bits_per_word(self, value): + self._check_open() + self._factory.connection.spi_close(self._handle) + self._bits_per_word = value + self._handle = self._factory.connection.spi_open( + self._device, self._baud, self._spi_flags()) + + def transfer(self, data): + self._check_open() + count, data = self._factory.connection.spi_xfer(self._handle, data) + if count < 0: + raise IOError('SPI transfer error %d' % count) + # Convert returned bytearray to list of ints. XXX Not sure how non-byte + # sized words (aux intf only) are returned ... padded to 16/32-bits? + return [int(b) for b in data] + + +class PiGPIOSoftwareSPI(SPI, Device): + def __init__(self, factory, clock_pin, mosi_pin, miso_pin, select_pin): + self._select_pin = None + self._factory = weakref.proxy(factory) + self._address = factory.address + ( + ) + 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._mode = 0 + self._select_high = False + self._lsb_first = False + self._baud = 100000 + try: + self._factory.connection.bb_spi_open( + select_pin, miso_pin, mosi_pin, clock_pin, + self._baud, self._spi_flags()) + # Only set after opening bb_spi; if that fails then close() will + # also fail if bb_spi_close is attempted on an un-open interface + self._select_pin = select_pin + self._clock_pin = clock_pin + self._mosi_pin = mosi_pin + self._miso_pin = miso_pin + except: + self.close() + raise + + def close(self): + try: + self._factory._spis.remove(self) + except (ReferenceError, ValueError): + # If the factory has died already or we're not present in its + # internal list, ignore the error + pass + if not self.closed: + self._factory.connection.bb_spi_close(self._select_pin) + self._select_pin = None + self._release_all() + super(PiGPIOSoftwareSPI, self).close() + + @property + def closed(self): + return self._select_pin is None or self._factory.connection is None + + def __repr__(self): + try: + self._check_open() + return ( + 'SPI(clock_pin=%d, mosi_pin=%d, miso_pin=%d, select_pin=%d)' % ( + self._clock_pin, self._mosi_pin, self._miso_pin, self._select_pin + )) + except DeviceClosed: + return 'SPI(closed)' + + def _spi_flags(self): + return ( + self._mode << 0 | + self._select_high << 2 | + self._lsb_first << 14 | + self._lsb_first << 15 + ) + + def _get_clock_mode(self): + return self._clock_mode + + def _set_clock_mode(self, value): + self._check_open() + if not 0 <= value < 4: + raise SPIInvalidClockmode("%d is not a valid SPI clock mode" % value) + self._factory.connection.bb_spi_close(self._select_pin) + self._clock_mode = value + self._factory.connection.bb_spi_open( + self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin, + self._baud, self._spi_flags()) + + def _get_select_high(self): + return self._select_high + + def _set_select_high(self, value): + self._check_open() + self._factory.connection.bb_spi_close(self._select_pin) + self._select_high = bool(value) + self._factory.connection.bb_spi_open( + self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin, + self._baud, self._spi_flags()) + + def _get_lsb_first(self): + return self._lsb_first + + def _set_lsb_first(self, value): + self._check_open() + self._factory.connection.bb_spi_close(self._select_pin) + self._lsb_first = bool(value) + self._factory.connection.bb_spi_open( + self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin, + self._baud, self._spi_flags()) + + def transfer(self, data): + self._check_open() + count, data = self._factory.connection.bb_spi_xfer(self._select_pin, data) + if count < 0: + raise IOError('SPI transfer error %d' % count) + # Convert returned bytearray to list of ints. bb_spi only supports + # byte-sized words so no issues here + return [int(b) for b in data] + + +class PiGPIOHardwareSPIShared(SharedMixin, PiGPIOHardwareSPI): + @classmethod + def _shared_key(cls, factory, port, device): + return (factory, port, device) + + +class PiGPIOSoftwareSPIShared(SharedMixin, PiGPIOSoftwareSPI): + @classmethod + def _shared_key(cls, factory, clock_pin, mosi_pin, miso_pin, select_pin): + return (factory, select_pin) diff --git a/gpiozero/pins/rpigpio.py b/gpiozero/pins/rpigpio.py index 1597fb0..679b8c2 100644 --- a/gpiozero/pins/rpigpio.py +++ b/gpiozero/pins/rpigpio.py @@ -7,10 +7,10 @@ from __future__ import ( str = type('') import warnings +import weakref from RPi import GPIO -from . import LocalPin -from .data import pi_info +from .local import LocalPiFactory, LocalPiPin from ..exc import ( PinInvalidFunction, PinSetInput, @@ -19,12 +19,10 @@ from ..exc import ( PinInvalidState, PinInvalidBounce, PinPWMFixedValue, - PinNonPhysical, - PinNoPins, ) -class RPiGPIOPin(LocalPin): +class RPiGPIOFactory(LocalPiFactory): """ Uses the `RPi.GPIO`_ library to interface to the Pi's GPIO pins. This is the default pin implementation if the RPi.GPIO library is installed. @@ -39,7 +37,7 @@ class RPiGPIOPin(LocalPin): However, you can also construct RPi.GPIO pins manually if you wish:: - from gpiozero.pins.rpigpio import RPiGPIOPin + from gpiozero.pins.rpigpio import RPiGPIOFactory from gpiozero import LED led = LED(RPiGPIOPin(12)) @@ -47,8 +45,18 @@ class RPiGPIOPin(LocalPin): .. _RPi.GPIO: https://pypi.python.org/pypi/RPi.GPIO """ - _PINS = {} + def __init__(self): + super(RPiGPIOFactory, self).__init__() + GPIO.setmode(GPIO.BCM) + GPIO.setwarnings(False) + self.pin_class = RPiGPIOPin + def close(self): + super(RPiGPIOFactory, self).close() + GPIO.cleanup() + + +class RPiGPIOPin(LocalPiPin): GPIO_FUNCTIONS = { 'input': GPIO.IN, 'output': GPIO.OUT, @@ -75,69 +83,43 @@ class RPiGPIOPin(LocalPin): GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()} GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()} - PI_INFO = None - - def __new__(cls, number): - if not cls._PINS: - GPIO.setmode(GPIO.BCM) - GPIO.setwarnings(False) - if cls.PI_INFO is None: - cls.PI_INFO = pi_info() - try: - return cls._PINS[number] - except KeyError: - self = super(RPiGPIOPin, cls).__new__(cls) - try: - cls.PI_INFO.physical_pin('GPIO%d' % number) - except PinNoPins: - warnings.warn( - PinNonPhysical( - 'no physical pins exist for GPIO%d' % number)) - self._number = number - self._pull = 'up' if cls.PI_INFO.pulled_up('GPIO%d' % number) else 'floating' - self._pwm = None - self._frequency = None - self._duty_cycle = None - self._bounce = -666 - self._when_changed = None - self._edges = GPIO.BOTH - GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[self._pull]) - cls._PINS[number] = self - return self - - def __repr__(self): - return "GPIO%d" % self._number - - @property - def number(self): - return self._number + def __init__(self, factory, number): + super(RPiGPIOPin, self).__init__(factory, number) + self._pull = 'up' if factory.pi_info.pulled_up('GPIO%d' % number) else 'floating' + self._pwm = None + self._frequency = None + self._duty_cycle = None + self._bounce = -666 + self._when_changed = None + self._edges = GPIO.BOTH + GPIO.setup(self.number, GPIO.IN, self.GPIO_PULL_UPS[self._pull]) def close(self): self.frequency = None self.when_changed = None - GPIO.cleanup(self._number) + GPIO.cleanup(self.number) def output_with_state(self, state): self._pull = 'floating' - GPIO.setup(self._number, GPIO.OUT, initial=state) + GPIO.setup(self.number, GPIO.OUT, initial=state) def input_with_pull(self, pull): - if pull != 'up' and self.PI_INFO.pulled_up('GPIO%d' % self._number): + if pull != 'up' and self.factory.pi_info.pulled_up('GPIO%d' % self.number): raise PinFixedPull('%r has a physical pull-up resistor' % self) try: - GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[pull]) + GPIO.setup(self.number, GPIO.IN, self.GPIO_PULL_UPS[pull]) self._pull = pull except KeyError: raise PinInvalidPull('invalid pull "%s" for pin %r' % (pull, self)) def _get_function(self): - return self.GPIO_FUNCTION_NAMES[GPIO.gpio_function(self._number)] + return self.GPIO_FUNCTION_NAMES[GPIO.gpio_function(self.number)] def _set_function(self, value): if value != 'input': self._pull = 'floating' if value in ('input', 'output') and value in self.GPIO_FUNCTIONS: - GPIO.setup(self._number, self.GPIO_FUNCTIONS[value], self.GPIO_PULL_UPS[self._pull]) + GPIO.setup(self.number, self.GPIO_FUNCTIONS[value], self.GPIO_PULL_UPS[self._pull]) else: raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self)) @@ -145,7 +127,7 @@ class RPiGPIOPin(LocalPin): if self._pwm: return self._duty_cycle else: - return GPIO.input(self._number) + return GPIO.input(self.number) def _set_state(self, value): if self._pwm: @@ -156,7 +138,7 @@ class RPiGPIOPin(LocalPin): self._duty_cycle = value else: try: - GPIO.output(self._number, value) + GPIO.output(self.number, value) except ValueError: raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) except RuntimeError: @@ -168,10 +150,10 @@ class RPiGPIOPin(LocalPin): 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.PI_INFO.pulled_up('GPIO%d' % self._number): + if value != 'up' and self.factory.pi_info.pulled_up('GPIO%d' % self.number): raise PinFixedPull('%r has a physical pull-up resistor' % self) try: - GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[value]) + GPIO.setup(self.number, GPIO.IN, self.GPIO_PULL_UPS[value]) self._pull = value except KeyError: raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self)) @@ -182,7 +164,7 @@ class RPiGPIOPin(LocalPin): def _set_frequency(self, value): if self._frequency is None and value is not None: try: - self._pwm = GPIO.PWM(self._number, value) + self._pwm = GPIO.PWM(self.number, value) except RuntimeError: raise PinPWMFixedValue('cannot start PWM on pin %r' % self) self._pwm.start(0) @@ -228,11 +210,11 @@ class RPiGPIOPin(LocalPin): if self._when_changed is None and value is not None: self._when_changed = value GPIO.add_event_detect( - self._number, self._edges, + self.number, self._edges, callback=lambda channel: self._when_changed(), bouncetime=self._bounce) elif self._when_changed is not None and value is None: - GPIO.remove_event_detect(self._number) + GPIO.remove_event_detect(self.number) self._when_changed = None else: self._when_changed = value diff --git a/gpiozero/pins/rpio.py b/gpiozero/pins/rpio.py index 58d5893..2413b57 100644 --- a/gpiozero/pins/rpio.py +++ b/gpiozero/pins/rpio.py @@ -8,11 +8,12 @@ str = type('') import warnings +import weakref import RPIO import RPIO.PWM from RPIO.Exceptions import InvalidChannelException -from . import LocalPin, PINS_CLEANUP +from .local import LocalPiPin, LocalPiFactory from .data import pi_info from ..exc import ( PinInvalidFunction, @@ -22,12 +23,10 @@ from ..exc import ( PinInvalidBounce, PinInvalidState, PinPWMError, - PinNonPhysical, - PinNoPins, ) -class RPIOPin(LocalPin): +class RPIOFactory(LocalPiFactory): """ Uses the `RPIO`_ library to interface to the Pi's GPIO pins. This is the default pin implementation if the RPi.GPIO library is not installed, @@ -48,9 +47,22 @@ class RPIOPin(LocalPin): .. _RPIO: https://pythonhosted.org/RPIO/ """ + def __init__(self): + super(RPIOFactory, self).__init__() + RPIO.setmode(RPIO.BCM) + RPIO.setwarnings(False) + RPIO.wait_for_interrupts(threaded=True) + RPIO.PWM.setup() + RPIO.PWM.init_channel(0, 10000) + self.pin_class = RPIOPin - _PINS = {} + def close(self): + RPIO.PWM.cleanup() + RPIO.stop_waiting_for_interrupts() + RPIO.cleanup() + +class RPIOPin(LocalPiPin): GPIO_FUNCTIONS = { 'input': RPIO.IN, 'output': RPIO.OUT, @@ -66,64 +78,32 @@ class RPIOPin(LocalPin): GPIO_FUNCTION_NAMES = {v: k for (k, v) in GPIO_FUNCTIONS.items()} GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()} - PI_INFO = None - - def __new__(cls, number): - if not cls._PINS: - RPIO.setmode(RPIO.BCM) - RPIO.setwarnings(False) - RPIO.wait_for_interrupts(threaded=True) - RPIO.PWM.setup() - RPIO.PWM.init_channel(0, 10000) - PINS_CLEANUP.append(RPIO.PWM.cleanup) - PINS_CLEANUP.append(RPIO.stop_waiting_for_interrupts) - PINS_CLEANUP.append(RPIO.cleanup) - if cls.PI_INFO is None: - cls.PI_INFO = pi_info() + def __init__(self, factory, number): + super(RPIOPin, self).__init__(factory, number) + self._pull = 'up' if factory.pi_info.pulled_up('GPIO%d' % number) else 'floating' + self._pwm = False + self._duty_cycle = None + self._bounce = None + self._when_changed = None + self._edges = 'both' try: - return cls._PINS[number] - except KeyError: - self = super(RPIOPin, cls).__new__(cls) - try: - cls.PI_INFO.physical_pin('GPIO%d' % number) - except PinNoPins: - warnings.warn( - PinNonPhysical( - 'no physical pins exist for GPIO%d' % number)) - self._number = number - self._pull = 'up' if cls.PI_INFO.pulled_up('GPIO%d' % number) else 'floating' - self._pwm = False - self._duty_cycle = None - self._bounce = None - self._when_changed = None - self._edges = 'both' - try: - RPIO.setup(self._number, RPIO.IN, self.GPIO_PULL_UPS[self._pull]) - except InvalidChannelException as e: - raise ValueError(e) - cls._PINS[number] = self - return self - - def __repr__(self): - return "GPIO%d" % self._number - - @property - def number(self): - return self._number + RPIO.setup(self.number, RPIO.IN, self.GPIO_PULL_UPS[self._pull]) + except InvalidChannelException as e: + raise ValueError(e) def close(self): self.frequency = None self.when_changed = None - RPIO.setup(self._number, RPIO.IN, RPIO.PUD_OFF) + RPIO.setup(self.number, RPIO.IN, RPIO.PUD_OFF) def _get_function(self): - return self.GPIO_FUNCTION_NAMES[RPIO.gpio_function(self._number)] + return self.GPIO_FUNCTION_NAMES[RPIO.gpio_function(self.number)] def _set_function(self, value): if value != 'input': self._pull = 'floating' try: - RPIO.setup(self._number, self.GPIO_FUNCTIONS[value], self.GPIO_PULL_UPS[self._pull]) + RPIO.setup(self.number, self.GPIO_FUNCTIONS[value], self.GPIO_PULL_UPS[self._pull]) except KeyError: raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self)) @@ -131,23 +111,23 @@ class RPIOPin(LocalPin): if self._pwm: return self._duty_cycle else: - return RPIO.input(self._number) + return RPIO.input(self.number) def _set_state(self, value): if not 0 <= value <= 1: raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) if self._pwm: - RPIO.PWM.clear_channel_gpio(0, self._number) + RPIO.PWM.clear_channel_gpio(0, self.number) if value == 0: - RPIO.output(self._number, False) + RPIO.output(self.number, False) elif value == 1: - RPIO.output(self._number, True) + RPIO.output(self.number, True) else: - RPIO.PWM.add_channel_pulse(0, self._number, start=0, width=int(1000 * value)) + RPIO.PWM.add_channel_pulse(0, self.number, start=0, width=int(1000 * value)) self._duty_cycle = value else: try: - RPIO.output(self._number, value) + RPIO.output(self.number, value) except ValueError: raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) except RuntimeError: @@ -159,10 +139,10 @@ class RPIOPin(LocalPin): 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.PI_INFO.pulled_up('GPIO%d' % self._number): + if value != 'up' and self.factory.pi_info.pulled_up('GPIO%d' % self.number): raise PinFixedPull('%r has a physical pull-up resistor' % self) try: - RPIO.setup(self._number, RPIO.IN, self.GPIO_PULL_UPS[value]) + RPIO.setup(self.number, RPIO.IN, self.GPIO_PULL_UPS[value]) self._pull = value except KeyError: raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self)) @@ -182,10 +162,10 @@ class RPIOPin(LocalPin): self._pwm = True # Dirty hack to get RPIO's PWM support to setup, but do nothing, # for a given GPIO pin - RPIO.PWM.add_channel_pulse(0, self._number, start=0, width=0) - RPIO.PWM.clear_channel_gpio(0, self._number) + RPIO.PWM.add_channel_pulse(0, self.number, start=0, width=0) + RPIO.PWM.clear_channel_gpio(0, self.number) elif self._pwm and value is None: - RPIO.PWM.clear_channel_gpio(0, self._number) + RPIO.PWM.clear_channel_gpio(0, self.number) self._pwm = False def _get_bounce(self): @@ -219,12 +199,12 @@ class RPIOPin(LocalPin): if self._when_changed is None and value is not None: self._when_changed = value RPIO.add_interrupt_callback( - self._number, + self.number, lambda channel, value: self._when_changed(), self._edges, self.GPIO_PULL_UPS[self._pull], self._bounce) elif self._when_changed is not None and value is None: try: - RPIO.del_interrupt_callback(self._number) + RPIO.del_interrupt_callback(self.number) except KeyError: # Ignore this exception which occurs during shutdown; this # simply means RPIO's built-in cleanup has already run and diff --git a/gpiozero/pins/spi.py b/gpiozero/pins/spi.py new file mode 100644 index 0000000..a92ff5c --- /dev/null +++ b/gpiozero/pins/spi.py @@ -0,0 +1,86 @@ +from __future__ import ( + unicode_literals, + print_function, + absolute_import, + division, + ) +str = type('') + + +import operator +from threading import RLock + +from ..devices import Device, SharedMixin +from ..input_devices import InputDevice +from ..output_devices import OutputDevice + + +class SPISoftwareBus(SharedMixin, Device): + def __init__(self, clock_pin, mosi_pin, miso_pin): + self.lock = None + self.clock = None + self.mosi = None + self.miso = None + super(SPISoftwareBus, self).__init__() + self.lock = RLock() + try: + self.clock = OutputDevice(clock_pin, active_high=True) + if mosi_pin is not None: + self.mosi = OutputDevice(mosi_pin) + if miso_pin is not None: + self.miso = InputDevice(miso_pin) + except: + self.close() + raise + + def close(self): + super(SPISoftwareBus, self).close() + if self.lock: + with self.lock: + if self.miso is not None: + self.miso.close() + self.miso = None + if self.mosi is not None: + self.mosi.close() + self.mosi = None + if self.clock is not None: + self.clock.close() + self.clock = None + self.lock = None + + @property + def closed(self): + return self.lock is None + + @classmethod + def _shared_key(cls, clock_pin, mosi_pin, miso_pin): + return (clock_pin, mosi_pin, miso_pin) + + def transfer(self, data, clock_phase=False, lsb_first=False, bits_per_word=8): + """ + Writes data (a list of integer words where each word is assumed to have + :attr:`bits_per_word` bits or less) to the SPI interface, and reads an + equivalent number of words, returning them as a list of integers. + """ + result = [] + with self.lock: + shift = operator.lshift if lsb_first else operator.rshift + for write_word in data: + mask = 1 if lsb_first else 1 << (bits_per_word - 1) + read_word = 0 + for _ in range(bits_per_word): + if self.mosi is not None: + self.mosi.value = bool(write_word & mask) + self.clock.on() + if self.miso is not None and not clock_phase: + if self.miso.value: + read_word |= mask + self.clock.off() + if self.miso is not None and clock_phase: + if self.miso.value: + read_word |= mask + mask = shift(mask, 1) + result.append(read_word) + return result + + diff --git a/gpiozero/spi.py b/gpiozero/spi.py deleted file mode 100644 index e89ab98..0000000 --- a/gpiozero/spi.py +++ /dev/null @@ -1,419 +0,0 @@ -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, - ) -str = type('') - - -import warnings -import operator -from threading import RLock - -try: - from spidev import SpiDev -except ImportError: - SpiDev = None - -from .devices import Device, SharedMixin, _PINS, _PINS_LOCK -from .input_devices import InputDevice -from .output_devices import OutputDevice -from .exc import SPIBadArgs, SPISoftwareFallback, GPIOPinInUse, DeviceClosed - - -class SPIHardwareInterface(Device): - def __init__(self, port, device): - self._device = None - super(SPIHardwareInterface, self).__init__() - # XXX How can we detect conflicts with existing GPIO instances? This - # isn't ideal ... in fact, it's downright crap and doesn't guard - # against conflicts created *after* this instance, but it's all I can - # come up with right now ... - conflicts = (11, 10, 9, (8, 7)[device]) - with _PINS_LOCK: - for pin in _PINS: - if pin.number in conflicts: - raise GPIOPinInUse( - 'pin %r is already in use by another gpiozero object' % pin - ) - self._device_num = device - self._device = SpiDev() - self._device.open(port, device) - self._device.max_speed_hz = 500000 - - def close(self): - if self._device: - try: - self._device.close() - finally: - self._device = None - super(SPIHardwareInterface, self).close() - - @property - def closed(self): - return self._device is None - - def __repr__(self): - try: - self._check_open() - return ( - "hardware SPI on clock_pin=11, mosi_pin=10, miso_pin=9, " - "select_pin=%d" % ( - 8 if self._device_num == 0 else 7)) - except DeviceClosed: - return "hardware SPI closed" - - def read(self, n): - return self.transfer((0,) * n) - - def write(self, data): - return len(self.transfer(data)) - - def transfer(self, data): - """ - Writes data (a list of integer words where each word is assumed to have - :attr:`bits_per_word` bits or less) to the SPI interface, and reads an - equivalent number of words, returning them as a list of integers. - """ - return self._device.xfer2(data) - - def _get_clock_mode(self): - return self._device.mode - - def _set_clock_mode(self, value): - self._device.mode = value - - def _get_clock_polarity(self): - return bool(self.clock_mode & 2) - - def _set_clock_polarity(self, value): - self.clock_mode = self.clock_mode & (~2) | (bool(value) << 1) - - def _get_clock_phase(self): - return bool(self.clock_mode & 1) - - def _set_clock_phase(self, value): - self.clock_mode = self.clock_mode & (~1) | bool(value) - - def _get_lsb_first(self): - return self._device.lsbfirst - - def _set_lsb_first(self, value): - self._device.lsbfirst = bool(value) - - def _get_select_high(self): - return self._device.cshigh - - def _set_select_high(self, value): - self._device.cshigh = bool(value) - - def _get_bits_per_word(self): - return self._device.bits_per_word - - def _set_bits_per_word(self, value): - self._device.bits_per_word = value - - clock_polarity = property(_get_clock_polarity, _set_clock_polarity) - clock_phase = property(_get_clock_phase, _set_clock_phase) - clock_mode = property(_get_clock_mode, _set_clock_mode) - lsb_first = property(_get_lsb_first, _set_lsb_first) - select_high = property(_get_select_high, _set_select_high) - bits_per_word = property(_get_bits_per_word, _set_bits_per_word) - - -class SPISoftwareBus(SharedMixin, Device): - def __init__(self, clock_pin, mosi_pin, miso_pin): - self.lock = None - self.clock = None - self.mosi = None - self.miso = None - super(SPISoftwareBus, self).__init__() - self.lock = RLock() - try: - self.clock = OutputDevice(clock_pin, active_high=True) - if mosi_pin is not None: - self.mosi = OutputDevice(mosi_pin) - if miso_pin is not None: - self.miso = InputDevice(miso_pin) - except: - self.close() - raise - - def close(self): - super(SPISoftwareBus, self).close() - if self.lock: - with self.lock: - if self.miso is not None: - self.miso.close() - self.miso = None - if self.mosi is not None: - self.mosi.close() - self.mosi = None - if self.clock is not None: - self.clock.close() - self.clock = None - self.lock = None - - @property - def closed(self): - return self.lock is None - - @classmethod - def _shared_key(cls, clock_pin, mosi_pin, miso_pin): - return (clock_pin, mosi_pin, miso_pin) - - def transfer(self, data, clock_phase=False, lsb_first=False, bits_per_word=8): - """ - Writes data (a list of integer words where each word is assumed to have - :attr:`bits_per_word` bits or less) to the SPI interface, and reads an - equivalent number of words, returning them as a list of integers. - """ - result = [] - with self.lock: - shift = operator.lshift if lsb_first else operator.rshift - for write_word in data: - mask = 1 if lsb_first else 1 << (bits_per_word - 1) - read_word = 0 - for _ in range(bits_per_word): - if self.mosi is not None: - self.mosi.value = bool(write_word & mask) - self.clock.on() - if self.miso is not None and not clock_phase: - if self.miso.value: - read_word |= mask - self.clock.off() - if self.miso is not None and clock_phase: - if self.miso.value: - read_word |= mask - mask = shift(mask, 1) - result.append(read_word) - return result - - -class SPISoftwareInterface(OutputDevice): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin): - self._bus = None - super(SPISoftwareInterface, self).__init__(select_pin, active_high=False) - try: - self._clock_phase = False - self._lsb_first = False - self._bits_per_word = 8 - self._bus = SPISoftwareBus(clock_pin, mosi_pin, miso_pin) - except: - self.close() - raise - - def close(self): - if self._bus: - self._bus.close() - self._bus = None - super(SPISoftwareInterface, self).close() - - def __repr__(self): - try: - self._check_open() - return ( - "software SPI on clock_pin=%d, mosi_pin=%d, miso_pin=%d, " - "select_pin=%d" % ( - self._bus.clock.pin.number, - self._bus.mosi.pin.number, - self._bus.miso.pin.number, - self.pin.number)) - except DeviceClosed: - return "software SPI closed" - - def read(self, n): - return self.transfer((0,) * n) - - def write(self, data): - return len(self.transfer(data)) - - def transfer(self, data): - with self._bus.lock: - self.on() - try: - return self._bus.transfer( - data, self._clock_phase, self._lsb_first, self._bits_per_word) - finally: - self.off() - - def _get_clock_mode(self): - return (self.clock_polarity << 1) | self.clock_phase - - def _set_clock_mode(self, value): - value = int(value) - if not 0 <= value <= 3: - raise ValueError('clock_mode must be a value between 0 and 3 inclusive') - self.clock_polarity = bool(value & 2) - self.clock_phase = bool(value & 1) - - def _get_clock_polarity(self): - with self._bus.lock: - return not self._bus.clock.active_high - - def _set_clock_polarity(self, value): - with self._bus.lock: - self._bus.clock.active_high = not value - self._bus.clock.off() - - def _get_clock_phase(self): - return self._clock_phase - - def _set_clock_phase(self, value): - self._clock_phase = bool(value) - - def _get_lsb_first(self): - return self._lsb_first - - def _set_lsb_first(self, value): - self._lsb_first = bool(value) - - def _get_bits_per_word(self): - return self._bits_per_word - - def _set_bits_per_word(self, value): - if value < 1: - raise ValueError('bits_per_word must be positive') - self._bits_per_word = int(value) - - def _get_select_high(self): - return self.active_high - - def _set_select_high(self, value): - with self._bus.lock: - self.active_high = value - self.off() - - clock_polarity = property(_get_clock_polarity, _set_clock_polarity) - clock_phase = property(_get_clock_phase, _set_clock_phase) - clock_mode = property(_get_clock_mode, _set_clock_mode) - lsb_first = property(_get_lsb_first, _set_lsb_first) - bits_per_word = property(_get_bits_per_word, _set_bits_per_word) - select_high = property(_get_select_high, _set_select_high) - - -class SharedSPIHardwareInterface(SharedMixin, SPIHardwareInterface): - @classmethod - def _shared_key(cls, port, device): - return (port, device) - - -class SharedSPISoftwareInterface(SharedMixin, SPISoftwareInterface): - @classmethod - def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin): - return (clock_pin, mosi_pin, miso_pin, select_pin) - - -def extract_spi_args(**kwargs): - """ - Given a set of keyword arguments, splits it into those relevant to SPI - implementations and all the rest. SPI arguments are augmented with defaults - and converted into the pin format (from the port/device format) if - necessary. - - Returns a tuple of ``(spi_args, other_args)``. - """ - pin_defaults = { - 'clock_pin': 11, - 'mosi_pin': 10, - 'miso_pin': 9, - 'select_pin': 8, - } - dev_defaults = { - 'port': 0, - 'device': 0, - } - spi_args = { - key: value for (key, value) in kwargs.items() - if key in pin_defaults or key in dev_defaults - } - kwargs = { - key: value for (key, value) in kwargs.items() - if key not in spi_args - } - if not spi_args: - spi_args = pin_defaults - elif set(spi_args) <= set(pin_defaults): - spi_args = { - key: spi_args.get(key, default) - for key, default in pin_defaults.items() - } - elif set(spi_args) <= set(dev_defaults): - spi_args = { - key: spi_args.get(key, default) - for key, default in dev_defaults.items() - } - if spi_args['port'] != 0: - raise SPIBadArgs('port 0 is the only valid SPI port') - if spi_args['device'] not in (0, 1): - raise SPIBadArgs('device must be 0 or 1') - spi_args = { - key: value if key != 'select_pin' else (8, 7)[spi_args['device']] - for key, value in pin_defaults.items() - } - else: - raise SPIBadArgs( - 'you must either specify port and device, or clock_pin, mosi_pin, ' - 'miso_pin, and select_pin; combinations of the two schemes (e.g. ' - 'port and clock_pin) are not permitted') - return spi_args, kwargs - - -def SPI(**spi_args): - """ - Returns an SPI interface, for the specified SPI *port* and *device*, or for - the specified pins (*clock_pin*, *mosi_pin*, *miso_pin*, and *select_pin*). - Only one of the schemes can be used; attempting to mix *port* and *device* - with pin numbers will raise :exc:`SPIBadArgs`. - - If the pins specified match the hardware SPI pins (clock on GPIO11, MOSI on - GPIO10, MISO on GPIO9, and chip select on GPIO8 or GPIO7), and the spidev - module can be imported, a :class:`SPIHardwareInterface` instance will be - returned. Otherwise, a :class:`SPISoftwareInterface` will be returned which - will use simple bit-banging to communicate. - - Both interfaces have the same API, support clock polarity and phase - attributes, and can handle half and full duplex communications, but the - hardware interface is significantly faster (though for many things this - doesn't matter). - - Finally, the *shared* keyword argument specifies whether the resulting - SPI interface can be repeatedly created and used by multiple devices - (useful with multi-channel devices like numerous ADCs). - """ - spi_args, kwargs = extract_spi_args(**spi_args) - shared = kwargs.pop('shared', False) - if kwargs: - raise SPIBadArgs( - 'unrecognized keyword argument %s' % kwargs.popitem()[0]) - if all(( - spi_args['clock_pin'] == 11, - spi_args['mosi_pin'] == 10, - spi_args['miso_pin'] == 9, - spi_args['select_pin'] in (7, 8), - )): - if SpiDev is None: - warnings.warn( - SPISoftwareFallback( - 'failed to import spidev, falling back to software SPI')) - else: - try: - hardware_spi_args = { - 'port': 0, - 'device': {8: 0, 7: 1}[spi_args['select_pin']], - } - if shared: - return SharedSPIHardwareInterface(**hardware_spi_args) - else: - return SPIHardwareInterface(**hardware_spi_args) - except Exception as e: - warnings.warn( - SPISoftwareFallback( - 'failed to initialize hardware SPI, falling back to ' - 'software (error was: %s)' % str(e))) - if shared: - return SharedSPISoftwareInterface(**spi_args) - else: - return SPISoftwareInterface(**spi_args) - diff --git a/gpiozero/spi_devices.py b/gpiozero/spi_devices.py index 7aff666..3840583 100644 --- a/gpiozero/spi_devices.py +++ b/gpiozero/spi_devices.py @@ -16,7 +16,6 @@ except ImportError: from .exc import DeviceClosed, SPIBadChannel from .devices import Device -from .spi import SPI class SPIDevice(Device): @@ -28,13 +27,12 @@ class SPIDevice(Device): specified with the constructor. """ def __init__(self, **spi_args): - self._spi = SPI(**spi_args) + self._spi = self._pin_factory.spi(**spi_args) def close(self): if self._spi: - s = self._spi + self._spi.close() self._spi = None - s.close() super(SPIDevice, self).close() @property diff --git a/setup.py b/setup.py index 0cd18c1..3540700 100644 --- a/setup.py +++ b/setup.py @@ -68,12 +68,12 @@ if sys.version_info[:2] == (3, 2): __entry_points__ = { 'gpiozero_pin_factories': [ - 'PiGPIOPin = gpiozero.pins.pigpiod:PiGPIOPin', - 'RPiGPIOPin = gpiozero.pins.rpigpio:RPiGPIOPin', - 'RPIOPin = gpiozero.pins.rpio:RPIOPin', - 'NativePin = gpiozero.pins.native:NativePin', - 'MockPin = gpiozero.pins.mock:MockPin', - 'MockPWMPin = gpiozero.pins.mock:MockPWMPin', + 'pigpio = gpiozero.pins.pigpiod:PiGPIOFactory', + 'rpigpio = gpiozero.pins.rpigpio:RPiGPIOFactory', + 'rpio = gpiozero.pins.rpio:RPIOFactory', + 'native = gpiozero.pins.native:NativeFactory', + 'mock = gpiozero.pins.mock:MockFactory', + 'mockpwm = gpiozero.pins.mock:MockPWMFactory', ], 'console_scripts': [ 'pinout = gpiozero.cli.pinout:main', diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..79b6d4c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +from __future__ import ( + unicode_literals, + print_function, + absolute_import, + division, + ) +str = type('') + +import os +os.environ['GPIOZERO_PIN_FACTORY'] = 'mock' diff --git a/tests/test_boards.py b/tests/test_boards.py index 8152f66..c0f34e0 100644 --- a/tests/test_boards.py +++ b/tests/test_boards.py @@ -11,26 +11,39 @@ import sys import pytest from time import sleep -from gpiozero.pins.mock import MockPin, MockPWMPin from gpiozero import * +from gpiozero.pins.mock import MockPWMPin, MockPin def setup_function(function): - import gpiozero.devices # dirty, but it does the job - if function.__name__ in ('test_robot', 'test_ryanteck_robot', 'test_camjam_kit_robot', 'test_led_borg', 'test_snow_pi_initial_value_pwm'): - gpiozero.devices.pin_factory = MockPWMPin - else: - gpiozero.devices.pin_factory = MockPin + Device._pin_factory.pin_class = MockPWMPin if function.__name__ in ( + 'test_robot', + 'test_ryanteck_robot', + 'test_camjam_kit_robot', + 'test_led_borg', + 'test_led_board_pwm_value', + 'test_led_board_pwm_bad_value', + 'test_snow_pi_initial_value_pwm', + 'test_led_board_pwm_initial_value', + 'test_led_board_pwm_bad_initial_value', + 'test_led_board_fade_background', + 'test_led_bar_graph_pwm_value', + 'test_led_bar_graph_pwm_initial_value', + ) else MockPin def teardown_function(function): - MockPin.clear_pins() + Device._pin_factory.reset() + +def teardown_module(module): + # make sure we reset the default + Device._pin_factory.pwm = False def test_composite_output_on_off(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: device.on() assert all((pin1.state, pin2.state, pin3.state)) @@ -38,9 +51,9 @@ def test_composite_output_on_off(): assert not any((pin1.state, pin2.state, pin3.state)) def test_composite_output_toggle(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: device.toggle() assert all((pin1.state, pin2.state, pin3.state)) @@ -51,9 +64,9 @@ def test_composite_output_toggle(): assert not pin3.state def test_composite_output_value(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: assert device.value == (0, 0, 0) device.toggle() @@ -64,9 +77,9 @@ def test_composite_output_value(): assert device[2].is_active def test_led_board_on_off(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: assert isinstance(board[0], LED) assert isinstance(board[1], LED) @@ -121,9 +134,9 @@ def test_led_board_on_off(): assert pin3.state def test_led_board_active_low(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: assert not board.active_high assert not board[0].active_high @@ -145,9 +158,9 @@ def test_led_board_active_low(): assert not pin3.state def test_led_board_value(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: assert board.value == (0, 0, 0) board.value = (0, 1, 0) @@ -156,9 +169,9 @@ def test_led_board_value(): assert board.value == (1, 0, 1) def test_led_board_pwm_value(): - pin1 = MockPWMPin(2) - pin2 = MockPWMPin(3) - pin3 = MockPWMPin(4) + 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: assert board.value == (0, 0, 0) board.value = (0, 1, 0) @@ -167,9 +180,9 @@ def test_led_board_pwm_value(): assert board.value == (0.5, 0, 0.75) def test_led_board_pwm_bad_value(): - pin1 = MockPWMPin(2) - pin2 = MockPWMPin(3) - pin3 = MockPWMPin(4) + 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 pytest.raises(ValueError): board.value = (-1, 0, 0) @@ -177,18 +190,18 @@ def test_led_board_pwm_bad_value(): board.value = (0, 2, 0) def test_led_board_initial_value(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: assert board.value == (0, 0, 0) with LEDBoard(pin1, pin2, foo=pin3, initial_value=1) as board: assert board.value == (1, 1, 1) def test_led_board_pwm_initial_value(): - pin1 = MockPWMPin(2) - pin2 = MockPWMPin(3) - pin3 = MockPWMPin(4) + 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: assert board.value == (0, 0, 0) with LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=1) as board: @@ -197,18 +210,18 @@ def test_led_board_pwm_initial_value(): assert board.value == (0.5, 0.5, 0.5) def test_led_board_pwm_bad_initial_value(): - pin1 = MockPWMPin(2) - pin2 = MockPWMPin(3) - pin3 = MockPWMPin(4) + pin1 = Device._pin_factory.pin(2) + 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) with pytest.raises(ValueError): LEDBoard(pin1, pin2, foo=pin3, pwm=True, initial_value=2) def test_led_board_nested(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: assert list(led.pin for led in board.leds) == [pin1, pin2, pin3] assert board.value == (0, (0, 0)) @@ -218,9 +231,9 @@ def test_led_board_nested(): assert pin3.state def test_led_board_bad_blink(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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 pytest.raises(ValueError): board.blink(fade_in_time=1, fade_out_time=1) @@ -232,9 +245,9 @@ def test_led_board_bad_blink(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_led_board_blink_background(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: board.blink(0.1, 0.1, n=2) board._blink_thread.join() # naughty, but ensures no arbitrary waits in the test @@ -252,9 +265,9 @@ def test_led_board_blink_background(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_led_board_blink_foreground(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: board.blink(0.1, 0.1, n=2, background=False) test = [ @@ -271,9 +284,9 @@ def test_led_board_blink_foreground(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_led_board_blink_control(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: board.blink(0.1, 0.1, n=2) # make sure the blink thread's started @@ -296,9 +309,9 @@ def test_led_board_blink_control(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_led_board_blink_take_over(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: board[1].blink(0.1, 0.1, n=2) board.blink(0.1, 0.1, n=2) # immediately take over blinking @@ -318,9 +331,9 @@ def test_led_board_blink_take_over(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_led_board_blink_control_all(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: board.blink(0.1, 0.1, n=2) # make sure the blink thread's started @@ -340,9 +353,9 @@ def test_led_board_blink_control_all(): pin3.assert_states_and_times(test) def test_led_board_blink_interrupt_on(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: board.blink(1, 0.1) sleep(0.2) @@ -352,9 +365,9 @@ def test_led_board_blink_interrupt_on(): pin3.assert_states([False, True, False]) def test_led_board_blink_interrupt_off(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: board.blink(0.1, 1) sleep(0.2) @@ -366,9 +379,9 @@ def test_led_board_blink_interrupt_off(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_led_board_fade_background(): - pin1 = MockPWMPin(2) - pin2 = MockPWMPin(3) - pin3 = MockPWMPin(4) + pin1 = Device._pin_factory.pin(2) + pin2 = Device._pin_factory.pin(3) + pin3 = Device._pin_factory.pin(4) with LEDBoard(pin1, LEDBoard(pin2, pin3, pwm=True), pwm=True) as board: board.blink(0, 0, 0.2, 0.2, n=2) board._blink_thread.join() @@ -400,9 +413,9 @@ def test_led_board_fade_background(): pin3.assert_states_and_times(test) def test_led_bar_graph_value(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: assert isinstance(graph[0], LED) assert isinstance(graph[1], LED) @@ -433,9 +446,9 @@ def test_led_bar_graph_value(): assert graph.value == -2/3 def test_led_bar_graph_active_low(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: assert not graph.active_high assert not graph[0].active_high @@ -455,9 +468,9 @@ def test_led_bar_graph_active_low(): assert not pin3.state and pin1.state and pin2.state def test_led_bar_graph_pwm_value(): - pin1 = MockPWMPin(2) - pin2 = MockPWMPin(3) - pin3 = MockPWMPin(4) + 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: assert isinstance(graph[0], PWMLED) assert isinstance(graph[1], PWMLED) @@ -482,9 +495,9 @@ def test_led_bar_graph_pwm_value(): assert graph.value == -1/2 def test_led_bar_graph_bad_value(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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 pytest.raises(ValueError): graph.value = -2 @@ -492,9 +505,9 @@ def test_led_bar_graph_bad_value(): graph.value = 2 def test_led_bar_graph_bad_init(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + pin1 = Device._pin_factory.pin(2) + pin2 = Device._pin_factory.pin(3) + pin3 = Device._pin_factory.pin(4) with pytest.raises(TypeError): LEDBarGraph(pin1, pin2, foo=pin3) with pytest.raises(ValueError): @@ -503,9 +516,9 @@ def test_led_bar_graph_bad_init(): LEDBarGraph(pin1, pin2, pin3, initial_value=2) def test_led_bar_graph_initial_value(): - pin1 = MockPin(2) - pin2 = MockPin(3) - pin3 = MockPin(4) + 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: assert graph.value == 1/3 assert pin1.state and not (pin2.state or pin3.state) @@ -514,9 +527,9 @@ def test_led_bar_graph_initial_value(): assert pin3.state and not (pin1.state or pin2.state) def test_led_bar_graph_pwm_initial_value(): - pin1 = MockPWMPin(2) - pin2 = MockPWMPin(3) - pin3 = MockPWMPin(4) + 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: assert graph.value == 0.5 assert (pin1.state, pin2.state, pin3.state) == (1, 0.5, 0) @@ -525,17 +538,17 @@ def test_led_bar_graph_pwm_initial_value(): assert (pin1.state, pin2.state, pin3.state) == (0, 0.5, 1) def test_led_borg(): - pins = [MockPWMPin(n) for n in (17, 27, 22)] + pins = [Device._pin_factory.pin(n) for n in (17, 27, 22)] with LedBorg() as board: assert [device.pin for device in board._leds] == pins def test_pi_liter(): - pins = [MockPin(n) for n in (4, 17, 27, 18, 22, 23, 24, 25)] + pins = [Device._pin_factory.pin(n) for n in (4, 17, 27, 18, 22, 23, 24, 25)] with PiLiter() as board: assert [device.pin for device in board] == pins def test_pi_liter_graph(): - pins = [MockPin(n) for n in (4, 17, 27, 18, 22, 23, 24, 25)] + pins = [Device._pin_factory.pin(n) for n in (4, 17, 27, 18, 22, 23, 24, 25)] with PiLiterBarGraph() as board: board.value = 0.5 assert [pin.state for pin in pins] == [1, 1, 1, 1, 0, 0, 0, 0] @@ -543,9 +556,9 @@ def test_pi_liter_graph(): assert board.value == 5/8 def test_traffic_lights(): - red_pin = MockPin(2) - amber_pin = MockPin(3) - green_pin = MockPin(4) + 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: board.red.on() assert board.red.value @@ -574,15 +587,15 @@ def test_traffic_lights(): def test_traffic_lights_bad_init(): with pytest.raises(ValueError): TrafficLights() - red_pin = MockPin(2) - amber_pin = MockPin(3) - green_pin = MockPin(4) - yellow_pin = MockPin(5) + red_pin = Device._pin_factory.pin(2) + amber_pin = Device._pin_factory.pin(3) + 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) def test_pi_traffic(): - pins = [MockPin(n) for n in (9, 10, 11)] + pins = [Device._pin_factory.pin(n) for n in (9, 10, 11)] with PiTraffic() as board: assert [device.pin for device in board] == pins @@ -591,27 +604,27 @@ def test_pi_stop(): PiStop() with pytest.raises(ValueError): PiStop('E') - pins_a = [MockPin(n) for n in (7, 8, 25)] + pins_a = [Device._pin_factory.pin(n) for n in (7, 8, 25)] with PiStop('A') as board: assert [device.pin for device in board] == pins_a - pins_aplus = [MockPin(n) for n in (21, 20, 16)] + pins_aplus = [Device._pin_factory.pin(n) for n in (21, 20, 16)] with PiStop('A+') as board: assert [device.pin for device in board] == pins_aplus - pins_b = [MockPin(n) for n in (10, 9, 11)] + pins_b = [Device._pin_factory.pin(n) for n in (10, 9, 11)] with PiStop('B') as board: assert [device.pin for device in board] == pins_b - pins_bplus = [MockPin(n) for n in (13, 19, 26)] + pins_bplus = [Device._pin_factory.pin(n) for n in (13, 19, 26)] with PiStop('B+') as board: assert [device.pin for device in board] == pins_bplus - pins_c = [MockPin(n) for n in (18, 15, 14)] + pins_c = [Device._pin_factory.pin(n) for n in (18, 15, 14)] with PiStop('C') as board: assert [device.pin for device in board] == pins_c - pins_d = [MockPin(n) for n in (2, 3, 4)] + pins_d = [Device._pin_factory.pin(n) for n in (2, 3, 4)] with PiStop('D') as board: assert [device.pin for device in board] == pins_d def test_snow_pi(): - pins = [MockPin(n) for n in (23, 24, 25, 17, 18, 22, 7, 8, 9)] + pins = [Device._pin_factory.pin(n) for n in (23, 24, 25, 17, 18, 22, 7, 8, 9)] with SnowPi() as board: assert [device.pin for device in board.leds] == pins @@ -626,17 +639,17 @@ def test_snow_pi_initial_value(): assert all(device.pin.state == True for device in board.leds) def test_snow_pi_initial_value_pwm(): - pins = [MockPWMPin(n) for n in (23, 24, 25, 17, 18, 22, 7, 8, 9)] + pins = [Device._pin_factory.pin(n) for n in (23, 24, 25, 17, 18, 22, 7, 8, 9)] with SnowPi(pwm=True, initial_value=0.5) as board: assert [device.pin for device in board.leds] == pins assert all(device.pin.state == 0.5 for device in board.leds) def test_traffic_lights_buzzer(): - red_pin = MockPin(2) - amber_pin = MockPin(3) - green_pin = MockPin(4) - buzzer_pin = MockPin(5) - button_pin = MockPin(6) + red_pin = Device._pin_factory.pin(2) + amber_pin = Device._pin_factory.pin(3) + green_pin = Device._pin_factory.pin(4) + 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), @@ -651,17 +664,17 @@ def test_traffic_lights_buzzer(): assert board.button.is_active def test_fish_dish(): - pins = [MockPin(n) for n in (9, 22, 4, 8, 7)] + pins = [Device._pin_factory.pin(n) for n in (9, 22, 4, 8, 7)] with FishDish() as board: assert [led.pin for led in board.lights] + [board.buzzer.pin, board.button.pin] == pins def test_traffic_hat(): - pins = [MockPin(n) for n in (24, 23, 22, 5, 25)] + pins = [Device._pin_factory.pin(n) for n in (24, 23, 22, 5, 25)] with TrafficHat() as board: assert [led.pin for led in board.lights] + [board.buzzer.pin, board.button.pin] == pins def test_robot(): - pins = [MockPWMPin(n) for n in (2, 3, 4, 5)] + pins = [Device._pin_factory.pin(n) for n in (2, 3, 4, 5)] with Robot((2, 3), (4, 5)) as robot: assert ( [device.pin for device in robot.left_motor] + @@ -696,12 +709,12 @@ def test_robot(): assert robot.value == (0, -0.5) def test_ryanteck_robot(): - pins = [MockPWMPin(n) for n in (17, 18, 22, 23)] + pins = [Device._pin_factory.pin(n) for n in (17, 18, 22, 23)] with RyanteckRobot() as board: assert [device.pin for motor in board for device in motor] == pins def test_camjam_kit_robot(): - pins = [MockPWMPin(n) for n in (9, 10, 7, 8)] + pins = [Device._pin_factory.pin(n) for n in (9, 10, 7, 8)] with CamJamKitRobot() as board: assert [device.pin for motor in board for device in motor] == pins @@ -714,7 +727,7 @@ def test_energenie_bad_init(): Energenie(5) def test_energenie(): - pins = [MockPin(n) for n in (17, 22, 23, 27, 24, 25)] + pins = [Device._pin_factory.pin(n) for n in (17, 22, 23, 27, 24, 25)] with Energenie(1, initial_value=True) as device1, \ Energenie(2, initial_value=False) as device2: assert repr(device1) == '' diff --git a/tests/test_devices.py b/tests/test_devices.py index e46a735..01b714a 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -6,86 +6,87 @@ from __future__ import ( ) str = type('') +import warnings import pytest -from gpiozero.pins.mock import MockPin from gpiozero import * def teardown_function(function): - MockPin.clear_pins() + Device._pin_factory.reset() + # TODO add more devices tests! -def test_device_no_pin(): +def test_device_bad_pin(): with pytest.raises(GPIOPinMissing): device = GPIODevice() + with pytest.raises(PinInvalidPin): + device = GPIODevice(60) + +def test_device_non_physical(): + with warnings.catch_warnings(record=True) as w: + device = GPIODevice(37) + assert len(w) == 1 + assert w[0].category == PinNonPhysical def test_device_init(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with GPIODevice(pin) as device: assert not device.closed assert device.pin == pin def test_device_init_twice_same_pin(): - pin = MockPin(2) - with GPIODevice(pin) as device: + with GPIODevice(2) as device: with pytest.raises(GPIOPinInUse): - device2 = GPIODevice(pin) + GPIODevice(2) def test_device_init_twice_different_pin(): - pin = MockPin(2) - pin2 = MockPin(3) - with GPIODevice(pin) as device: - with GPIODevice(pin2) as device2: + with GPIODevice(2) as device: + with GPIODevice(3) as device2: pass def test_device_close(): - pin = MockPin(2) - device = GPIODevice(pin) + device = GPIODevice(2) device.close() assert device.closed assert device.pin is None def test_device_reopen_same_pin(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) device = GPIODevice(pin) device.close() device2 = GPIODevice(pin) assert not device2.closed - assert device2.pin == pin + assert device2.pin is pin assert device.closed assert device.pin is None device2.close() def test_device_repr(): - pin = MockPin(2) - with GPIODevice(pin) as device: - assert repr(device) == '' % pin + with GPIODevice(2) as device: + assert repr(device) == '' % device.pin def test_device_repr_after_close(): - pin = MockPin(2) - device = GPIODevice(pin) + device = GPIODevice(2) device.close() assert repr(device) == '' def test_device_unknown_attr(): - pin = MockPin(2) - with GPIODevice(pin) as device: + with GPIODevice(2) as device: with pytest.raises(AttributeError): device.foo = 1 def test_device_context_manager(): - pin = MockPin(2) - with GPIODevice(pin) as device: + with GPIODevice(2) as device: assert not device.closed assert device.closed def test_composite_device_sequence(): with CompositeDevice( - InputDevice(MockPin(2)), - InputDevice(MockPin(3)) + InputDevice(2), + InputDevice(3) ) as device: assert len(device) == 2 assert device[0].pin.number == 2 @@ -94,8 +95,8 @@ def test_composite_device_sequence(): def test_composite_device_values(): with CompositeDevice( - InputDevice(MockPin(2)), - InputDevice(MockPin(3)) + InputDevice(2), + InputDevice(3) ) as device: assert device.value == (0, 0) assert not device.is_active @@ -105,8 +106,8 @@ def test_composite_device_values(): def test_composite_device_named(): with CompositeDevice( - foo=InputDevice(MockPin(2)), - bar=InputDevice(MockPin(3)), + foo=InputDevice(2), + bar=InputDevice(3), _order=('foo', 'bar') ) as device: assert device.namedtuple._fields == ('foo', 'bar') @@ -121,13 +122,13 @@ def test_composite_device_bad_init(): with pytest.raises(ValueError): CompositeDevice(2) with pytest.raises(ValueError): - CompositeDevice(MockPin(2)) + CompositeDevice(Device._pin_factory.pin(2)) def test_composite_device_read_only(): - device = CompositeDevice( - foo=InputDevice(MockPin(2)), - bar=InputDevice(MockPin(3)) - ) - with pytest.raises(AttributeError): - device.foo = 1 + with CompositeDevice( + foo=InputDevice(2), + bar=InputDevice(3) + ) as device: + with pytest.raises(AttributeError): + device.foo = 1 diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 85e26a5..faf5894 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -12,57 +12,52 @@ import pytest from threading import Event from functools import partial -from gpiozero.pins.mock import ( - MockPin, - MockPulledUpPin, - MockChargingPin, - MockTriggerPin, - ) +from gpiozero.pins.mock import MockPulledUpPin, MockChargingPin, MockTriggerPin from gpiozero import * def teardown_function(function): - MockPin.clear_pins() + Device._pin_factory.reset() + def test_input_initial_values(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with InputDevice(pin, pull_up=True) as device: assert pin.function == 'input' assert pin.pull == 'up' assert device.pull_up - device.close() - device = InputDevice(pin, pull_up=False) + with InputDevice(pin, pull_up=False) as device: assert pin.pull == 'down' assert not device.pull_up def test_input_is_active_low(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with InputDevice(pin, pull_up=True) as device: pin.drive_high() assert not device.is_active - assert repr(device) == '' + assert repr(device) == '' pin.drive_low() assert device.is_active - assert repr(device) == '' + assert repr(device) == '' def test_input_is_active_high(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with InputDevice(pin, pull_up=False) as device: pin.drive_high() assert device.is_active - assert repr(device) == '' + assert repr(device) == '' pin.drive_low() assert not device.is_active - assert repr(device) == '' + assert repr(device) == '' def test_input_pulled_up(): - pin = MockPulledUpPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPulledUpPin) with pytest.raises(PinFixedPull): InputDevice(pin, pull_up=False) def test_input_event_activated(): event = Event() - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with DigitalInputDevice(pin) as device: device.when_activated = lambda: event.set() assert not event.is_set() @@ -71,7 +66,7 @@ def test_input_event_activated(): def test_input_event_deactivated(): event = Event() - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with DigitalInputDevice(pin) as device: device.when_deactivated = lambda: event.set() assert not event.is_set() @@ -82,7 +77,7 @@ def test_input_event_deactivated(): def test_input_partial_callback(): event = Event() - pin = MockPin(2) + pin = Device._pin_factory.pin(2) def foo(a, b): event.set() return a + b @@ -95,22 +90,22 @@ def test_input_partial_callback(): assert event.is_set() def test_input_wait_active(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with DigitalInputDevice(pin) as device: pin.drive_high() assert device.wait_for_active(1) assert not device.wait_for_inactive(0) def test_input_wait_inactive(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with DigitalInputDevice(pin) as device: assert device.wait_for_inactive(1) assert not device.wait_for_active(0) def test_input_smoothed_attrib(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with SmoothedInputDevice(pin, threshold=0.5, queue_len=5, partial=False) as device: - assert repr(device) == '' + assert repr(device) == '' assert device.threshold == 0.5 assert device.queue_len == 5 assert not device.partial @@ -120,7 +115,7 @@ def test_input_smoothed_attrib(): device.threshold = 1 def test_input_smoothed_values(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with SmoothedInputDevice(pin) as device: device._queue.start() assert not device.is_active @@ -130,7 +125,7 @@ def test_input_smoothed_values(): assert device.wait_for_inactive(1) def test_input_button(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with Button(pin) as button: assert pin.pull == 'up' assert not button.is_pressed @@ -142,7 +137,7 @@ def test_input_button(): assert button.wait_for_release(1) def test_input_line_sensor(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with LineSensor(pin) as sensor: pin.drive_low() # logic is inverted for line sensor assert sensor.wait_for_line(1) @@ -152,7 +147,7 @@ def test_input_line_sensor(): assert not sensor.line_detected def test_input_motion_sensor(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with MotionSensor(pin) as sensor: pin.drive_high() assert sensor.wait_for_motion(1) @@ -164,7 +159,7 @@ def test_input_motion_sensor(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_input_light_sensor(): - pin = MockChargingPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockChargingPin) with LightSensor(pin) as sensor: pin.charge_time = 0.1 assert sensor.wait_for_dark(1) @@ -174,8 +169,8 @@ def test_input_light_sensor(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_input_distance_sensor(): - echo_pin = MockPin(2) - trig_pin = MockTriggerPin(3) + echo_pin = Device._pin_factory.pin(2) + trig_pin = Device._pin_factory.pin(3, pin_class=MockTriggerPin) trig_pin.echo_pin = echo_pin trig_pin.echo_time = 0.02 with pytest.raises(ValueError): diff --git a/tests/test_mock_pin.py b/tests/test_mock_pin.py index c8bdb95..4d5414e 100644 --- a/tests/test_mock_pin.py +++ b/tests/test_mock_pin.py @@ -11,25 +11,24 @@ from threading import Event import pytest -from gpiozero.pins.mock import MockPin, MockPWMPin +from gpiozero.pins.mock import MockPWMPin, MockPin from gpiozero import * def teardown_function(function): - MockPin.clear_pins() + Device._pin_factory.reset() + # Some rough tests to make sure our MockPin is up to snuff. This is just # enough to get reasonable coverage but it's by no means comprehensive... def test_mock_pin_init(): - with pytest.raises(TypeError): - MockPin() with pytest.raises(ValueError): - MockPin(60) - assert MockPin(2).number == 2 + Device._pin_factory.pin(60) + assert Device._pin_factory.pin(2).number == 2 def test_mock_pin_defaults(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) assert pin.bounce == None assert pin.edges == 'both' assert pin.frequency == None @@ -39,30 +38,23 @@ def test_mock_pin_defaults(): assert pin.when_changed == None def test_mock_pin_open_close(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) pin.close() def test_mock_pin_init_twice_same_pin(): - pin1 = MockPin(2) - pin2 = MockPin(pin1.number) + pin1 = Device._pin_factory.pin(2) + pin2 = Device._pin_factory.pin(pin1.number) assert pin1 is pin2 def test_mock_pin_init_twice_different_pin(): - pin1 = MockPin(2) - pin2 = MockPin(pin1.number+1) + pin1 = Device._pin_factory.pin(2) + pin2 = Device._pin_factory.pin(pin1.number+1) assert pin1 != pin2 assert pin1.number == 2 assert pin2.number == pin1.number+1 -def test_mock_pwm_pin_init(): - with pytest.raises(TypeError): - MockPWMPin() - with pytest.raises(ValueError): - MockPWMPin(60) - assert MockPWMPin(2).number == 2 - def test_mock_pwm_pin_defaults(): - pin = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) assert pin.bounce == None assert pin.edges == 'both' assert pin.frequency == None @@ -72,38 +64,38 @@ def test_mock_pwm_pin_defaults(): assert pin.when_changed == None def test_mock_pwm_pin_open_close(): - pin = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) pin.close() def test_mock_pwm_pin_init_twice_same_pin(): - pin1 = MockPWMPin(2) - pin2 = MockPWMPin(pin1.number) + pin1 = Device._pin_factory.pin(2, pin_class=MockPWMPin) + pin2 = Device._pin_factory.pin(pin1.number, pin_class=MockPWMPin) assert pin1 is pin2 def test_mock_pwm_pin_init_twice_different_pin(): - pin1 = MockPWMPin(2) - pin2 = MockPWMPin(pin1.number+1) + pin1 = Device._pin_factory.pin(2, pin_class=MockPWMPin) + pin2 = Device._pin_factory.pin(pin1.number + 1, pin_class=MockPWMPin) assert pin1 != pin2 assert pin1.number == 2 assert pin2.number == pin1.number+1 def test_mock_pin_init_twice_different_modes(): - pin1 = MockPin(2) - pin2 = MockPWMPin(pin1.number+1) + pin1 = Device._pin_factory.pin(2, pin_class=MockPin) + pin2 = Device._pin_factory.pin(pin1.number + 1, pin_class=MockPWMPin) assert pin1 != pin2 with pytest.raises(ValueError): - pin3 = MockPWMPin(pin1.number) + Device._pin_factory.pin(pin1.number, pin_class=MockPWMPin) with pytest.raises(ValueError): - pin4 = MockPin(pin2.number) + Device._pin_factory.pin(pin2.number, pin_class=MockPin) def test_mock_pin_frequency_unsupported(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) pin.frequency = None with pytest.raises(PinPWMUnsupported): pin.frequency = 100 def test_mock_pin_frequency_supported(): - pin = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) pin.function = 'output' assert pin.frequency is None pin.frequency = 100 @@ -112,7 +104,7 @@ def test_mock_pin_frequency_supported(): assert not pin.state def test_mock_pin_pull(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) pin.function = 'input' assert pin.pull == 'floating' pin.pull = 'up' @@ -121,7 +113,7 @@ def test_mock_pin_pull(): assert not pin.state def test_mock_pin_state(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with pytest.raises(PinSetInput): pin.state = 1 pin.function = 'output' @@ -134,7 +126,7 @@ def test_mock_pin_state(): assert pin.state == 1 def test_mock_pwm_pin_state(): - pin = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with pytest.raises(PinSetInput): pin.state = 1 pin.function = 'output' @@ -147,7 +139,7 @@ def test_mock_pwm_pin_state(): assert pin.state == 0.5 def test_mock_pin_edges(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) assert pin.when_changed is None fired = Event() pin.function = 'input' diff --git a/tests/test_outputs.py b/tests/test_outputs.py index eabc414..3816226 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -16,15 +16,16 @@ except ImportError: import pytest -from gpiozero.pins.mock import MockPin, MockPWMPin +from gpiozero.pins.mock import MockPWMPin from gpiozero import * def teardown_function(function): - MockPin.clear_pins() + Device._pin_factory.reset() + def test_output_initial_values(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with OutputDevice(pin, initial_value=False) as device: assert pin.function == 'output' assert not pin.state @@ -35,7 +36,7 @@ def test_output_initial_values(): assert state == pin.state def test_output_write_active_high(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with OutputDevice(pin) as device: device.on() assert pin.state @@ -43,7 +44,7 @@ def test_output_write_active_high(): assert not pin.state def test_output_write_active_low(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with OutputDevice(pin, active_high=False) as device: device.on() assert not pin.state @@ -51,7 +52,7 @@ def test_output_write_active_low(): assert pin.state def test_output_write_closed(): - with OutputDevice(MockPin(2)) as device: + with OutputDevice(Device._pin_factory.pin(2)) as device: device.close() assert device.closed device.close() @@ -60,14 +61,14 @@ def test_output_write_closed(): device.on() def test_output_write_silly(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with OutputDevice(pin) as device: pin.function = 'input' with pytest.raises(AttributeError): device.on() def test_output_value(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with OutputDevice(pin) as device: assert not device.value assert not pin.state @@ -79,7 +80,7 @@ def test_output_value(): assert not pin.state def test_output_digital_toggle(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with DigitalOutputDevice(pin) as device: assert not device.value assert not pin.state @@ -93,7 +94,7 @@ def test_output_digital_toggle(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_output_blink_background(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with DigitalOutputDevice(pin) as device: start = time() device.blink(0.1, 0.1, n=2) @@ -111,7 +112,7 @@ def test_output_blink_background(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_output_blink_foreground(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with DigitalOutputDevice(pin) as device: start = time() device.blink(0.1, 0.1, n=2, background=False) @@ -125,7 +126,7 @@ def test_output_blink_foreground(): ]) def test_output_blink_interrupt_on(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with DigitalOutputDevice(pin) as device: device.blink(1, 0.1) sleep(0.2) @@ -133,7 +134,7 @@ def test_output_blink_interrupt_on(): pin.assert_states([False, True, False]) def test_output_blink_interrupt_off(): - pin = MockPin(2) + pin = Device._pin_factory.pin(2) with DigitalOutputDevice(pin) as device: device.blink(0.1, 1) sleep(0.2) @@ -142,14 +143,14 @@ def test_output_blink_interrupt_off(): def test_output_pwm_bad_initial_value(): with pytest.raises(ValueError): - PWMOutputDevice(MockPin(2), initial_value=2) + PWMOutputDevice(Device._pin_factory.pin(2), initial_value=2) def test_output_pwm_not_supported(): with pytest.raises(AttributeError): - PWMOutputDevice(MockPin(2)) + PWMOutputDevice(Device._pin_factory.pin(2)) def test_output_pwm_states(): - pin = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: device.value = 0.1 device.value = 0.2 @@ -157,7 +158,7 @@ def test_output_pwm_states(): pin.assert_states([0.0, 0.1, 0.2, 0.0]) def test_output_pwm_read(): - pin = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin, frequency=100) as device: assert device.frequency == 100 device.value = 0.1 @@ -170,14 +171,14 @@ def test_output_pwm_read(): assert device.frequency is None def test_output_pwm_write(): - pin = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: device.on() device.off() pin.assert_states([False, True, False]) def test_output_pwm_toggle(): - pin = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: device.toggle() device.value = 0.5 @@ -187,7 +188,7 @@ 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 = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin, active_high=False) as device: device.value = 0.1 assert isclose(device.value, 0.1) @@ -196,17 +197,18 @@ def test_output_pwm_active_high_read(): assert device.value def test_output_pwm_bad_value(): - with pytest.raises(ValueError): - PWMOutputDevice(MockPWMPin(2)).value = 2 + with PWMOutputDevice(Device._pin_factory.pin(2, pin_class=MockPWMPin)) as device: + with pytest.raises(ValueError): + device.value = 2 def test_output_pwm_write_closed(): - device = PWMOutputDevice(MockPWMPin(2)) - device.close() - with pytest.raises(GPIODeviceClosed): - device.on() + with PWMOutputDevice(Device._pin_factory.pin(2, pin_class=MockPWMPin)) as device: + device.close() + with pytest.raises(GPIODeviceClosed): + device.on() def test_output_pwm_write_silly(): - pin = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: pin.function = 'input' with pytest.raises(AttributeError): @@ -215,7 +217,7 @@ 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 = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: start = time() device.blink(0.1, 0.1, n=2) @@ -233,7 +235,7 @@ 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 = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: start = time() device.blink(0.1, 0.1, n=2, background=False) @@ -249,7 +251,7 @@ 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 = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: start = time() device.blink(0, 0, 0.2, 0.2, n=2) @@ -283,7 +285,7 @@ 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 = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: start = time() device.blink(0, 0, 0.2, 0.2, n=2, background=False) @@ -315,7 +317,7 @@ 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 = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: start = time() device.pulse(0.2, 0.2, n=2) @@ -349,7 +351,7 @@ 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 = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: start = time() device.pulse(0.2, 0.2, n=2, background=False) @@ -379,7 +381,7 @@ def test_output_pwm_pulse_foreground(): ]) def test_output_pwm_blink_interrupt(): - pin = MockPWMPin(2) + pin = Device._pin_factory.pin(2, pin_class=MockPWMPin) with PWMOutputDevice(pin) as device: device.blink(1, 0.1) sleep(0.2) @@ -391,7 +393,7 @@ def test_rgbled_missing_pins(): RGBLED() def test_rgbled_initial_value(): - r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + 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: assert r.frequency assert g.frequency @@ -401,24 +403,24 @@ def test_rgbled_initial_value(): assert isclose(b.state, 0.0) def test_rgbled_initial_value_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + 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: assert r.state == 0 assert g.state == 1 assert b.state == 1 def test_rgbled_initial_bad_value(): - r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with pytest.raises(ValueError): RGBLED(r, g, b, initial_value=(0.1, 0.2, 1.2)) def test_rgbled_initial_bad_value_nonpwm(): - r, g, b = (MockPin(i) 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, pwm=False, initial_value=(0.1, 0.2, 0)) def test_rgbled_value(): - r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: assert isinstance(device._leds[0], PWMLED) assert isinstance(device._leds[1], PWMLED) @@ -436,7 +438,7 @@ def test_rgbled_value(): assert device.value == (0.5, 0.5, 0.5) def test_rgbled_value_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, pwm=False) as device: assert isinstance(device._leds[0], LED) assert isinstance(device._leds[1], LED) @@ -451,7 +453,7 @@ def test_rgbled_value_nonpwm(): assert device.value == (0, 0, 0) def test_rgbled_bad_value(): - r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: with pytest.raises(ValueError): device.value = (2, 0, 0) @@ -460,7 +462,7 @@ def test_rgbled_bad_value(): device.value = (0, -1, 0) def test_rgbled_bad_value_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, pwm=False) as device: with pytest.raises(ValueError): device.value = (2, 0, 0) @@ -478,7 +480,7 @@ def test_rgbled_bad_value_nonpwm(): device.value = (0, 0, 0.5) def test_rgbled_toggle(): - r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: assert not device.is_active assert device.value == (0, 0, 0) @@ -490,7 +492,7 @@ def test_rgbled_toggle(): assert device.value == (0, 0, 0) def test_rgbled_toggle_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, pwm=False) as device: assert not device.is_active assert device.value == (0, 0, 0) @@ -501,10 +503,18 @@ def test_rgbled_toggle_nonpwm(): assert not device.is_active assert device.value == (0, 0, 0) +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 pytest.raises(ValueError): + device.blink(fade_in_time=1) + with pytest.raises(ValueError): + device.blink(fade_out_time=1) + @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_blink_background(): - r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: start = time() device.blink(0.1, 0.1, n=2) @@ -525,7 +535,7 @@ def test_rgbled_blink_background(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_blink_background_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, pwm=False) as device: start = time() device.blink(0.1, 0.1, n=2) @@ -546,7 +556,7 @@ 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 = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: start = time() device.blink(0.1, 0.1, n=2, background=False) @@ -565,7 +575,7 @@ def test_rgbled_blink_foreground(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_blink_foreground_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, pwm=False) as device: start = time() device.blink(0.1, 0.1, n=2, background=False) @@ -584,7 +594,7 @@ 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 = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: start = time() device.blink(0, 0, 0.2, 0.2, n=2) @@ -619,7 +629,7 @@ def test_rgbled_fade_background(): b.assert_states_and_times(expected) def test_rgbled_fade_background_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, pwm=False) as device: with pytest.raises(ValueError): device.blink(0, 0, 0.2, 0.2, n=2) @@ -627,7 +637,7 @@ def test_rgbled_fade_background_nonpwm(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_fade_foreground(): - r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: start = time() device.blink(0, 0, 0.2, 0.2, n=2, background=False) @@ -660,7 +670,7 @@ def test_rgbled_fade_foreground(): b.assert_states_and_times(expected) def test_rgbled_fade_foreground_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, pwm=False) as device: with pytest.raises(ValueError): device.blink(0, 0, 0.2, 0.2, n=2, background=False) @@ -668,7 +678,7 @@ def test_rgbled_fade_foreground_nonpwm(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_pulse_background(): - r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: start = time() device.pulse(0.2, 0.2, n=2) @@ -703,7 +713,7 @@ def test_rgbled_pulse_background(): b.assert_states_and_times(expected) def test_rgbled_pulse_background_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, pwm=False) as device: with pytest.raises(ValueError): device.pulse(0.2, 0.2, n=2) @@ -711,7 +721,7 @@ def test_rgbled_pulse_background_nonpwm(): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') def test_rgbled_pulse_foreground(): - r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: start = time() device.pulse(0.2, 0.2, n=2, background=False) @@ -744,13 +754,13 @@ def test_rgbled_pulse_foreground(): b.assert_states_and_times(expected) def test_rgbled_pulse_foreground_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, 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 = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: device.blink(1, 0.1) sleep(0.2) @@ -760,7 +770,7 @@ def test_rgbled_blink_interrupt(): b.assert_states([0, 1, 0]) def test_rgbled_blink_interrupt_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, pwm=False) as device: device.blink(1, 0.1) sleep(0.2) @@ -770,7 +780,7 @@ def test_rgbled_blink_interrupt_nonpwm(): b.assert_states([0, 1, 0]) def test_rgbled_close(): - r, g, b = (MockPWMPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i, pin_class=MockPWMPin) for i in (1, 2, 3)) with RGBLED(r, g, b) as device: assert not device.closed device.close() @@ -779,7 +789,7 @@ def test_rgbled_close(): assert device.closed def test_rgbled_close_nonpwm(): - r, g, b = (MockPin(i) for i in (1, 2, 3)) + r, g, b = (Device._pin_factory.pin(i) for i in (1, 2, 3)) with RGBLED(r, g, b, pwm=False) as device: assert not device.closed device.close() @@ -792,8 +802,8 @@ def test_motor_missing_pins(): Motor() def test_motor_pins(): - f = MockPWMPin(1) - b = MockPWMPin(2) + f = Device._pin_factory.pin(1, pin_class=MockPWMPin) + b = Device._pin_factory.pin(2, pin_class=MockPWMPin) with Motor(f, b) as device: assert device.forward_device.pin is f assert isinstance(device.forward_device, PWMOutputDevice) @@ -801,8 +811,8 @@ def test_motor_pins(): assert isinstance(device.backward_device, PWMOutputDevice) def test_motor_pins_nonpwm(): - f = MockPin(1) - b = MockPin(2) + f = Device._pin_factory.pin(1) + b = Device._pin_factory.pin(2) with Motor(f, b, pwm=False) as device: assert device.forward_device.pin is f assert isinstance(device.forward_device, DigitalOutputDevice) @@ -810,8 +820,8 @@ def test_motor_pins_nonpwm(): assert isinstance(device.backward_device, DigitalOutputDevice) def test_motor_close(): - f = MockPWMPin(1) - b = MockPWMPin(2) + f = Device._pin_factory.pin(1, pin_class=MockPWMPin) + b = Device._pin_factory.pin(2, pin_class=MockPWMPin) with Motor(f, b) as device: device.close() assert device.closed @@ -821,8 +831,8 @@ def test_motor_close(): assert device.closed def test_motor_close_nonpwm(): - f = MockPin(1) - b = MockPin(2) + f = Device._pin_factory.pin(1) + b = Device._pin_factory.pin(2) with Motor(f, b, pwm=False) as device: device.close() assert device.closed @@ -830,8 +840,8 @@ def test_motor_close_nonpwm(): assert device.backward_device.pin is None def test_motor_value(): - f = MockPWMPin(1) - b = MockPWMPin(2) + f = Device._pin_factory.pin(1, pin_class=MockPWMPin) + b = Device._pin_factory.pin(2, pin_class=MockPWMPin) with Motor(f, b) as device: device.value = -1 assert device.is_active @@ -855,8 +865,8 @@ def test_motor_value(): assert b.state == 0 and f.state == 0 def test_motor_value_nonpwm(): - f = MockPin(1) - b = MockPin(2) + f = Device._pin_factory.pin(1) + b = Device._pin_factory.pin(2) with Motor(f, b, pwm=False) as device: device.value = -1 assert device.is_active @@ -872,17 +882,21 @@ def test_motor_value_nonpwm(): assert b.state == 0 and f.state == 0 def test_motor_bad_value(): - f = MockPWMPin(1) - b = MockPWMPin(2) + f = Device._pin_factory.pin(1, pin_class=MockPWMPin) + b = Device._pin_factory.pin(2, pin_class=MockPWMPin) with Motor(f, b) as device: with pytest.raises(ValueError): device.value = -2 with pytest.raises(ValueError): device.value = 2 + with pytest.raises(ValueError): + device.forward(2) + with pytest.raises(ValueError): + device.backward(2) def test_motor_bad_value_nonpwm(): - f = MockPin(1) - b = MockPin(2) + f = Device._pin_factory.pin(1) + b = Device._pin_factory.pin(2) with Motor(f, b, pwm=False) as device: with pytest.raises(ValueError): device.value = -2 @@ -894,8 +908,8 @@ def test_motor_bad_value_nonpwm(): device.value = -0.5 def test_motor_reverse(): - f = MockPWMPin(1) - b = MockPWMPin(2) + f = Device._pin_factory.pin(1, pin_class=MockPWMPin) + b = Device._pin_factory.pin(2, pin_class=MockPWMPin) with Motor(f, b) as device: device.forward() assert device.value == 1 @@ -911,8 +925,8 @@ def test_motor_reverse(): assert b.state == 0 and f.state == 0.5 def test_motor_reverse_nonpwm(): - f = MockPin(1) - b = MockPin(2) + f = Device._pin_factory.pin(1) + b = Device._pin_factory.pin(2) with Motor(f, b, pwm=False) as device: device.forward() assert device.value == 1 @@ -922,13 +936,13 @@ def test_motor_reverse_nonpwm(): assert b.state == 1 and f.state == 0 def test_servo_pins(): - p = MockPWMPin(1) + p = Device._pin_factory.pin(1, pin_class=MockPWMPin) with Servo(p) as device: assert device.pwm_device.pin is p assert isinstance(device.pwm_device, PWMOutputDevice) def test_servo_bad_value(): - p = MockPWMPin(1) + p = Device._pin_factory.pin(1, pin_class=MockPWMPin) with pytest.raises(ValueError): Servo(p, initial_value=2) with pytest.raises(ValueError): @@ -937,12 +951,12 @@ def test_servo_bad_value(): Servo(p, max_pulse_width=30/1000) def test_servo_pins_nonpwm(): - p = MockPin(2) + p = Device._pin_factory.pin(2) with pytest.raises(PinPWMUnsupported): Servo(p) def test_servo_close(): - p = MockPWMPin(2) + p = Device._pin_factory.pin(2, pin_class=MockPWMPin) with Servo(p) as device: device.close() assert device.closed @@ -951,7 +965,7 @@ def test_servo_close(): assert device.closed def test_servo_pulse_width(): - p = MockPWMPin(2) + p = Device._pin_factory.pin(2, pin_class=MockPWMPin) with Servo(p, 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) @@ -965,7 +979,7 @@ def test_servo_pulse_width(): assert device.pulse_width is None def test_servo_values(): - p = MockPWMPin(1) + p = Device._pin_factory.pin(1, pin_class=MockPWMPin) with Servo(p) as device: device.min() assert device.is_active @@ -992,13 +1006,13 @@ def test_servo_values(): assert device.value is None def test_angular_servo_range(): - p = MockPWMPin(1) + p = Device._pin_factory.pin(1, pin_class=MockPWMPin) with AngularServo(p, 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 = MockPWMPin(1) + p = Device._pin_factory.pin(1, pin_class=MockPWMPin) with AngularServo(p) as device: device.angle = 0 assert device.angle == 0 diff --git a/tests/test_pins_data.py b/tests/test_pins_data.py index ea43e8f..2a9af16 100644 --- a/tests/test_pins_data.py +++ b/tests/test_pins_data.py @@ -11,44 +11,44 @@ import re import pytest from mock import patch, MagicMock -import gpiozero.devices import gpiozero.pins.data -import gpiozero.pins.native -from gpiozero.pins.data import pi_info, Style, HeaderInfo, PinInfo -from gpiozero import PinMultiplePins, PinNoPins, PinUnknownPi +import gpiozero.pins.local +from gpiozero.pins.local import LocalPiFactory +from gpiozero.pins.data import Style, HeaderInfo, PinInfo +from gpiozero import * def test_pi_revision(): - save_factory = gpiozero.devices.pin_factory - try: + # We're not using _set_pin_factory here because we don't want to implicitly + # close the old instance, just replace it while we test stuff + with patch('gpiozero.devices.Device._pin_factory', LocalPiFactory()): # Can't use MockPin for this as we want something that'll actually try # and read /proc/cpuinfo (MockPin simply parrots the 2B's data); - # NativePin is used as we're guaranteed to be able to import it - gpiozero.devices.pin_factory = gpiozero.pins.native.NativePin + # LocalPiFactory is used as we can definitely instantiate it (strictly + # speaking it's abstract but we're only interested in the pi_info + # stuff) with patch('io.open') as m: m.return_value.__enter__.return_value = ['lots of irrelevant', 'lines', 'followed by', 'Revision: 0002', 'Serial: xxxxxxxxxxx'] assert pi_info().revision == '0002' - # LocalPin caches the revision (because realistically it isn't going to - # change at runtime); we need to wipe it here though - gpiozero.pins.native.NativePin._PI_REVISION = None + # LocalPiFactory caches the revision (because realistically it + # isn't going to change at runtime); we need to wipe it here though + Device._pin_factory._info = None m.return_value.__enter__.return_value = ['Revision: a21042'] assert pi_info().revision == 'a21042' # Check over-volting result (some argument over whether this is 7 or # 8 character result; make sure both work) - gpiozero.pins.native.NativePin._PI_REVISION = None + Device._pin_factory._info = None m.return_value.__enter__.return_value = ['Revision: 1000003'] assert pi_info().revision == '0003' - gpiozero.pins.native.NativePin._PI_REVISION = None + Device._pin_factory._info = None m.return_value.__enter__.return_value = ['Revision: 100003'] assert pi_info().revision == '0003' with pytest.raises(PinUnknownPi): m.return_value.__enter__.return_value = ['nothing', 'relevant', 'at all'] - gpiozero.pins.native.NativePin._PI_REVISION = None + Device._pin_factory._info = None pi_info() with pytest.raises(PinUnknownPi): pi_info('0fff') - finally: - gpiozero.devices.pin_factory = save_factory def test_pi_info(): r = pi_info('900011') @@ -73,14 +73,14 @@ def test_pi_info_other_types(): def test_physical_pins(): # Assert physical pins for some well-known Pi's; a21041 is a Pi2B - assert pi_info('a21041').physical_pins('3V3') == {('P1', 1), ('P1', 17)} - assert pi_info('a21041').physical_pins('GPIO2') == {('P1', 3)} + assert pi_info('a21041').physical_pins('3V3') == {('J8', 1), ('J8', 17)} + assert pi_info('a21041').physical_pins('GPIO2') == {('J8', 3)} assert pi_info('a21041').physical_pins('GPIO47') == set() def test_physical_pin(): with pytest.raises(PinMultiplePins): assert pi_info('a21041').physical_pin('GND') - assert pi_info('a21041').physical_pin('GPIO3') == ('P1', 5) + assert pi_info('a21041').physical_pin('GPIO3') == ('J8', 5) with pytest.raises(PinNoPins): assert pi_info('a21041').physical_pin('GPIO47') @@ -114,6 +114,18 @@ def test_pprint_content(): pi_info('0014').headers['SODIMM'].pprint(color=False) assert len(''.join(stdout.output).splitlines()) == 100 +def test_format_content(): + with patch('sys.stdout') as stdout: + stdout.output = [] + stdout.write = lambda buf: stdout.output.append(buf) + pi_info('900092').pprint(color=False) + s = ''.join(stdout.output) + assert '{0:mono}\n'.format(pi_info('900092')) == s + stdout.output = [] + pi_info('900092').pprint(color=True) + s = ''.join(stdout.output) + assert '{0:color full}\n'.format(pi_info('900092')) == s + def test_pprint_headers(): assert len(pi_info('0002').headers) == 1 assert len(pi_info('000e').headers) == 2 @@ -133,7 +145,8 @@ def test_pprint_headers(): stdout.output = [] pi_info('900092').pprint() s = ''.join(stdout.output) - assert 'P1:\n' in s + assert 'J8:\n' in s + assert 'P1:\n' not in s assert 'P5:\n' not in s def test_pprint_color(): @@ -194,11 +207,12 @@ def test_pprint_missing_pin(): assert ('(%d)' % i) def test_pprint_rows_cols(): - assert '{0:row1}'.format(pi_info('900092').headers['P1']) == '1o' - assert '{0:row2}'.format(pi_info('900092').headers['P1']) == 'oo' + assert '{0:row1}'.format(pi_info('900092').headers['J8']) == '1o' + assert '{0:row2}'.format(pi_info('900092').headers['J8']) == 'oo' assert '{0:col1}'.format(pi_info('0002').headers['P1']) == '1oooooooooooo' assert '{0:col2}'.format(pi_info('0002').headers['P1']) == 'ooooooooooooo' with pytest.raises(ValueError): '{0:row16}'.format(pi_info('0002').headers['P1']) with pytest.raises(ValueError): '{0:col3}'.format(pi_info('0002').headers['P1']) + diff --git a/tests/test_spi.py b/tests/test_spi.py index f035bde..5159109 100644 --- a/tests/test_spi.py +++ b/tests/test_spi.py @@ -4,93 +4,113 @@ from __future__ import ( print_function, division, ) +nstr = str str = type('') import sys -import mock import pytest +from array import array +from mock import patch from collections import namedtuple +from gpiozero.pins.native import NativeFactory +from gpiozero.pins.local import ( + LocalPiHardwareSPI, + LocalPiSoftwareSPI, + LocalPiHardwareSPIShared, + LocalPiSoftwareSPIShared, + ) +from gpiozero.pins.mock import MockSPIDevice from gpiozero import * -from gpiozero.pins.mock import MockPin, MockSPIDevice -from gpiozero.spi import * -def setup_function(function): - import gpiozero.devices - gpiozero.devices.pin_factory = MockPin - def teardown_function(function): - MockPin.clear_pins() + Device._pin_factory.reset() def test_spi_hardware_params(): - with mock.patch('gpiozero.spi.SpiDev') as spidev: - with SPI() as device: - assert isinstance(device, SPIHardwareInterface) - with SPI(port=0, device=0) as device: - assert isinstance(device, SPIHardwareInterface) - with SPI(port=0, device=1) as device: - assert isinstance(device, SPIHardwareInterface) - with SPI(clock_pin=11) as device: - assert isinstance(device, SPIHardwareInterface) - with SPI(clock_pin=11, mosi_pin=10, select_pin=8) as device: - assert isinstance(device, SPIHardwareInterface) - with SPI(clock_pin=11, mosi_pin=10, select_pin=7) as device: - assert isinstance(device, SPIHardwareInterface) - with SPI(shared=True) as device: - assert isinstance(device, SharedSPIHardwareInterface) - with pytest.raises(ValueError): - SPI(port=1) - with pytest.raises(ValueError): - SPI(device=2) - with pytest.raises(ValueError): - SPI(port=0, clock_pin=12) - with pytest.raises(ValueError): - SPI(foo='bar') + 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: + assert isinstance(device, LocalPiHardwareSPI) + with Device._pin_factory.spi(port=0, device=0) as device: + assert isinstance(device, LocalPiHardwareSPI) + with Device._pin_factory.spi(port=0, device=1) as device: + assert isinstance(device, LocalPiHardwareSPI) + with Device._pin_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: + assert isinstance(device, LocalPiHardwareSPI) + with Device._pin_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: + assert isinstance(device, LocalPiHardwareSPIShared) + with pytest.raises(ValueError): + Device._pin_factory.spi(port=1) + with pytest.raises(ValueError): + Device._pin_factory.spi(device=2) + with pytest.raises(ValueError): + Device._pin_factory.spi(port=0, clock_pin=12) + with pytest.raises(ValueError): + Device._pin_factory.spi(foo='bar') def test_spi_software_params(): - with mock.patch('gpiozero.spi.SpiDev') as spidev: - with SPI(select_pin=6) as device: - assert isinstance(device, SPISoftwareInterface) - with SPI(clock_pin=11, mosi_pin=9, miso_pin=10) as device: - assert isinstance(device, SPISoftwareInterface) - with SPI(select_pin=6, shared=True) as device: - assert isinstance(device, SharedSPISoftwareInterface) - # Ensure software fallback works when SpiDev isn't present - with SPI() as device: - assert isinstance(device, SPISoftwareInterface) + 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: + assert isinstance(device, LocalPiSoftwareSPI) + with Device._pin_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: + assert isinstance(device, LocalPiSoftwareSPIShared) + with patch('gpiozero.devices.Device._pin_factory', NativeFactory()): + # 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() + # Ensure software fallback works when SpiDev isn't present + with Device._pin_factory.spi() as device: + assert isinstance(device, LocalPiSoftwareSPI) def test_spi_hardware_conflict(): - with mock.patch('gpiozero.spi.SpiDev') as spidev: + with patch('gpiozero.pins.local.SpiDev') as spidev: with LED(11) as led: with pytest.raises(GPIOPinInUse): - SPI(port=0, device=0) + Device._pin_factory.spi(port=0, device=0) + with patch('gpiozero.pins.local.SpiDev') as spidev: + with Device._pin_factory.spi(port=0, device=0) as spi: + with pytest.raises(GPIOPinInUse): + LED(11) def test_spi_hardware_read(): - with mock.patch('gpiozero.spi.SpiDev') as spidev: + with patch('gpiozero.pins.local.SpiDev') as spidev: spidev.return_value.xfer2.side_effect = lambda data: list(range(10))[:len(data)] - with SPI() as device: + with Device._pin_factory.spi() as device: assert device.read(3) == [0, 1, 2] assert device.read(6) == list(range(6)) def test_spi_hardware_write(): - with mock.patch('gpiozero.spi.SpiDev') as spidev: + with patch('gpiozero.pins.local.SpiDev') as spidev: spidev.return_value.xfer2.side_effect = lambda data: list(range(10))[:len(data)] - with SPI() as device: + with Device._pin_factory.spi() as device: assert device.write([0, 1, 2]) == 3 assert spidev.return_value.xfer2.called_with([0, 1, 2]) assert device.write(list(range(6))) == 6 assert spidev.return_value.xfer2.called_with(list(range(6))) def test_spi_hardware_modes(): - with mock.patch('gpiozero.spi.SpiDev') as spidev: + with patch('gpiozero.pins.local.SpiDev') as spidev: spidev.return_value.mode = 0 spidev.return_value.lsbfirst = False spidev.return_value.cshigh = True spidev.return_value.bits_per_word = 8 - with SPI() as device: + with Device._pin_factory.spi() as device: assert device.clock_mode == 0 assert not device.clock_polarity assert not device.clock_phase @@ -116,7 +136,7 @@ def test_spi_software_read(): super(SPISlave, self).on_start() for i in range(10): self.tx_word(i) - with SPISlave(11, 10, 9, 8) as slave, SPI() as master: + with SPISlave(11, 10, 9, 8) as slave, Device._pin_factory.spi() as master: assert master.read(3) == [0, 1, 2] assert master.read(6) == [0, 1, 2, 3, 4, 5] slave.clock_phase = True @@ -125,7 +145,7 @@ def test_spi_software_read(): assert master.read(6) == [0, 1, 2, 3, 4, 5] def test_spi_software_write(): - with MockSPIDevice(11, 10, 9, 8) as test_device, SPI() as master: + with MockSPIDevice(11, 10, 9, 8) as test_device, Device._pin_factory.spi() as master: master.write([0]) assert test_device.rx_word() == 0 master.write([2, 0]) @@ -134,7 +154,7 @@ def test_spi_software_write(): assert test_device.rx_word() == 257 def test_spi_software_clock_mode(): - with SPI() as master: + with Device._pin_factory.spi() as master: assert master.clock_mode == 0 assert not master.clock_polarity assert not master.clock_phase @@ -151,7 +171,7 @@ def test_spi_software_clock_mode(): master.clock_mode = 5 def test_spi_software_attr(): - with SPI() as master: + with Device._pin_factory.spi() as master: assert not master.lsb_first assert not master.select_high assert master.bits_per_word == 8 diff --git a/tests/test_spi_devices.py b/tests/test_spi_devices.py index ecf0ca8..6c876ad 100644 --- a/tests/test_spi_devices.py +++ b/tests/test_spi_devices.py @@ -15,16 +15,12 @@ try: except ImportError: from gpiozero.compat import isclose -from gpiozero import * from gpiozero.pins.mock import MockSPIDevice, MockPin +from gpiozero import * -def setup_function(function): - import gpiozero.devices - gpiozero.devices.pin_factory = MockPin - def teardown_function(function): - MockPin.clear_pins() + Device._pin_factory.reset() def clamp(v, min_value, max_value): return min(max_value, max(min_value, v))