Cómo crear un módulo IoT Edge que use las GPIOs de Raspberry Pi 4

Tiempo de lectura: 8 minutos
Imagen ornamental para la entrada: Cómo crear un módulo IO para IoT Edge usando Raspberry Pi 4

Se acercan las navidades y este año en mi carta he pedido una Raspberry Pi 4 con la que poder jugar en mis ratos libres. No soy una persona a la que le guste esperar, y no he podido aguantar las ganas de ponerme a probarla con varios proyectos que tenía pendientes. De ahí, que me haya parecido muy interesante dedicar la última entrada del año a uno de esos proyectos que tenía pendiente 🙂

Del título queda bastante claro cuál va a ser el tema de esta entrada, cómo poder gestionar el GPIO (general-purpose input/output) de una Raspberry Pi con un módulo de IoT Edge, y con ella pretendo dar fin a la temática de estas últimas entradas dedicadas al mundo IoT relacionado con Azure y Docker. Por si tienes interés, son todas estas:

¿Por qué una Raspberry Pi?

La verdad es que cuando hablamos de IoT, normalmente una pensamos en placas hechas a medida, que trabajan con muy pocos recursos y tienen una función específica muy concreta. Aunque Raspberry Pi es una placa que en su última versión no es precisamente un hardware simplón (hay que tener en cuenta que soporta el 4K, más que muchos ordenadores actuales), sí que es un hardware muy asequible. Además, el número de entradas que nos ofrece no es nada desdeñable:

Diagrama de pines GPIO de la Raspberry Pi 4 Model B

Precisamente del hecho de que sea un PC sencillo y de muy bajo consumo (11W y eso que tengo un overclock de un 40%), junto a un precio muy razonable, es lo que ha motivado el que esta entrada se haga en base a ella. Realmente, todo lo que vamos a ver aquí es susceptible de aplicar en cualquier otro hardware sobre el que estemos desplegando módulos de IoT Edge y no solo en una Raspberry Pi, incluso el uso de sus GPIO.

Llegados a este punto, voy a hacer una aclaración importante: Todo lo dicho en esta entrada aplica solo cuando estemos utilizando IoT Edge y no Docker en general.

Esto es porque hoy en día, el runtime de IoT Edge no nos permite levantar contenedores con cualquier configuración. Esta limitación nos va a obligar a dar más permisos de los que se consideran deseables puesto que nuestro módulo va a necesitar correr con permisos privilegiados como veremos más adelante.

Si por el contrario, queremos acceder al GPIO de Raspberry Pi desde Docker directamente, yo utilizaría un modelo en el que asigne un usuario especifico con acceso al GPIO.

Probando las GPIOs de una Raspberry Pi

Llegados a este punto, si sigues leyendo esto es porque asumes que quieres utilizar IoT Edge y que por tanto vas a crear un módulo que se despliegue en una Raspberry Pi y que opere su GPIOs.

Como es importante aprender a andar antes de aprender a correr, lo primero que he hecho es crear un pequeño programa que sea capaz de leer una entrada y escribir una salida de las GPIO de Raspberry Pi directamente, sin IoT Edge de por medio.

Para esto, hay que decir que .Net Core tiene soporte directo para este tipo de GPIO a través del paquete ‘System.Device.Gpio‘, con lo que el proceso se simplifica bastante.

using System;
using System.Device.Gpio;

namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            // GPIO 6 which is physical pin 31
            int outPin = 6;
            // GPIO 12 is physical pin 32
            int inPin = 12;

            var controller = new GpioController();

            // Sets the LED pin to output mode so we can switch something on
            controller.OpenPin(outPin, PinMode.Output);

            // Sets the button pin to input mode so we can read a value
            controller.OpenPin(inPin, PinMode.Input);

            // Read pin value
            Console.WriteLine(controller.Read(inPin));

            // Set pin value
            controller.Write(outPin, PinValue.High);

            // Reset pin value
            controller.Write(outPin, PinValue.Low);
        }
    }
}

