Entradas y salidas digitales en Python

Para programar entradas y salidas digitales en Python tenemos también una amplia variedad de bibliotecas:

  • La biblioteca GPIO Zero proporciona una interfaz abstracta muy sencilla de usar para una amplia variedad de periféricos. Es especialmente cómoda para el uso de entradas y salidas digitales.
  • La biblioteca RPi.GPIO es un módulo que proporciona acceso exclusivamente a las entradas y salidas digitales. Actualmente no soporta SPI, I2C, hardware PWM o comunicaciones serie, aunque está previsto que lo soporte en el futuro.
  • La biblioteca wiringPi2 no es más que un envoltorio de la biblioteca C del mismo nombre. Por tanto gran parte de lo que decimos en el capítulo anterior sobre wiringPi es también aplicable a Python.
  • La biblioteca pigpio es un envoltorio de la interfaz a pigpiod en C. El programa pigpiod ya hemos tenido ocasión de utilizarlo en el segundo capítulo, a través de su interfaz de línea de órdenes pigs. El módulo Python ofrece unas características similares y permite una conexión remota a pigpiod. Eso significa que podemos ejecutar nuestro programa en un ordenador convencional y que éste se comunique con la Raspberry Pi para realizar la interacción con el mundo físico.

En este capítulo describiremos exclusivamente la primera, aunque animamos a experimentar con otras alternativas una vez que se conozcan los fundamentos.

GPIO Zero fue desarrollada fundamentalmente por Ben Nuttal de la Raspberry Pi Foundation y Dave Jones, el creador de la biblioteca picamera. Sigue fielmente el propósito de la Fundación, impulsar la docencia de la electrónica y la informática en edades tempranas. Por tanto es extremadamente fácil de usar.

Luces y botones

Un aspecto importante de GPIO Zero es que cambia el foco del programador hacia el sistema. No se piensa en términos de los periféricos del BCM2835, sino en términos de los componentes del sistema. Esto hace que su uso sea tremendamente intuitivo.

from gpiozero import LED, Button
from signal import pause

led = LED(17)
button = Button(3)

button.when_pressed = led.on
button.when_released = led.off

pause()

Practicamente se explica solo. Utiliza una interfaz declarativa, en la que simplemente se describe lo que tiene que pasar cuando suceden eventos en los componentes del sistema. No se indica expícitamente qué patas son salidas o entradas, pero lo hace de forma automática al indicar que en GPIO17 hemos puesto un LED y en GPIO3 un pulsador. No se indica explícitamente que se active un pull-up, pero lo hace por el mero hecho de decir que en la pata GPIO3 hemos puesto un pulsador. Podemos cambiar el pull-up por un pull-down añadiendo un parámetro en el constructor button = Button(3, pull_up = False).

La línea button.when_pressed = led.on indica que cuando ocurra el evento pressed sobre el botón se invoque a la función led.on(). Es así de simple. Evidentemente por debajo hay un hilo que está consultando el estado de la entrada digital, pero se oculta completamente al usuario.

Es muy frecuente que una vez declarado todo lo que debe ocurrir ya no tengamos nada más que hacer. Si no hacemos nada más el programa terminaría. Así que no nos queda más remedio que quedarnos en algún tipo de bucle que no haga nada. La forma más conveniente en Python es llamar a la función signal.pause() que simplemente espera hasta que reciba una señal. Ya hemos hablado de señales cuando describíamos la orden kill. Un simple Control-C (interrupción de usuario) es recibido como una señal y por tanto saldría.

Cada vez que asociamos un dispositivo a un pin la biblioteca lo registra como ocupado y generaría una excepción si intentamos asociar dos dispositivos al mismo pin. Esto es extremadamente útil para evitar errores de programación, pero en ocasiones tenemos multiplexados varios dispositivos a una misma pata. Para eso se proporciona el método close, que libera el pin para otros usos:

