diff --git a/docs/api_pins.rst b/docs/api_pins.rst index 4167de6..ad7dade 100644 --- a/docs/api_pins.rst +++ b/docs/api_pins.rst @@ -20,10 +20,12 @@ integer number instead, it uses one of the following classes to provide the 2. :class:`gpiozero.pins.rpio.RPIOPin` -3. :class:`gpiozero.pins.native.NativePin` +3. :class:`gpiozero.pins.pigpiod.PiGPIOPin` + +4. :class:`gpiozero.pins.native.NativePin` You can change the default pin implementation by over-writing the -``DefaultPin`` global in devices like so:: +``DefaultPin`` global in the ``devices`` module like so:: from gpiozero.pins.native import NativePin import gpiozero.devices @@ -35,8 +37,24 @@ You can change the default pin implementation by over-writing the # 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:: +Alternatively, instead of passing an integer to the device constructor, you +can pass a :class:`Pin` object itself:: + + from gpiozero.pins.native import NativePin + from gpiozero import LED + + led = LED(NativePin(16)) + +This is particularly useful with implementations that can take extra parameters +such as :class:`PiGPIOPin` which can address pins on remote machines:: + + from gpiozero.pins.pigpiod import PiGPIOPin + from gpiozero import LED + + led = LED(PiGPIOPin(16, host='my_other_pi')) + +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 @@ -52,13 +70,6 @@ part of IO extender chips. For example:: comments from testers! -Abstract Pin -============ - -.. autoclass:: Pin - :members: - - RPiGPIOPin ========== @@ -75,6 +86,14 @@ RPIOPin .. autoclass:: RPIOPin +PiGPIOPin +========= + +.. currentmodule:: gpiozero.pins.pigpiod + +.. autoclass:: PiGPIOPin + + NativePin ========= @@ -82,3 +101,10 @@ NativePin .. autoclass:: NativePin + +Abstract Pin +============ + +.. autoclass:: Pin + :members: + diff --git a/docs/conf.py b/docs/conf.py index 73b3a4b..ab831ff 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,6 +39,7 @@ 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['pigpio'] = Mock() sys.modules['w1thermsensor'] = Mock() sys.modules['spidev'] = Mock() diff --git a/gpiozero/devices.py b/gpiozero/devices.py index a96b9d5..a1feb4e 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -38,8 +38,12 @@ except ImportError: from .pins.rpio import RPIOPin DefaultPin = RPIOPin except ImportError: - from .pins.native import NativePin - DefaultPin = NativePin + try: + from .pins.pigipod import PiGPIOPin + DefaultPin = PiGPIOPin + except ImportError: + from .pins.native import NativePin + DefaultPin = NativePin _THREADS = set() diff --git a/gpiozero/pins/native.py b/gpiozero/pins/native.py index 4b1de95..0966a3d 100644 --- a/gpiozero/pins/native.py +++ b/gpiozero/pins/native.py @@ -156,6 +156,13 @@ class NativePin(Pin): use any class which requests PWM will raise an exception. This implementation is also experimental; we make no guarantees it will not eat your Pi for breakfast! + + You can construct native pin instances manually like so:: + + from gpiozero.pins.native import NativePin + from gpiozero import LED + + led = LED(NativePin(12)) """ _MEM = None diff --git a/gpiozero/pins/pigpiod.py b/gpiozero/pins/pigpiod.py new file mode 100644 index 0000000..23bc01c --- /dev/null +++ b/gpiozero/pins/pigpiod.py @@ -0,0 +1,240 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +str = type('') + +import pigpio + +from . import Pin +from ..exc import ( + PinInvalidFunction, + PinSetInput, + PinFixedPull, + ) + + +class PiGPIOPin(Pin): + """ + 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 + access to the GPIO pins, and communicates with this daemon over a network + socket. + + While this does mean only the daemon itself should control the pins, the + architecture does have several advantages: + + * Pins can be remote controlled from another machine (the other + machine doesn't even have to be a Raspberry Pi; it simply needs the + `pigpio`_ client library installed on it) + * The daemon supports hardware PWM via the DMA controller + * Your script itself doesn't require root privileges; it just needs to + be able to communicate with the daemon + + You can construct pigpiod pins manually like so:: + + from gpiozero.pins.pigpiod import PiGPIOPin + from gpiozero import LED + + led = LED(PiGPIOPin(12)) + + This is particularly useful for controlling pins on a remote machine. To + accomplish this simply specify the host (and optionally port) when + constructing the pin:: + + from gpiozero.pins.pigpiod import PiGPIOPin + from gpiozero import LED + from signal import pause + + led = LED(PiGPIOPin(12, host='192.168.0.2')) + + .. note:: + + In some circumstances, especially when playing with PWM, it does appear + to be possible to get the daemon into "unusual" states. We would be + most interested to hear any bug reports relating to this (it may be a + bug in our pin implementation). A workaround for now is simply to + restart the ``pigpiod`` daemon. + + .. _pigpio: http://abyz.co.uk/rpi/pigpio/ + """ + + _CONNECTIONS = {} + _PINS = {} + + GPIO_FUNCTIONS = { + 'input': pigpio.INPUT, + 'output': pigpio.OUTPUT, + 'alt0': pigpio.ALT0, + 'alt1': pigpio.ALT1, + 'alt2': pigpio.ALT2, + 'alt3': pigpio.ALT3, + 'alt4': pigpio.ALT4, + 'alt5': pigpio.ALT5, + } + + GPIO_PULL_UPS = { + 'up': pigpio.PUD_UP, + 'down': pigpio.PUD_DOWN, + 'floating': pigpio.PUD_OFF, + } + + GPIO_EDGES = { + 'both': pigpio.EITHER_EDGE, + 'rising': pigpio.RISING_EDGE, + 'falling': pigpio.FALLING_EDGE, + } + + 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, host='localhost', port=8888): + try: + return cls._PINS[(host, port, number)] + except KeyError: + self = super(PiGPIOPin, cls).__new__(cls) + cls._PINS[(host, port, number)] = self + try: + self._connection = cls._CONNECTIONS[(host, port)] + except KeyError: + self._connection = pigpio.pi(host, port) + cls._CONNECTIONS[(host, port)] = self._connection + self._host = host + self._port = port + self._number = number + self._pull = 'up' if number in (2, 3) else 'floating' + self._pwm = False + self._bounce = None + self._when_changed = None + self._callback = None + self._edges = pigpio.EITHER_EDGE + self._connection.set_mode(self._number, pigpio.INPUT) + self._connection.set_pull_up_down(self._number, self.GPIO_PULL_UPS[self._pull]) + self._connection.set_glitch_filter(self._number, 0) + self._connection.set_PWM_range(self._number, 255) + return self + + def __repr__(self): + if self._host == 'localhost': + return "GPIO%d" % self._number + else: + return "GPIO%d on %s:%d" % (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 + + 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: + self.frequency = None + self.when_changed = None + self.function = 'input' + self.pull = 'floating' + + def _get_function(self): + return self.GPIO_FUNCTION_NAMES[self._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]) + 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) / 255 + else: + return bool(self._connection.read(self._number)) + + def _set_state(self, value): + if self._pwm: + try: + self._connection.set_PWM_dutycycle(self._number, int(value * 255)) + except pigpio.error: + raise PinInvalidValue('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)) + + 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: + self._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 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_dutycycle(self._number, 0) + self._pwm = True + elif self._pwm and value is not None: + self._connection.set_PWM_frequency(self._number, value) + elif self._pwm and value is None: + self._connection.set_PWM_dutycycle(self._number, 0) + self._pwm = False + + def _get_bounce(self): + return None if not self._bounce else self._bounce / 1000000 + + def _set_bounce(self, value): + if value is None: + value = 0 + self._connection.set_glitch_filter(self._number, int(value * 1000000)) + + 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): + if self._callback is None: + return None + return self._callback.callb.func + + def _set_when_changed(self, value): + if self._callback is not None: + self._callback.cancel() + self._callback = None + if value is not None: + self._callback = self._connection.callback( + self._number, self._edges, + lambda gpio, level, tick: value()) + diff --git a/gpiozero/pins/rpigpio.py b/gpiozero/pins/rpigpio.py index 5c1304f..fad3b8b 100644 --- a/gpiozero/pins/rpigpio.py +++ b/gpiozero/pins/rpigpio.py @@ -22,6 +22,20 @@ class RPiGPIOPin(Pin): the default pin implementation if the RPi.GPIO library is installed. Supports all features including PWM (via software). + Because this is the default pin implementation you can use it simply by + specifying an integer number for the pin in most operations, e.g.:: + + from gpiozero import LED + + led = LED(12) + + However, you can also construct RPi.GPIO pins manually if you wish:: + + from gpiozero.pins.rpigpio import RPiGPIOPin + from gpiozero import LED + + led = LED(RPiGPIOPin(12)) + .. _RPi.GPIO: https://pypi.python.org/pypi/RPi.GPIO """ diff --git a/gpiozero/pins/rpio.py b/gpiozero/pins/rpio.py index 7ec7f6d..ed7bb24 100644 --- a/gpiozero/pins/rpio.py +++ b/gpiozero/pins/rpio.py @@ -32,6 +32,13 @@ class RPIOPin(Pin): 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``. + You can construct RPIO pins manually like so:: + + from gpiozero.pins.rpio import RPIOPin + from gpiozero import LED + + led = LED(RPIOPin(12)) + .. _RPIO: https://pythonhosted.org/RPIO/ """