diff --git a/docs/api_pins.rst b/docs/api_pins.rst new file mode 100644 index 0000000..4167de6 --- /dev/null +++ b/docs/api_pins.rst @@ -0,0 +1,84 @@ +==== +Pins +==== + +.. currentmodule:: gpiozero + +As of release 1.1, the GPIO Zero library can be roughly divided into two +things: pins and the devices that are connected to them. The majority of the +documentation focuses on devices as pins are below the level that most users +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): + +1. :class:`gpiozero.pins.rpigpio.RPiGPIOPin` + +2. :class:`gpiozero.pins.rpio.RPIOPin` + +3. :class:`gpiozero.pins.native.NativePin` + +You can change the default pin implementation by over-writing the +``DefaultPin`` global in devices like so:: + + from gpiozero.pins.native import NativePin + import gpiozero.devices + # Force the default pin implementation to be NativePin + gpiozero.devices.DefaultPin = NativePin + + from gpiozero import LED + + # This will now use NativePin instead of RPiGPIOPin + led = LED(16) + +In future, this separation should allow 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! + + +Abstract Pin +============ + +.. autoclass:: Pin + :members: + + +RPiGPIOPin +========== + +.. currentmodule:: gpiozero.pins.rpigpio + +.. autoclass:: RPiGPIOPin + + +RPIOPin +======= + +.. currentmodule:: gpiozero.pins.rpio + +.. autoclass:: RPIOPin + + +NativePin +========= + +.. currentmodule:: gpiozero.pins.native + +.. autoclass:: NativePin + diff --git a/docs/conf.py b/docs/conf.py index d8c655f..73b3a4b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,6 +37,8 @@ class Mock(object): sys.modules['RPi'] = Mock() sys.modules['RPi.GPIO'] = sys.modules['RPi'].GPIO +sys.modules['RPIO'] = Mock() +sys.modules['RPIO.PWM'] = sys.modules['RPIO'].PWM sys.modules['w1thermsensor'] = Mock() sys.modules['spidev'] = Mock() diff --git a/docs/index.rst b/docs/index.rst index 71ffc0a..ec082e6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,5 +12,6 @@ Table of Contents api_output api_boards api_generic + api_pins changelog license diff --git a/docs/recipes.rst b/docs/recipes.rst index dbaa69a..e1f0a12 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -281,7 +281,7 @@ Each button plays a different sound! buttons = [Button(pin) for pin in sound_pins] for button in buttons: - sound = sound_pins[button.pin] + sound = sound_pins[button.pin.number] button.when_pressed = sound.play pause() diff --git a/gpiozero/__init__.py b/gpiozero/__init__.py index 19b3d73..e78ade6 100644 --- a/gpiozero/__init__.py +++ b/gpiozero/__init__.py @@ -5,6 +5,23 @@ from __future__ import ( division, ) +from .pins.exc import ( + PinError, + PinFixedFunction, + PinInvalidFunction, + PinInvalidState, + PinInvalidPull, + PinInvalidEdges, + PinSetInput, + PinFixedPull, + PinEdgeDetectUnsupported, + PinPWMError, + PinPWMUnsupported, + PinPWMFixedValue, +) +from .pins import ( + Pin, +) from .exc import ( GPIODeviceClosed, GPIODeviceError, diff --git a/gpiozero/devices.py b/gpiozero/devices.py index 47856b2..8153912 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -13,29 +13,45 @@ from threading import Thread, Event, RLock from collections import deque from types import FunctionType -from RPi import GPIO - from .exc import GPIODeviceError, GPIODeviceClosed, InputDeviceError -_GPIO_THREADS = set() -_GPIO_PINS = set() +# Get a pin implementation to use as the default; we prefer RPi.GPIO's 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 +from .pins import PINS_CLEANUP +try: + from .pins.rpigpio import RPiGPIOPin + DefaultPin = RPiGPIOPin +except ImportError: + try: + from .pins.rpio import RPIOPin + DefaultPin = RPIOPin + except ImportError: + from .pins.native import NativePin + DefaultPin = NativePin + + +_THREADS = set() +_PINS = set() # Due to interactions between RPi.GPIO cleanup and the GPIODevice.close() # method the same thread may attempt to acquire this lock, leading to deadlock # unless the lock is re-entrant -_GPIO_PINS_LOCK = RLock() +_PINS_LOCK = RLock() -def _gpio_threads_shutdown(): - while _GPIO_THREADS: - for t in _GPIO_THREADS.copy(): +def _shutdown(): + while _THREADS: + for t in _THREADS.copy(): t.stop() - with _GPIO_PINS_LOCK: - while _GPIO_PINS: - GPIO.remove_event_detect(_GPIO_PINS.pop()) - GPIO.cleanup() + 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 + for routine in PINS_CLEANUP: + routine() -atexit.register(_gpio_threads_shutdown) -GPIO.setmode(GPIO.BCM) -GPIO.setwarnings(False) +atexit.register(_shutdown) class GPIOMeta(type): @@ -196,20 +212,22 @@ class GPIODevice(ValuesMixin, GPIOBase): # value of pin until we've verified that it isn't already allocated self._pin = None if pin is None: - raise GPIODeviceError('No GPIO pin number given') - with _GPIO_PINS_LOCK: - if pin in _GPIO_PINS: + raise GPIODeviceError('No pin given') + if isinstance(pin, int): + pin = DefaultPin(pin) + with _PINS_LOCK: + if pin in _PINS: raise GPIODeviceError( - 'pin %d is already in use by another gpiozero object' % pin + 'pin %r is already in use by another gpiozero object' % pin ) - _GPIO_PINS.add(pin) + _PINS.add(pin) self._pin = pin - self._active_state = GPIO.HIGH - self._inactive_state = GPIO.LOW + self._active_state = True + self._inactive_state = False def _read(self): try: - return GPIO.input(self.pin) == self._active_state + return self.pin.state == self._active_state except TypeError: self._check_open() raise @@ -260,13 +278,12 @@ class GPIODevice(ValuesMixin, GPIOBase): ... """ super(GPIODevice, self).close() - with _GPIO_PINS_LOCK: + with _PINS_LOCK: pin = self._pin self._pin = None - if pin in _GPIO_PINS: - _GPIO_PINS.remove(pin) - GPIO.remove_event_detect(pin) - GPIO.cleanup(pin) + if pin in _PINS: + _PINS.remove(pin) + pin.close() @property def closed(self): @@ -275,9 +292,10 @@ class GPIODevice(ValuesMixin, GPIOBase): @property def pin(self): """ - The pin (in BCM numbering) that the device is connected to. This will - be ``None`` if the device has been closed (see the :meth:`close` - method). + The :class:`Pin` that the device is connected to. This will be ``None`` + if the device has been closed (see the :meth:`close` method). When + dealing with GPIO pins, query ``pin.number`` to discover the GPIO + pin (in BCM numbering) that the device is connected to. """ return self._pin @@ -293,7 +311,7 @@ class GPIODevice(ValuesMixin, GPIOBase): def __repr__(self): try: - return "" % ( + return "" % ( self.__class__.__name__, self.pin, self.is_active) except GPIODeviceClosed: return "" % self.__class__.__name__ @@ -307,7 +325,7 @@ class GPIOThread(Thread): def start(self): self.stopping.clear() - _GPIO_THREADS.add(self) + _THREADS.add(self) super(GPIOThread, self).start() def stop(self): @@ -316,7 +334,7 @@ class GPIOThread(Thread): def join(self): super(GPIOThread, self).join() - _GPIO_THREADS.discard(self) + _THREADS.discard(self) class GPIOQueue(GPIOThread): diff --git a/gpiozero/input_devices.py b/gpiozero/input_devices.py index 167b7f1..01891cc 100644 --- a/gpiozero/input_devices.py +++ b/gpiozero/input_devices.py @@ -13,7 +13,6 @@ from functools import wraps from time import sleep, time from threading import Event -from RPi import GPIO from spidev import SpiDev from .exc import InputDeviceError, GPIODeviceError, GPIODeviceClosed @@ -39,39 +38,18 @@ class InputDevice(GPIODevice): ``False`` (the default), the pin will be pulled low. """ def __init__(self, pin=None, pull_up=False): - if pin in (2, 3) and not pull_up: - raise InputDeviceError( - 'GPIO pins 2 and 3 are fitted with physical pull up ' - 'resistors; you cannot initialize them with pull_up=False' - ) - # _pull_up should be assigned first as __repr__ relies upon it to - # support the case where __repr__ is called during debugging of an - # instance that has failed to initialize (due to an exception in the - # super-class __init__) - self._pull_up = pull_up super(InputDevice, self).__init__(pin) - self._active_edge = GPIO.FALLING if pull_up else GPIO.RISING - self._inactive_edge = GPIO.RISING if pull_up else GPIO.FALLING - self._active_state = GPIO.LOW if pull_up else GPIO.HIGH - self._inactive_state = GPIO.HIGH if pull_up else GPIO.LOW - pull = GPIO.PUD_UP if pull_up else GPIO.PUD_DOWN - try: - # NOTE: catch_warnings isn't thread-safe but hopefully no-one's - # messing around with GPIO init within background threads... - with warnings.catch_warnings(record=True) as w: - GPIO.setup(pin, GPIO.IN, pull) - # The only warning we want to squash is a RuntimeWarning that is - # thrown when setting pins 2 or 3. Anything else should be replayed - for warning in w: - if warning.category != RuntimeWarning or pin not in (2, 3): - warnings.showwarning( - warning.message, warning.category, warning.filename, - warning.lineno, warning.file, warning.line - ) + if self.pin.function != 'input': + self.pin.function = 'input' + pull = 'up' if pull_up else 'down' + if self.pin.pull != pull: + self.pin.pull = pull except: self.close() raise + self._active_state = False if pull_up else True + self._inactive_state = True if pull_up else False @property def pull_up(self): @@ -79,11 +57,11 @@ class InputDevice(GPIODevice): If ``True``, the device uses a pull-up resistor to set the GPIO pin "high" by default. Defaults to ``False``. """ - return self._pull_up + return self.pin.pull == 'up' def __repr__(self): try: - return "" % ( + return "" % ( self.__class__.__name__, self.pin, self.pull_up, self.is_active) except: return super(InputDevice, self).__repr__() @@ -245,20 +223,15 @@ class DigitalInputDevice(WaitableInputDevice): def __init__(self, pin=None, pull_up=False, bounce_time=None): super(DigitalInputDevice, self).__init__(pin, pull_up) try: - # Yes, that's really the default bouncetime in RPi.GPIO... - GPIO.add_event_detect( - self.pin, GPIO.BOTH, callback=self._fire_events, - bouncetime=-666 if bounce_time is None else int(bounce_time * 1000) - ) + self.pin.bounce = bounce_time + self.pin.edges = 'both' + self.pin.when_changed = self._fire_events # Call _fire_events once to set initial state of events - super(DigitalInputDevice, self)._fire_events() + self._fire_events() except: self.close() raise - def _fire_events(self, channel): - super(DigitalInputDevice, self)._fire_events() - class SmoothedInputDevice(WaitableInputDevice): """ @@ -565,9 +538,9 @@ class LightSensor(SmoothedInputDevice): try: self._charge_time_limit = charge_time_limit self._charged = Event() - GPIO.add_event_detect( - self.pin, GPIO.RISING, lambda channel: self._charged.set() - ) + self.pin.edges = 'rising' + self.pin.bounce = None + self.pin.when_changed = self._charged.set self._queue.start() except: self.close() @@ -579,13 +552,13 @@ class LightSensor(SmoothedInputDevice): def _read(self): # Drain charge from the capacitor - GPIO.setup(self.pin, GPIO.OUT) - GPIO.output(self.pin, GPIO.LOW) + self.pin.function = 'output' + self.pin.state = False sleep(0.1) # Time the charging of the capacitor start = time() self._charged.clear() - GPIO.setup(self.pin, GPIO.IN) + self.pin.function = 'input' self._charged.wait(self.charge_time_limit) return ( 1.0 - min(self.charge_time_limit, time() - start) / diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index 208dca7..088cc30 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -10,8 +10,6 @@ from time import sleep from threading import Lock from itertools import repeat, cycle, chain -from RPi import GPIO - from .exc import OutputDeviceError, GPIODeviceError, GPIODeviceClosed from .devices import GPIODevice, GPIOThread, CompositeDevice, SourceMixin @@ -42,36 +40,20 @@ class OutputDevice(SourceMixin, GPIODevice): def __init__(self, pin=None, active_high=True, initial_value=False): self._active_high = active_high super(OutputDevice, self).__init__(pin) - self._active_state = GPIO.HIGH if active_high else GPIO.LOW - self._inactive_state = GPIO.LOW if active_high else GPIO.HIGH - try: - # NOTE: catch_warnings isn't thread-safe but hopefully no-one's - # messing around with GPIO init within background threads... - with warnings.catch_warnings(record=True) as w: - # This is horrid, but we can't specify initial=None with setup - if initial_value is None: - GPIO.setup(pin, GPIO.OUT) - else: - GPIO.setup(pin, GPIO.OUT, initial= - [self._inactive_state, self._active_state][bool(initial_value)]) - GPIO.setup(pin, GPIO.OUT) - # The only warning we want to squash is a RuntimeWarning that is - # thrown when setting pins 2 or 3. Anything else should be replayed - for warning in w: - if warning.category != RuntimeWarning or pin not in (2, 3): - warnings.showwarning( - warning.message, warning.category, warning.filename, - warning.lineno, warning.file, warning.line - ) - except: - self.close() - raise + self._active_state = True if active_high else False + self._inactive_state = False if active_high else True + if initial_value is None: + self.pin.function = 'output' + elif initial_value: + self.pin.output_with_state(self._active_state) + else: + self.pin.output_with_state(self._inactive_state) def _write(self, value): if not self.active_high: value = not value try: - GPIO.output(self.pin, bool(value)) + self.pin.state = bool(value) except ValueError: self._check_open() raise @@ -102,7 +84,7 @@ class OutputDevice(SourceMixin, GPIODevice): def __repr__(self): try: - return '' % ( + return '' % ( self.__class__.__name__, self.pin, self.active_high, self.is_active) except: return super(OutputDevice, self).__repr__() @@ -285,40 +267,33 @@ class PWMOutputDevice(OutputDevice): to 100Hz. """ def __init__(self, pin=None, active_high=True, initial_value=0, frequency=100): - self._pwm = None self._blink_thread = None if not 0 <= initial_value <= 1: raise OutputDeviceError("initial_value must be between 0 and 1") super(PWMOutputDevice, self).__init__(pin, active_high) try: - self._pwm = GPIO.PWM(self.pin, frequency) - self._value = initial_value - if not active_high: - initial_value = 1 - initial_value - self._pwm.start(100 * initial_value) - self._frequency = frequency + # XXX need a way of setting these together + self.pin.frequency = frequency + self.value = initial_value except: self.close() raise def close(self): self._stop_blink() - if self._pwm: - # Ensure we wipe out the PWM object so that re-runs don't attempt - # to re-stop the PWM thread (otherwise, the fact that close is - # called from __del__ can easily result in us stopping the PWM - # on *another* instance on the same pin) - p = self._pwm - self._pwm = None - p.stop() + try: + self.pin.frequency = None + except AttributeError: + # If the pin's already None, ignore the exception + pass super(PWMOutputDevice, self).close() def _read(self): self._check_open() if self.active_high: - return self._value + return self.pin.state else: - return 1 - self._value + return 1 - self.pin.state def _write(self, value): if not self.active_high: @@ -326,11 +301,10 @@ class PWMOutputDevice(OutputDevice): if not 0 <= value <= 1: raise OutputDeviceError("PWM value must be between 0 and 1") try: - self._pwm.ChangeDutyCycle(value * 100) + self.pin.state = value except AttributeError: self._check_open() raise - self._value = value @property def value(self): @@ -361,7 +335,7 @@ class PWMOutputDevice(OutputDevice): toggle it to 0.9, and so on. """ self._stop_blink() - self.value = 1.0 - self.value + self.value = 1 - self.value @property def is_active(self): @@ -377,12 +351,11 @@ class PWMOutputDevice(OutputDevice): The frequency of the pulses used with the PWM device, in Hz. The default is 100Hz. """ - return self._frequency + return self.pin.frequency @frequency.setter def frequency(self, value): - self._pwm.ChangeFrequency(value) - self._frequency = value + self.pin.frequency = value def blink( self, on_time=1, off_time=1, fade_in_time=0, fade_out_time=0, diff --git a/gpiozero/pins/__init__.py b/gpiozero/pins/__init__.py new file mode 100644 index 0000000..df7f860 --- /dev/null +++ b/gpiozero/pins/__init__.py @@ -0,0 +1,241 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + +from .exc import ( + PinFixedFunction, + PinSetInput, + PinFixedPull, + PinPWMUnsupported, + PinEdgeDetectUnsupported, + ) + + +PINS_CLEANUP = [] + + +class Pin(object): + """ + Abstract base class representing a GPIO pin or a pin from an IO extender. + + Descendents should override property getters and setters to accurately + represent the capabilities of pins. The following functions *must* be + overridden: + + * :meth:`_get_function` + * :meth:`_get_state` + + The following functions *may* be overridden if applicable: + + * :meth:`close` + * :meth:`_set_function` + * :meth:`_set_state` + * :meth:`_get_frequency` + * :meth:`_set_frequency` + * :meth:`_get_pull` + * :meth:`_set_pull` + * :meth:`_get_bounce` + * :meth:`_set_bounce` + * :meth:`_get_edges` + * :meth:`_set_edges` + * :meth:`_get_when_changed` + * :meth:`_set_when_changed` + * :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" + + def close(self): + """ + Cleans up the resources allocated to the pin. After this method is + called, this :class:`Pin` instance may no longer be used to query or + control the pin's state. + """ + pass + + def output_with_state(self, state): + """ + Sets the pin's function to "output" and specifies an initial state + for the pin. By default this is equivalent to performing:: + + pin.function = 'output' + pin.state = state + + However, descendents may override this in order to provide the smallest + possible delay between configuring the pin for output and specifying an + initial value (which can be important for avoiding "blips" in + active-low configurations). + """ + self.function = 'output' + self.state = state + + def input_with_pull(self, pull): + """ + Sets the pin's function to "input" and specifies an initial pull-up + for the pin. By default this is equivalent to performing:: + + pin.function = 'input' + pin.pull = pull + + However, descendents may override this order to provide the smallest + possible delay between configuring the pin for input and pulling the + pin up/down (which can be important for avoiding "blips" in some + configurations). + """ + self.function = 'input' + self.pull = pull + + def _get_function(self): + return "input" + + def _set_function(self, value): + raise PinFixedFunction("Cannot set the function of pin %r" % self) + + function = property( + lambda self: self._get_function(), + lambda self, value: self._set_function(value), + doc="""\ + The function of the pin. This property is a string indicating the + current function or purpose of the pin. Typically this is the string + "input" or "output". However, in some circumstances it can be other + strings indicating non-GPIO related functionality. + + With certain pin types (e.g. GPIO pins), this attribute can be changed + to configure the function of a pin. If an invalid function is + specified, for this attribute, :exc:`PinInvalidFunction` will be + raised. If this pin is fixed function and an attempt is made to set + this attribute, :exc:`PinFixedFunction` will be raised. + """) + + def _get_state(self): + return 0 + + def _set_state(self, value): + raise PinSetInput("Cannot set the state of input pin %r" % self) + + state = property( + lambda self: self._get_state(), + lambda self, value: self._set_state(value), + 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). + + 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. + + 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 + value is specified for this attribute, :exc:`PinInvalidState` will be + raised. + """) + + def _get_pull(self): + return 'floating' + + def _set_pull(self, value): + raise PinFixedPull("Cannot change pull-up on pin %r" % self) + + pull = property( + lambda self: self._get_pull(), + lambda self, value: self._set_pull(value), + doc="""\ + The pull-up state of the pin represented as a string. This is typically + one of the strings "up", "down", or "floating" but additional values + may be supported by the underlying hardware. + + If the pin does not support changing pull-up state (for example because + of a fixed pull-up resistor), attempts to set this property will raise + :exc:`PinFixedPull`. If the specified value is not supported by the + underlying hardware, :exc:`PinInvalidPull` is raised. + """) + + def _get_frequency(self): + return None + + def _set_frequency(self, value): + if value is not None: + raise PinPWMUnsupported("PWM is not supported on pin %r" % self) + + frequency = property( + lambda self: self._get_frequency(), + lambda self, value: self._set_frequency(value), + doc="""\ + The frequency (in Hz) for the pin's PWM implementation, or ``None`` if + PWM is not currently in use. This value always defaults to ``None`` and + may be changed with certain pin types to activate or deactivate PWM. + + If the pin does not support PWM, :exc:`PinPWMUnsupported` will be + raised when attempting to set this to a value other than ``None``. + """) + + def _get_bounce(self): + return None + + def _set_bounce(self, value): + if value is not None: + raise PinEdgeDetectUnsupported("Edge detection is not supported on pin %r" % self) + + bounce = property( + lambda self: self._get_bounce(), + lambda self, value: self._set_bounce(value), + doc="""\ + The amount of bounce detection (elimination) currently in use by edge + detection, measured in seconds. If bounce detection is not currently in + use, this is ``None``. + + 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, + even if only in software. + """) + + def _get_edges(self): + return 'both' + + def _set_edges(self, value): + raise PinEdgeDetectUnsupported("Edge detection is not supported on pin %r" % self) + + edges = property( + lambda self: self._get_edges(), + lambda self, value: self._set_edges(value), + 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", or "falling". + + If the pin does not support edge detection, attempts to set this + property will raise :exc:`PinEdgeDetectUnsupported`. + """) + + def _get_when_changed(self): + return None + + def _set_when_changed(self, value): + raise PinEdgeDetectUnsupported("Edge detection is not supported on pin %r" % self) + + when_changed = property( + lambda self: self._get_when_changed(), + lambda self, value: self._set_when_changed(value), + doc="""\ + A function or bound method to be called when the pin's state changes + (more specifically when the edge specified by :attr:`edges` is detected + on the pin). The function or bound method must take no parameters. + + If the pin does not support edge detection, attempts to set this + property will raise :exc:`PinEdgeDetectUnsupported`. + """) + diff --git a/gpiozero/pins/exc.py b/gpiozero/pins/exc.py new file mode 100644 index 0000000..5e27aba --- /dev/null +++ b/gpiozero/pins/exc.py @@ -0,0 +1,45 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +class PinError(Exception): + "Base class for errors related to pin implementations" + +class PinFixedFunction(PinError, AttributeError): + "Error raised when attempting to change the function of a fixed type pin" + +class PinInvalidFunction(PinError, ValueError): + "Error raised when attempting to change the function of a pin to an invalid value" + +class PinInvalidState(PinError, ValueError): + "Error raised when attempting to assign an invalid state to a pin" + +class PinInvalidPull(PinError, ValueError): + "Error raised when attempting to assign an invalid pull-up to a pin" + +class PinInvalidEdges(PinError, ValueError): + "Error raised when attempting to assign an invalid edge detection to a pin" + +class PinSetInput(PinError, AttributeError): + "Error raised when attempting to set a read-only pin" + +class PinFixedPull(PinError, AttributeError): + "Error raised when attempting to set the pull of a pin with fixed pull-up" + +class PinEdgeDetectUnsupported(PinError, AttributeError): + "Error raised when attempting to use edge detection on unsupported pins" + +class PinPWMError(PinError): + "Base class for errors related to PWM implementations" + +class PinPWMUnsupported(PinPWMError, AttributeError): + "Error raised when attempting to activate PWM on unsupported pins" + +class PinPWMFixedValue(PinPWMError, AttributeError): + "Error raised when attempting to initialize PWM on an input pin" + diff --git a/gpiozero/pins/native.py b/gpiozero/pins/native.py new file mode 100644 index 0000000..6bd6da2 --- /dev/null +++ b/gpiozero/pins/native.py @@ -0,0 +1,336 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +nstr = str +str = type('') + +import io +import os +import mmap +import errno +import struct +from time import sleep +from threading import Thread, Event, Lock +from collections import Counter + +from . import Pin, PINS_CLEANUP +from .exc import ( + PinInvalidPull, + PinInvalidEdges, + PinInvalidFunction, + PinFixedPull, + ) + + +class GPIOMemory(object): + + GPIO_BASE_OFFSET = 0x200000 + PERI_BASE_OFFSET = { + 'BCM2708': 0x20000000, + 'BCM2835': 0x20000000, + 'BCM2709': 0x3f000000, + 'BCM2836': 0x3f000000, + } + + # From BCM2835 data-sheet, p.91 + GPFSEL_OFFSET = 0x00 >> 2 + GPSET_OFFSET = 0x1c >> 2 + GPCLR_OFFSET = 0x28 >> 2 + GPLEV_OFFSET = 0x34 >> 2 + GPEDS_OFFSET = 0x40 >> 2 + GPREN_OFFSET = 0x4c >> 2 + GPFEN_OFFSET = 0x58 >> 2 + GPHEN_OFFSET = 0x64 >> 2 + GPLEN_OFFSET = 0x70 >> 2 + GPAREN_OFFSET = 0x7c >> 2 + GPAFEN_OFFSET = 0x88 >> 2 + GPPUD_OFFSET = 0x94 >> 2 + GPPUDCLK_OFFSET = 0x98 >> 2 + + def __init__(self): + try: + self.fd = os.open('/dev/gpiomem', os.O_RDWR | os.O_SYNC) + except OSError: + try: + self.fd = os.open('/dev/mem', os.O_RDWR | os.O_SYNC) + except OSError: + raise IOError( + 'unable to open /dev/gpiomem or /dev/mem; ' + 'upgrade your kernel or run as root') + else: + offset = self.peripheral_base() + self.GPIO_BASE_OFFSET + else: + offset = 0 + self.mem = mmap.mmap(self.fd, 4096, offset=offset) + + def close(self): + self.mem.close() + os.close(self.fd) + + def peripheral_base(self): + try: + with io.open('/proc/device-tree/soc/ranges', 'rb') as f: + f.seek(4) + return struct.unpack(nstr('>L'), f.read(4))[0] + except IOError: + with io.open('/proc/cpuinfo', 'r') as f: + for line in f: + if line.startswith('Hardware'): + try: + return self.PERI_BASE_OFFSET[line.split(':')[1].strip()] + except KeyError: + raise IOError('unable to determine RPi revision') + raise IOError('unable to determine peripheral base') + + def __getitem__(self, index): + return struct.unpack_from(nstr('> self._func_shift) & 7] + + def _set_function(self, value): + try: + value = self.FUNCTION_VALUES[value] + except KeyError: + raise PinInvalidFunction('invalid function "%s" for pin %r' % self) + self._MEM[self._func_offset] = ( + self._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)) + + def _set_state(self, value): + if value: + self._MEM[self._set_offset] = 1 << self._set_shift + else: + self._MEM[self._clear_offset] = 1 << self._clear_shift + + def _get_pull(self): + return self.PULL_NAMES[self._pull] + + 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._number in (2, 3): + raise PinFixedPull('%r has a physical pull-up resistor' % self) + try: + value = self.PULL_VALUES[value] + except KeyError: + raise PinInvalidPull('invalid pull direction "%s" for pin %r' % self) + self._MEM[self._MEM.GPPUD_OFFSET] = value + sleep(0.000000214) + self._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._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)) + return self.EDGE_NAMES[(rising, falling)] + + def _set_edges(self, value): + try: + rising, falling = self.EDGE_VALUES[value] + except KeyError: + raise PinInvalidEdges('invalid edge specification "%s" for pin %r' % self) + f = self.when_changed + self.when_changed = None + try: + self._MEM[self._rising_offset] = ( + self._MEM[self._rising_offset] + & ~(1 << self._rising_shift) + | (rising << self._rising_shift) + ) + self._MEM[self._falling_offset] = ( + self._MEM[self._falling_offset] + & ~(1 << self._falling_shift) + | (falling << self._falling_shift) + ) + finally: + self.when_changed = f + + def _get_when_changed(self): + return self._when_changed + + def _set_when_changed(self, value): + if self._when_changed is None and value is not None: + self._when_changed = value + self._change_thread = Thread(target=self._change_watch) + self._change_thread.daemon = True + self._change_event.clear() + self._change_thread.start() + elif self._when_changed is not None and value is None: + self._change_event.set() + self._change_thread.join() + self._change_thread = None + self._when_changed = None + else: + self._when_changed = value + + def _change_watch(self): + offset = self._edge_offset + mask = 1 << self._edge_shift + self._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 + self._when_changed() + diff --git a/gpiozero/pins/rpigpio.py b/gpiozero/pins/rpigpio.py new file mode 100644 index 0000000..cf6edfb --- /dev/null +++ b/gpiozero/pins/rpigpio.py @@ -0,0 +1,205 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + +from RPi import GPIO + +from . import Pin +from .exc import ( + PinInvalidFunction, + PinSetInput, + PinFixedPull, + ) + + +class RPiGPIOPin(Pin): + """ + 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. + Supports all features including PWM (via software). + + .. _RPi.GPIO: https://pypi.python.org/pypi/RPi.GPIO + """ + + _PINS = {} + + GPIO_FUNCTIONS = { + 'input': GPIO.IN, + 'output': GPIO.OUT, + 'i2c': GPIO.I2C, + 'spi': GPIO.SPI, + 'pwm': GPIO.PWM, + 'serial': GPIO.SERIAL, + 'unknown': GPIO.UNKNOWN, + } + + GPIO_PULL_UPS = { + 'up': GPIO.PUD_UP, + 'down': GPIO.PUD_DOWN, + 'floating': GPIO.PUD_OFF, + } + + GPIO_EDGES = { + 'both': GPIO.BOTH, + 'rising': GPIO.RISING, + 'falling': GPIO.FALLING, + } + + 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()} + GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()} + + def __new__(cls, number): + if not cls._PINS: + GPIO.setmode(GPIO.BCM) + GPIO.setwarnings(False) + try: + return cls._PINS[number] + except KeyError: + self = super(RPiGPIOPin, cls).__new__(cls) + cls._PINS[number] = self + self._number = number + self._pull = 'up' if number in (2, 3) 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]) + return self + + def __repr__(self): + return "GPIO%d" % self._number + + @property + def number(self): + return self._number + + def close(self): + self.frequency = None + self.when_changed = None + GPIO.cleanup(self._number) + + def output_with_state(self, state): + self._pull = 'floating' + GPIO.setup(self._number, GPIO.OUT, initial=state) + + def input_with_pull(self, pull): + if pull != 'up' and self._number in (2, 3): + raise PinFixedPull('%r has a physical pull-up resistor' % self) + try: + 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)] + + def _set_function(self, value): + if value != 'input': + self._pull = 'floating' + try: + GPIO.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)) + + def _get_state(self): + if self._pwm: + return self._duty_cycle + else: + return GPIO.input(self._number) + + def _set_state(self, value): + if self._pwm: + try: + self._pwm.ChangeDutyCycle(value * 100) + except ValueError: + raise PinInvalidValue('invalid state "%s" for pin %r' % (value, self)) + self._duty_cycle = value + else: + try: + GPIO.output(self._number, value) + except ValueError: + raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) + except RuntimeError: + raise PinSetInput('cannot set state of pin %r' % self) + + def _get_pull(self): + return self._pull + + 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._number in (2, 3): + raise PinFixedPull('%r has a physical pull-up resistor' % self) + try: + 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)) + + def _get_frequency(self): + return self._frequency + + def _set_frequency(self, value): + if self._frequency is None and value is not None: + try: + self._pwm = GPIO.PWM(self._number, value) + except RuntimeError: + raise PinPWMFixedValue('cannot start PWM on pin %r' % self) + self._pwm.start(0) + self._duty_cycle = 0 + self._frequency = value + elif self._frequency is not None and value is not None: + self._pwm.ChangeFrequency(value) + self._frequency = value + elif self._frequency is not None and value is None: + self._pwm.stop() + self._pwm = None + self._duty_cycle = None + self._frequency = None + + def _get_bounce(self): + return None if self._bounce == -666 else (self._bounce / 1000) + + def _set_bounce(self, value): + f = self.when_changed + self.when_changed = None + try: + self._bounce = -666 if value is None else (value * 1000) + finally: + self.when_changed = f + + def _get_edges(self): + return self.GPIO_EDGES_NAMES[self._edges] + + def _set_edges(self, value): + f = self.when_changed + self.when_changed = None + try: + self._edges = self.GPIO_EDGES[value] + finally: + self.when_changed = f + + def _get_when_changed(self): + return self._when_changed + + def _set_when_changed(self, value): + if self._when_changed is None and value is not None: + self._when_changed = value + GPIO.add_event_detect( + 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) + self._when_changed = None + else: + self._when_changed = value + diff --git a/gpiozero/pins/rpio.py b/gpiozero/pins/rpio.py new file mode 100644 index 0000000..6e00c72 --- /dev/null +++ b/gpiozero/pins/rpio.py @@ -0,0 +1,204 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + + +from threading import Lock + +import RPIO +import RPIO.PWM + +from . import Pin, PINS_CLEANUP +from .exc import ( + PinInvalidFunction, + PinSetInput, + PinFixedPull, + ) + + +class RPIOPin(Pin): + """ + 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, + but RPIO is. Supports all features including PWM (hardware via DMA). + + .. note:: + + Please note that at the time of writing, RPIO is only compatible with + Pi 1's; the Raspberry Pi 2 Model B is *not* supported. Also note that + root access is required so scripts must typically be run with ``sudo``. + + .. _RPIO: https://pythonhosted.org/RPIO/ + """ + + _PINS = {} + + GPIO_FUNCTIONS = { + 'input': RPIO.IN, + 'output': RPIO.OUT, + 'alt0': RPIO.ALT0, + } + + GPIO_PULL_UPS = { + 'up': RPIO.PUD_UP, + 'down': RPIO.PUD_DOWN, + 'floating': RPIO.PUD_OFF, + } + + 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()} + + 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) + try: + return cls._PINS[number] + except KeyError: + self = super(RPIOPin, cls).__new__(cls) + cls._PINS[number] = self + self._number = number + self._pull = 'up' if number in (2, 3) else 'floating' + self._pwm = False + self._duty_cycle = None + self._bounce = None + self._when_changed = None + self._edges = 'both' + RPIO.setup(self._number, RPIO.IN, self.GPIO_PULL_UPS[self._pull]) + return self + + def __repr__(self): + return "GPIO%d" % self._number + + @property + def number(self): + return self._number + + def close(self): + self.frequency = None + self.when_changed = None + RPIO.setup(self._number, RPIO.IN, RPIO.PUD_OFF) + + def _get_function(self): + 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]) + except KeyError: + raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self)) + + def _get_state(self): + if self._pwm: + return self._duty_cycle + else: + return RPIO.input(self._number) + + def _set_state(self, value): + if not 0 <= value <= 1: + raise PinInvalidValue('invalid state "%s" for pin %r' % (value, self)) + if self._pwm: + RPIO.PWM.clear_channel_gpio(0, self._number) + if value == 0: + RPIO.output(self._number, False) + elif value == 1: + RPIO.output(self._number, True) + else: + 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) + except ValueError: + raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) + except RuntimeError: + raise PinSetInput('cannot set state of pin %r' % self) + + def _get_pull(self): + return self._pull + + 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._number in (2, 3): + raise PinFixedPull('%r has a physical pull-up resistor' % self) + try: + 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)) + + def _get_frequency(self): + return 100 + + def _set_frequency(self, value): + if value is not None and value != 100: + raise PinPWMError( + 'RPIOPin implementation is currently limited to ' + '100Hz sub-cycles') + if not self._pwm and value is not None: + 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) + elif self._pwm and value is None: + RPIO.PWM.clear_channel_gpio(0, self._number) + self._pwm = False + + def _get_bounce(self): + return None if self._bounce is None else (self._bounce / 1000) + + def _set_bounce(self, value): + f = self.when_changed + self.when_changed = None + try: + self._bounce = None if value is None else (value * 1000) + finally: + self.when_changed = f + + def _get_edges(self): + return self._edges + + def _set_edges(self, value): + f = self.when_changed + self.when_changed = None + try: + self._edges = value + finally: + self.when_changed = f + + def _get_when_changed(self): + return self._when_changed + + def _set_when_changed(self, value): + if self._when_changed is None and value is not None: + self._when_changed = value + RPIO.add_interrupt_callback( + 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) + except KeyError: + # Ignore this exception which occurs during shutdown; this + # simply means RPIO's built-in cleanup has already run and + # removed the handler + pass + self._when_changed = None + else: + self._when_changed = value + diff --git a/setup.py b/setup.py index f49d0c2..192bd30 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,6 @@ __keywords__ = [ ] __requires__ = [ - 'RPi.GPIO', 'spidev', ]