Básicamente, este programa lo que hace es abrir los pines 31 y 32 en modo salida y entrada respectivamente, lee la entrada y escribe la salida. Es un programa muy sencillo pero valida que tenemos nuestro sistema funciona bien si vamos directos a sobre la Raspberry Pi.

Una cosa que me gusta de este paquete NuGet es que podemos crear un callback para los cambios sobre un pin en concreto, por lo que no necesitamos leer todo el tiempo ese pin, sino que se ejecutará el callback siempre que haya cambios.

Por otro lado, en caso de que nuestras entradas no estén soportadas por ese paquete, siempre podemos añadir ‘Iot.Device.Bindings‘ para ampliar la gama de GPIOs soportados.

Creando un módulo de IoT Edge que opere el GPIO de una Raspberry Pi

Hemos conseguido leer y escribir las GPIO sin problemas, por lo que vamos a llevarnos el código a un módulo de IoT Edge. Esto no tiene mucho misterio si has leído las entradas anteriores relacionadas con el tema, pero por si acaso, he dejado el código completo en GitHub.

Para no complicar mucho el proceso de manera innecesaria, simplemente estamos haciendo 3 cosas:

  • Inicializar tanto el GPIO como el módulo.
  • Escribir una salida cada 300 milisegundos.
  • Leer una entrada cada segundo y enviando el valor a la cola llamada ‘output’.

Una cosa a tener en cuenta, el código no es importante en esta entrada, con lo que hemos visto antes podría ser suficiente. Simplemente lo estamos ‘integrando’ un poco más el GPIO de Raspberry Pi en el módulo de IoT Edge.

Digo que el código no es importante, porque precisamente el reto aquí es conseguir que un módulo que corre como un contenedor Docker, gestionado totalmente por el runtime de IoT Edge, sea capaz de acceder al hardware subyacente para poder operar las salidas.

Adaptando el módulo creado para poder operar las GPIOs

Al principio de la entrada hacia un disclaimer sobre que esta no es la mejor manera si estamos trabajando con Docker directamente en lugar de IoT Edge, y es el momento de explicarlo.

Si ejecutamos el comando ls -l en el directorio /sys/class (que es donde se encuentran los puntos de montaje de parte del hardware de la Raspberry Pi), vamos a encontrarnos con algo como esto:

pi@raspberrypi:/sys/class $ ls -l
total 0
...
drwxrwx--- 2 root gpio 0 Dec 13 16:57 gpio
...

Esto nos está indicando que, aunque montásemos la ruta directamente en el módulo, esto no va a ser suficiente debido a los permisos del sistema de archivos.

Para que esto funcione, necesitamos que nuestro contenedor se ejecute con el usuario root, o con algún usuario que pertenezca el grupo gpio. Esto con Docker sería un problema muy sencillo de resolver simplemente indicando el modificador -u junto al usuario y grupo con el que queramos ejecutar el contenedor.

Es precisamente por esto, que para que nuestro módulo funcione vamos a necesitar hacer 2 cosas:

  • Ejecutar nuestro contenedor como root
  • Montar el directorio dentro de nuestro módulo

Aunque sinceramente, el tener que ejecutar el contenedor con privilegios de root me parece algo que se deba evitar siempre que sea posible. El problema aquí es que IoT Edge nos permite definir dentro de su plantilla algunas opciones propias de la creación de contenedores, pero a día de hoy, no se soportan todas (o al menos yo he sido incapaz de hacerlas funcionar, estaré encantado si alguien lo ha conseguido que me deje un comentario).

De hecho, las 2 únicas que he visto que se soportan son image y createOptions, por lo que no podemos indicar el usuario con el que queremos ejecutar el proceso.

Dicho esto, vamos a ver cómo es gracias a createOptions que vamos a conseguir tener al menos la versión menos segura.

