Aprendiendo MQTT 3: El suscriptor, ESP32 con sensor de temperatura y humedad ambiental.

Montaje ESP32 suscriptor MQTT con temperatura y humedad

Continuamos con la segunda entrega de nuestro montaje, en el que estamos aprendiendo a usar el protocolo de comunicaciones MQTT. Nos centraremos ahora en el suscriptor, que será el elemento encargado de recibir los mensajes de telemetría. Este suscriptor puede ser una pantalla que muestre datos (como en este montaje) o, también, un dispositivo que actúe de una manera u otra según los datos recibidos. Un ejemplo podría ser una electroválvula para riego que se abre o cierra cuando una sonda detecta un cierto nivel de humedad en el terreno.

¡Os animo a que os pongáis manos a la obra y hagáis el montaje es muy gratificante y os dejo todo lo necesario para hacerlo!

El suscriptor MQTT, desde el punto de vista de la programación, es de naturaleza asíncrona. ¿Qué quiero decir con esto? Pues que podrá realizar acciones como cambiar el texto del display, accionar un relé o guardar valores en una base de datos cuando reciba un mensaje, y esto podría ocurrir en cualquier momento. La forma más común y elegante de hacer esto es mediante una función de ‘callback’. Dependiendo del lenguaje de programación con el que estemos trabajando, a esta función también se le podría llamar ‘evento’, ‘acción’, entre otros términos, pero en esencia es un parámetro, propiedad o variable que aloja un método que se invoca cuando sucede algo específico, desencadenando así la ejecución de un bloque de código.

El montaje del display de Temperatura y Humedad.

Para este montaje usaremos un ESP32 que lo vamos a programar con ayuda de platformio en c++ con las librerías:

  • WiFi.h: para la conexión WiFi.
  • PubSubClient.h: para la conexión y operaciones MQTT.
  • Wire.h: para comunicación I2C.
  • LiquidCrystal_I2C.h: para manejar un LCD que utiliza I2C.

El montaje, a nivel de esquema eléctrico, consta de un display LCD de dos líneas y 16 caracteres cada una (16×2). Este se puede conectar de dos maneras: la primera, enviando datos de forma paralela, y la segunda, utilizando un adaptador I2C de serie a paralelo. Con el adaptador I2C, solo necesitamos dos cables para la comunicación con el ESP32, además de otros dos cables para la alimentación eléctrica, uno positivo y otro negativo.

Montaje ESP32 suscriptor MQTT con temperatura y humedad
Montaje ESP32 suscriptor MQTT con temperatura y humedad

Para el ESP32 con el que estoy trabajado tiene la señal i2c de reloj en el pin GPIO 22 y la señal de datos en el pin GPIO 21 (aunque en realidad no están funcionando aquí como GPIO que es una salida de propósito general sino como puerto I2C). Por otro lado el pin morado es está conectado a 3V3 (3,3 Voltios) positivo y el blanco a GND o negativo.

ESP32 Pinout
ESP32 descripción de los pines

El código con el framework de arduino:

Ahora pasamos a describir el código fuente que lo podéis encontrar en https://github.com/elrincondeada/mqttdemo/blob/master/LCDMqttClient/src/main.cpp

Lo primero que hacemos es importar las bibliotecas:

#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

Aquí se importan las bibliotecas necesarias para la conexión WiFi, el protocolo MQTT, la comunicación I2C y el manejo del LCD.

Definición de constantes y variables globales

#define TOPIC_TEMPERATURA "esp32/temperatura"
#define TOPIC_HUMEDAD "esp32/humedad"

Esto se hace para evitar errores al escribir un elemento de código que es constante y se va a usar varias veces. Es este caso el tópico así sabemos que siempre lo vamos a escribir igual y evitamos errores.

// Información de WiFi y MQTT
const char* ssid = "XXXXXXXXXXXXXXXXXXX";
const char* password = "XXXXXXXXXXXXXXX";
const char* mqttServer = "XXXXXXXXXXXXX";
const int mqttPort = 1883;