bz = Buzzer(16)
bz.on()
bz.off()
bz.close()
led = LED(16)
led.blink()

La lista de las clases y dispositivos soportados crece continuamente, así que la mejor forma de estudiar la biblioteca es emplear los métodos de consulta de documentación de Python.

Explorando GPIO Zero

No hay mejor forma de explorar la jerarquía de clases de GPIO Zero que leyendo el código con ayuda de IDLE. Hay multitud de ejemplos en los comentarios y documentación de cada clase de la biblioteca. Simplemente recuerda que las funciones que empiezan por un carácter de subrayado (_) no están pensadas para ser usadas fuera de la clase.

Abre IDLE y en el menú File selecciona Open Module. Escribe gpiozero. Aparecerá el archivo principal de la biblioteca, que importa todos los demás. Selecciona otra ver el menú File pero ahora pincha en Open.... Elige input_devices.py. En la nueva ventana vuelve a seleccionar el menú File pero ahora pincha en Class Browser. Aparecerá una ventana con todas las clases definidas en este archivo. El mismo procedimiento lo puedes repetir con output_devices.py, spi_devices.py o cualquier otro archivo de la biblioteca.

Es importante conocer cómo está hecha la biblioteca porque es muy probable que nos enfrentemos a algún dispositivo que no está actualmente modelado en GPIO Zero. Veamos la estructura de Button y LED. Navega primero por las clases de output_devices.py

class LED(DigitalOutputDevice):
    pass

La clase LED no es más que un sinónimo de DigitalOutputDevice.

class DigitalOutputDevice(OutputDevice):
    def __init__(self, pin=None, active_high=True, initial_value=False):
        # ...

    @property
    def value(self):
        return self._read()

    @value.setter
    def value(self, value):
        self._stop_blink()
        self._write(value)

    def close(self):
        self._stop_blink()
        super(DigitalOutputDevice, self).close()

    def on(self):
        self._stop_blink()
        self._write(True)

    def off(self):
        self._stop_blink()
        self._write(False)

    def blink(self, on_time=1, off_time=1, n=None, background=True):
        # ...

Tiene métodos on, off y blink (parpadear), así como una propiedad value que cuando se escribe cancela el parpadeo si lo hay y escribe el valor. Las propiedades funcionan igual que los atributos pero al asignar valores o leer valores se invocan funciones en lugar de acceder a los valores directamente.

Al derivar de OutputDevice incorpora toda la funcionalidad de esta clase:

class OutputDevice(SourceMixin, GPIODevice):
    def __init__(self, pin=None, active_high=True, initial_value=False):
        # ...

    def _write(self, value):
        if not self.active_high:
            value = not value
        try:
            self.pin.state = bool(value)
        except AttributeError:
            self._check_open()
            raise

    def on(self):
        self._write(True)

    def off(self):
        self._write(False)

    def toggle(self):
        with self._lock:
            if self.is_active:
                self.off()
            else:
                self.on()

    @property
    def value(self):
        return super(OutputDevice, self).value

    @value.setter
    def value(self, value):
        self._write(value)

    @property
    def active_high(self):
        return self._active_state

    @active_high.setter
    def active_high(self, value):
        self._active_state = True if value else False
        self._inactive_state = False if value else True

Básicamente proporciona los mismos servicios que DigitalOutputDevice salvo por el parpadeo. Además implementa la lógica necesaria para tratar las señales desde un punto de vista lógico, independientemente de si son activas a nivel alto o bajo.

Pero interesa especialmente las clases de las que deriva, SourceMixin y GPIODevice. La primera puede encontrarse en mixins.py. Un mixin es una clase que incorpora funcionalidad adicional a la clase a la que se la aplique, que depende de la propia clase. En este caso añade una propiedad source que dado un iterable lo recorre y asigna el valor leído a la propiedad value. Cada lectura se separa un tiempo que es definible en otra propiedad source_delay. Veremos en seguida cómo coopera con los dispositivos de entrada.