Dentro de las opciones que nos ofrece createOptions, encontramos una llamada HostConfig, en cuyo interior podemos definir diferentes bind mounts. Por otro lado, ya que no podemos elegir el usuario que ejecuta el contenedor, sí que podemos decirle al menos que se ejecute con permisos elevados (root) siempre que el contenedor no defina y utilice otro usuario.

Nuestro módulo teniendo estas dos cosas en cuenta, quedaría algo como esto:

"modules": {
  "IO": {
    "version": "1.0",
    "type": "docker",
    "status": "running",
    "restartPolicy": "always",
    "settings": {
      "image": "${MODULES.IO}",
      "createOptions": {
        "Privileged": true,
        "HostConfig": {
          "Binds": [
            "/sys:/sys"
          ]
        }
      }
    }
  }
}

Pero es importante señalar, que eso solo funcionará si no se ha definido otro usuario con el cual ejecutar el contenedor (cosa que es recomendable). Si vamos a los diferentes Dockerfiles que se han generado, podemos ver que en los que no son .debug sí que se está definiendo un usuario específico para el módulo, por lo que debemos comentar esas líneas de los Dockerfile:

RUN useradd -ms /bin/bash moduleuser
USER moduleuser

Probando nuestro módulo

Una vez que hemos terminado con todos estos cambios, ya estamos listos para probar nuestro módulo. Simplemente generamos la imagen del método que prefiramos, hardware dedicado o docker buildx (si te interesa este punto, hablamos de ello en Cómo crear una imagen docker multiarquitectura desde cero) y la desplegamos sobre nuestra Raspberry Pi. Previamente le hemos tenido que instalar el runtime de IoT Edge.

Una vez que el módulo se haya desplegado, vamos a recibir mensajes desde el dispositivo que se pueden parecer a estos:

[IoTHubMonitor] [12:05:44 AM] Message received from [Raspi/IO]:
"High"
[IoTHubMonitor] [12:05:45 AM] Message received from [Raspi/IO]:
"High"
[IoTHubMonitor] [12:05:46 AM] Message received from [Raspi/IO]:
"Low"
[IoTHubMonitor] [12:05:47 AM] Message received from [Raspi/IO]:
"High"
[IoTHubMonitor] [12:05:48 AM] Message received from [Raspi/IO]:
"Low"

Llegados a este punto y si vas siguiendo paso a paso, seguramente en tu caso no esté cambiando el mensaje que recibes. ¿Y dónde está la magia entonces? Pues en algo que no se puede ver, un cable que une físicamente esos dos pines (31 y 32, indicados en el programa):

La imagen muestra la Raspberry Pi con un cable que uno dos de sus GPIOs

Con este simple cable, estamos consiguiendo que la entrada que leemos y enviamos a la cola ‘output’ este constantemente cambiando de valor. Con esto tan simple, estamos comprobando que de verdad estamos accediendo a las GPIOs de las Raspberry Pi desde el módulo IoT Edge, ya que estamos cambiando el mensaje en función del valor de la entrada.

Conclusión

Aunque hay que hacer algunas cosas que podríamos considerar ‘feas’ desde el punto de vista de las buenas prácticas de Docker, deberíamos tener en cuenta que realmente nuestro objetivo es IoT Edge y no Docker. Aclaro esto porque podemos encontrarnos con otras situaciones donde tampoco podamos cumplir al 100% las buenas prácticas y no nos queda otra que esperar a que el soporte vaya siendo más completo.

Dicho esto, personalmente pienso que es una buenísima manera de conseguir mejorar el ciclo de vida de una aplicación que opere entradas y salidas. Estamos aprovechando toda la potencia que nos ofrece IoT Edge (y de la que ya hemos hablado largo y tendido) con el añadido de que podemos operar sensórica real que esté conectada a la Raspberry Pi. Las GPIO integradas nos ofrecen tanto entradas como salidas digitales y PWM de serie, con posibilidad de ampliar con entradas y salidas analógicas.

Deja un comentario