//Protipado de funciones
void callback(char*, byte*, unsigned int);

// Inicializa el cliente WiFi y MQTT
WiFiClient espClient;
PubSubClient client(espClient);

// Inicializa la LCD con dirección I2C, columnas y filas
LiquidCrystal_I2C lcd(0x27, 16, 2);

Empezamos con la declaración de variables globales, donde encontrarás ‘char*’. No es un simple arreglo de caracteres, es un puntero en C. Si te interesa profundizar en los fundamentos de C y qué es un puntero, te recomiendo este vídeo https://youtu.be/AUAX9xNenuo

Luego tenemos algo llamado ‘prototipado de función’. Se trata simplemente de un adelanto de la función que jugará un rol principal en este código, la función ‘callback’.

Finalmente, instanciamos el cliente WiFi para establecer la conexión con el exterior, el cliente MQTT para la comunicación de mensajes y el controlador del display LCD 16×2, todo ello gestionado mediante el protocolo I2C.

la función setup()

void setup() {
  // Inicializa el LCD
  lcd.init();
  lcd.backlight();
  
  // Conexión WiFi
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  // Conexión WiFi
   while (true) { // Bucle infinito
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("Conectado a WiFi!");
      break; // Sale del bucle cuando se conecta
    }
    Serial.print(".");
    delay(200);
   }

  
  // Conexión MQTT
  client.setServer(mqttServer, mqttPort);
  client.setCallback(callback);

  while (!client.connected()) {
    Serial.println("Conectando a MQTT...");

    if (client.connect("ESP32")) {
      Serial.println("Conectado");
    } else {
      Serial.print("Error de conexión: ");
      Serial.print(client.state());
      delay(2000);
    }
  }

  Serial.println("Suscripción a tópicos");
  // Se suscribe al tópico
  client.subscribe(TOPIC_TEMPERATURA);
  client.subscribe(TOPIC_HUMEDAD);
}

«La función void setup() es donde toda la magia inicial tiene lugar. Esta función se ejecuta una sola vez al inicio y configura todo lo necesario para que nuestro código funcione de manera óptima.

  1. Inicialización del LCD: Con las instrucciones lcd.init() y lcd.backlight(), inicializamos el display LCD y encendemos su retroiluminación para hacerlo visible.
  2. Conexión WiFi: Primero, iniciamos la comunicación serie con Serial.begin(115200). Luego, la instrucción WiFi.begin(ssid, password) intenta conectar el ESP32 a la red WiFi utilizando las credenciales almacenadas en las variables ssid y password.
  3. Verificación de Conexión WiFi: Aquí entra en juego un bucle infinito (while(true)) que comprueba constantemente si se ha establecido la conexión WiFi. Si la conexión es exitosa (WiFi.status() == WL_CONNECTED), se imprime un mensaje y se rompe el bucle con break.
  4. Conexión MQTT: Las instrucciones client.setServer(mqttServer, mqttPort) y client.setCallback(callback) configuran el servidor MQTT y la función de callback que se llamará cuando lleguen mensajes al tópico suscrito. Es el corazón de este desarrollo.
  5. Verificación de Conexión MQTT: Similar al WiFi, hay un bucle while que intenta conectar con el servidor MQTT hasta que lo logra. Si hay un error, imprime el estado del intento fallido.
  6. Suscripción a Tópicos: Finalmente, client.subscribe(TOPIC_TEMPERATURA) y client.subscribe(TOPIC_HUMEDAD) suscriben el cliente MQTT a los tópicos de temperatura y humedad, respectivamente.»

La función de callback:

void callback(char* topic, byte* payload, unsigned int length) {
  String payloadStr = "";

  Serial.println("mensaje recibido");

  // Convierte los bytes del payload a String
  for (int i = 0; i < length; i++) {
    payloadStr += (char)payload[i];
  }

  Serial.println(payloadStr);

  // Actualiza el LCD con el mensaje recibido
  if(strstr(topic,TOPIC_TEMPERATURA))
  {
    lcd.setCursor(0, 0);
    lcd.print("Temp. ");
  }

  if(strstr(topic,TOPIC_HUMEDAD)) 
  {
    lcd.setCursor(0, 1);
    lcd.print("Humd. ");
  }

  lcd.print(payloadStr);
}