La otra clase ancestro es GPIODevice que está definida en devices.py:

class GPIODevice(Device):
    """
    Extends :class:`Device`. Represents a generic GPIO device and provides
    the services common to all single-pin GPIO devices (like ensuring two
    GPIO devices do no share a :attr:`pin`).

    :param int pin:
        The GPIO pin (in BCM numbering) that the device is connected to. If
        this is ``None``, :exc:`GPIOPinMissing` will be raised. If the pin is
        already in use by another device, :exc:`GPIOPinInUse` will be raised.
    """
    ...

Es decir, simplemente se encarga de garantizar que el pin no está siendo usado por otro dispositivo. El resto lo delega en Device que está en el mismo archivo:

class Device(ValuesMixin, GPIOBase):
    @property
    def value(self):
        raise NotImplementedError

    @property
    def is_active(self):
        return bool(self.value)

Define una propiedad value que no está implementada (se implementa en las clases derivadas) y deriva de dos nuevas clases (ValuesMixin y GPIOBase).

ValuesMixin está en mixins.py. Simplemente añade la propiedad values que se construye como un generador que lee una y otra vez la propiedad value. Esto permite conectarlo con la propiedad source. Piensa por ejemplo qué hace el siguiente fragmento:

led1 = LED(17)
led2 = LED(18)
led1.source = led2.values

El resto de clases no interesa de momento. La biblioteca llega a una sofisticación notable impidiendo que un usuario ponga atributos sin darse cuenta, por un error sintáctico. Por ejemplo, prueba esto:

from gpiozero import *
led1 = LED(17)
led2 = LED(18)
led1.surce = led2.values

En Python esta sintaxis sería completamente legal, el error tipográfico se traduciría en que el programa no funciona y el led1 tiene un atributo extraño surce. En GPIO Zero es un error intentar añadir atributos después de la construcción.

Vamos a explorar un poco la clase Button. Navega ahora por las clases de input_devices.py.

class Button(HoldMixin, DigitalInputDevice):
    def __init__(
            self, pin=None, pull_up=True, bounce_time=None,
            hold_time=1, hold_repeat=False):
        # ...

Osea, que simplemente es una combinación de HoldMixin y DigitalInputDevice. La primera es un mixin y por tanto está en mixins.py.

class HoldMixin(EventsMixin):
    """
    Extends :class:`EventsMixin` to add the :attr:`when_held` event and the
    machinery to fire that event repeatedly (when :attr:`hold_repeat` is
    ``True``) at intervals defined by :attr:`hold_time`.
    """
    ...

Simplemente añade el evento when_held a la clase a la que se aplica. Si se mantiene pulsado el botón por un tiempo configurable en la propiedad hold_time entonces se invocará a la función when_held.

La clase EventsMixin es otro mixin que añade los eventos when_activated y when_deactivated, así como los métodos wait_for_active y wait_for_inactive.

Por otro lado DigitalInputDevice es una combinación de EventsMixin (que ya conocemos) y InputDevice:

class DigitalInputDevice(EventsMixin, InputDevice):
    def __init__(self, pin=None, pull_up=False, bounce_time=None):
        ...

En cuanto a InputDevice lo único que añade a un GPIODevice es la propiedad pull_up para configurar un pull-up o un pull-down.

class InputDevice(GPIODevice):
    def __init__(self, pin=None, pull_up=False):
        ...

    @property
    def pull_up(self):
        return self.pin.pull == 'up'

Pero si te has fijado bien no aparecen las propiedades when_pressed o when_released. ¿Cómo se definen? La respuesta está justo después de la definición de Button:

Button.is_pressed = Button.is_active
Button.pressed_time = Button.active_time
Button.when_pressed = Button.when_activated
Button.when_released = Button.when_deactivated
Button.wait_for_press = Button.wait_for_active
Button.wait_for_release = Button.wait_for_inactive

Añade nombres alternativos, más fáciles de recordar, para las propiedades estándar de todos los dispositivos.

Clases básicas

GPIO Zero tiene un buen montón de clases directamente utilizables:

Clase Descripción
LED LED conectado a pin con resistencia a masa.
Buzzer Buzzer o chicharra conectado a pin directamente con cátodo a masa.
PWMLED LED conectado a pin con resistencia a masa, con intensidad variable.
RGBLED LED tricolor a tres pines con resistencia a masa.
Motor Motor conectado con un puente H a dos pines para movimiento bidireccional.
Button Pulsador conectado a masa y al pin.
LineSensor Sensor infrarrojo de línea para robot seguidor de línea.
MotionSensor Sensor PIR con OUT conectado a pin a través de un divisor de tensión o level converter.
LightSensor LDR a 3.3V con condensador de 1uF a masa y ambas patas a un pin.
DistanceSensor Sensor de ultrasonidos con TRIG a un pin y ECHO a otro pin a traves de un divisor de tensión o level converter.
MCP3xxx Convertidores AD de Microchip MCP300x, MCP320x y MCP330x.

Actualmente GPIO Zero no soporta el ADS1118, pero se trata de un dispositivo SPI, como los MCP3xxx, así que su incorporación es sencilla. Lo veremos en seguida.

Además GPIO Zero incluye una clase CompositeDevice que permite agrupar varios dispositivos individuales. Tienes varios ejemplos en el archivo boards.py.

Ejemplos de uso

Poco más hay que decir de las entradas y salidas digitales, pero para ilustrar su uso vamos a poner los mismos ejemplos que en C.

En C poníamos un ejemplo en que el pin 18 conmutaba cada segundo. En Python podría traducirse directamente así:

from gpiozero import LED
from time import sleep

led = LED(18)
while True:
      led.toggle()
      sleep(1)

Pero todavía hay una forma más fácil:

from gpiozero import LED
from signal import pause

led = LED(18)
led.blink()
pause()

Si queremos cambiar el periodo no hay más que pasar parámetros adicionales. Por ejemplo:

led.blink(on_time=0.3, off_time=0.7)

El uso de PWM es también trivial. Por ejemplo, consideremos el caso de un parpadeo de un LED variando la intensidad de forma sinusoidal, como el LED de los MacBook cuando están en stand-by:

led.source = scaled(sin_values(100), 0, 1, -1, 1)

Al definir la propiedad source empieza un hilo que lee valores de esta fuente cada 0.01 segundos por defecto. La función sin_values va produciendo valores de un seno entre -1 y 1 con un periodo de 100 muestras. Simplemente tenemos que escalar estos valores que van de -1 a 1 en los correspondientes valores entre 0 y 1. Esa adaptación la hace el generador scaled. Todo este tipo de adaptadores que están en el módulo gpiozero.tools son extremadamente útiles, vamos a resumir los más importantes.

Función Descripción
negated Para cada valor v del iterable devuelve not v.
inverted Para cada valor v del iterable devuelve 1 - v.
scaled Escalado lineal de los valores del iterable.
clamped Recortado por arriba y por abajo.
absoluted Para cada valor v del iterable devuelve abs(v).
quantized Discretiza en un número de pasos homogéneos.
all_values Genera True cuando todos los iterables generan True.
any_values Genera True cuando alguno de los iterables generan True.
averaged Promedia los iterables.
queued Encola valores y hasta que no se llena la cola no genera.
pre_delayed Antes de generar espera un tiempo.
post_delayed Después de generar espera un tiempo.
random_values Genera números aleatorios entre 0 y 1.
sin_values Genera valores del seno. Se indica el periodo en muestras.
cos_values Genera valores del coseno. Se indica el periodo en muestras.
izip Combina varios generadores generando tuplas de elementos (está en la biblioteca itertools).
cycle Genera una secuencia infinita a partir de un iterable finito repitiéndolo (está en la biblioteca itertools).

results matching ""

    No results matching ""