La función callback es el núcleo de cualquier suscriptor MQTT, y aquí es donde reside toda la lógica que determina qué hacer cuando llega un mensaje a los tópicos a los que se ha suscrito el cliente. Vamos a desglosarla:

  1. Parámetros: La función acepta tres parámetros: char* topic, byte* payload, y unsigned int length. El topic indica el tópico al cual pertenece el mensaje recibido, payload es el cuerpo del mensaje en forma de bytes y length es la longitud del payload.
  2. Inicialización: String payloadStr = ""; inicializa una cadena de texto vacía donde almacenaremos el payload convertido.
  3. Mensaje Recibido: Serial.println("mensaje recibido"); es una forma de debugging que nos indica que ha llegado un nuevo mensaje.
  4. Conversión de Bytes a String: El bucle for toma cada byte del payload y lo convierte a su representación de carácter, acumulando todo en payloadStr.
  5. Impresión del Payload: Serial.println(payloadStr); imprime el payload recibido para verificación.
  6. Actualización de la LCD: Aquí es donde el programa se pone interesante. Si el tópico corresponde a la temperatura (strstr(topic, TOPIC_TEMPERATURA)), el cursor de la LCD se posiciona en la primera fila y escribe «Temp.». De forma similar, si el tópico es de humedad, escribe «Humd.» en la segunda fila.
  7. Imprimir Valor: Finalmente, lcd.print(payloadStr); imprime el valor del payload en la LCD, justo después de «Temp.» o «Humd.», dependiendo del tópico al que pertenece el mensaje.

función loop

void loop() {
  client.loop();
}

Esta línea se encarga de mantener el cliente MQTT en funcionamiento y es crucial para el correcto funcionamiento de todo el programa. ¿Por qué? Porque client.loop() se encarga de procesar cualquier mensaje entrante y saliente, y de mantener la conexión con el servidor MQTT viva. Si no se ejecutara esta función de forma continua en el bucle principal, no se recibirían ni procesarían mensajes MQTT, y el cliente podría incluso desconectarse del servidor.

Por lo tanto, aunque pueda parecer una parte trivial del código, no subestimes su importancia. Actúa como el corazón palpitante de tu cliente MQTT, asegurándose de que todo funcione como un reloj.

Conclusión:

Y ahí lo tienes, la segunda entrega de nuestra serie sobre MQTT, donde nos hemos centrado en el rol del suscriptor. No es magia, es tecnología bien aplicada. Hemos desentrañado el código que corre en un ESP32 y que le permite actuar como un suscriptor MQTT. Desde establecer una conexión Wi-Fi hasta interpretar los mensajes MQTT que le llegan, este pequeño dispositivo hace de todo.

Nuestra función callback es el cerebro del operativo: espera en silencio hasta que llega un mensaje relevante y luego se pone en acción para mostrarlo en nuestro LCD. Y no olvidemos nuestro persistente void loop(), el pulso del programa que asegura que el cliente MQTT está siempre listo para actuar.

Si te ha picado el gusanillo de la programación y quieres ir más allá, hay un mundo de posibilidades que puedes explorar. ¿Qué tal integrar una base de datos para guardar los datos recibidos? ¿O quizás quieras controlar otros dispositivos en función de los mensajes recibidos?

Espero que este tutorial te haya proporcionado una buena base para entender cómo funciona un suscriptor MQTT y cómo puedes empezar a jugar con estas tecnologías. Estamos en una era donde los objetos cotidianos se están volviendo cada vez más inteligentes y conectados, y entender estos conceptos básicos te coloca un paso adelante en este emocionante mundo del Internet de las Cosas.

Te animo a que realices tu mismo el montaje. Puedes cambiar el sensor, el display, conectarlo a un asistente domótico, a alexa… Seguiremos experimentando.

¡Hasta la próxima entrega!

Referencias: