ESP32 y Arduino

Tutorial de la librería PubSubClient para Arduino: configuración, callbacks, buffers y keep alive

Un tutorial práctico de PubSubClient para ESP32 y ESP8266: instala la librería, conéctate a un broker MQTT, gestiona los callbacks, dimensiona el buffer, ajusta el keep alive y pasa a TLS con credenciales reales.

Placa de microcontrolador que ejecuta la librería PubSubClient e intercambia mensajes MQTT con un broker en la nube
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

PubSubClient es la librería MQTT más usada en placas compatibles con Arduino, y es lo bastante pequeña como para funcionar en casi cualquier cosa con una pila de red. En este tutorial la instalarás, conectarás un ESP32 a un broker real, publicarás y te suscribirás, y aprenderás los tres ajustes que causan la mayoría de los problemas en campo: tamaño del buffer, keep alive y timeout del socket. Al final tendrás un sketch resistente a las reconexiones que podrás reutilizar en todos tus proyectos.

Qué es PubSubClient (y qué no es)

PubSubClient, escrita por Nick O'Leary, es un cliente MQTT 3.1.1 ligero. No gestiona la conexión de red por sí misma. En su lugar, envuelve cualquier objeto que implemente la interfaz Client de Arduino: WiFiClient en el ESP32 y el ESP8266, EthernetClient en un shield cableado o WiFiClientSecure cuando quieres TLS. Ese diseño es la razón por la que la misma librería funciona en tantas placas.

También es deliberadamente minimalista. Conocer sus límites de antemano ahorra horas de depuración:

Funcionalidad Comportamiento de PubSubClient
Versión del protocolo MQTT 3.1.1 (MQTT 3.1 opcional en compilación), sin MQTT 5
QoS de publicación Solo QoS 0
QoS de suscripción QoS 0 o QoS 1
Buffer de paquetes por defecto 256 bytes (topic + payload + cabecera)
Keep alive por defecto 15 segundos
Timeout del socket por defecto 15 segundos
Last Will Admitido en connect()
Publicación retenida Admitida
Modelo de hilos Un solo hilo; debes llamar a loop() con frecuencia

Si necesitas propiedades de MQTT 5 o publicar con QoS 2 desde el dispositivo, busca otro cliente. Para la gran mayoría de sensores y actuadores, telemetría con QoS 0 más suscripciones de comandos con QoS 1 es exactamente lo que necesitas. Nuestra guía de niveles de QoS de MQTT explica por qué esa combinación funciona bien.

Instala PubSubClient y prepara la placa

  1. En el IDE de Arduino abre Herramientas → Administrar bibliotecas, busca "PubSubClient" e instala la librería de Nick O'Leary (versión 2.8 o posterior).
  2. Instala el paquete de placas que necesites: "esp32 by Espressif Systems" para placas ESP32 o "esp8266 by ESP8266 Community" para placas tipo NodeMCU.
  3. Selecciona la placa y el puerto, y sube una vez el sketch Blink vacío para confirmar que la cadena de herramientas funciona.
  4. Elige un ID de dispositivo único, por ejemplo pubsub-7f3a9c. Lo usarás como client ID de MQTT y como prefijo de los topics.

Para la primera prueba usarás el área pública de broker.connectmq.com, el servidor MQTT público gratuito que opera ConnectMQ. Acepta conexiones anónimas en el puerto 1883 siempre que el client ID no esté vacío. Todo lo que se publica ahí es legible por cualquiera, así que no envíes nunca credenciales reales ni datos personales.

El ejemplo completo de PubSubClient

El sketch siguiente se conecta a la Wi-Fi, se conecta al broker con un Last Will, publica un mensaje JSON cada cinco segundos, se suscribe a un topic de comandos y se reconecta sin bloquear el resto del código.

#include <WiFi.h>          // usa <ESP8266WiFi.h> en ESP8266
#include <PubSubClient.h>

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* DEVICE_ID = "pubsub-7f3a9c";   // hazlo único

char topicTelemetry[64];
char topicCommand[64];
char topicStatus[64];

WiFiClient netClient;
PubSubClient mqtt(netClient);

unsigned long lastPublish = 0;
unsigned long lastReconnectAttempt = 0;

void onMessage(char* topic, byte* payload, unsigned int length) {
  // el payload NO termina en nulo: cópialo usando la longitud
  char msg[64];
  unsigned int n = length < sizeof(msg) - 1 ? length : sizeof(msg) - 1;
  memcpy(msg, payload, n);
  msg[n] = '\0';

  Serial.printf("Message on %s: %s\n", topic, msg);

  if (strcmp(msg, "ping") == 0) {
    mqtt.publish(topicStatus, "pong");
  }
}

void connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nIP address: %s\n", WiFi.localIP().toString().c_str());
}

bool connectMqtt() {
  // connect(clientId, user, pass, willTopic, willQos, willRetain, willMessage)
  bool ok = mqtt.connect(DEVICE_ID, nullptr, nullptr,
                         topicStatus, 1, true, "offline");
  if (ok) {
    mqtt.publish(topicStatus, "online", true);
    mqtt.subscribe(topicCommand, 1);
    Serial.println("MQTT connected");
  } else {
    Serial.printf("MQTT connect failed, rc=%d\n", mqtt.state());
  }
  return ok;
}

void setup() {
  Serial.begin(115200);

  snprintf(topicTelemetry, sizeof(topicTelemetry), "connectmq/demo/%s/telemetry", DEVICE_ID);
  snprintf(topicCommand,   sizeof(topicCommand),   "connectmq/demo/%s/cmd", DEVICE_ID);
  snprintf(topicStatus,    sizeof(topicStatus),    "connectmq/demo/%s/status", DEVICE_ID);

  connectWifi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);   // espacio para payloads JSON más grandes
  mqtt.setKeepAlive(30);     // segundos
  mqtt.setSocketTimeout(10); // segundos
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWifi();
  }

  if (!mqtt.connected()) {
    unsigned long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      connectMqtt();
    }
    return;
  }

  mqtt.loop();

  if (millis() - lastPublish > 5000) {
    lastPublish = millis();
    char payload[96];
    snprintf(payload, sizeof(payload),
             "{\"uptime\":%lu,\"rssi\":%d,\"heap\":%u}",
             millis() / 1000, WiFi.RSSI(), ESP.getFreeHeap());
    if (!mqtt.publish(topicTelemetry, payload)) {
      Serial.println("Publish failed");
    }
  }
}

Observa la salida desde tu ordenador con las herramientas de línea de comandos de Mosquitto:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-7f3a9c -t "connectmq/demo/pubsub-7f3a9c/#" -v

Después envía un comando a la placa:

mosquitto_pub -h broker.connectmq.com -p 1883 -i sender-7f3a9c -t "connectmq/demo/pubsub-7f3a9c/cmd" -m "ping"

Deberías ver aparecer pong en el topic de estado en menos de un segundo.

Cómo funcionan los callbacks de PubSubClient

La firma del callback es fija: void callback(char* topic, byte* payload, unsigned int length). Hay tres detalles importantes.

Primero, el payload es un puntero al buffer interno de la librería y no termina en nulo. Tratarlo como una cadena C con Serial.println((char*)payload) imprime basura después del mensaje real. Copia siempre usando length, como hace el ejemplo.

Segundo, el callback se ejecuta dentro de mqtt.loop(). Si publicas desde dentro del callback, la librería reutiliza el mismo buffer, así que los punteros originales topic y payload dejan de ser válidos después. Copia todo lo que necesites antes de llamar a publish().

Tercero, mantén los callbacks cortos. Un delay(2000) dentro del callback impide que loop() atienda la conexión y puede provocar un timeout de keep alive. Activa un indicador o guarda el valor y actúa en consecuencia en el bucle principal.

Tamaño del buffer: la trampa más habitual de PubSubClient

Desde la versión 2.8 puedes llamar a setBufferSize() en tiempo de ejecución en lugar de editar MQTT_MAX_PACKET_SIZE en la cabecera de la librería. El buffer debe contener el paquete MQTT completo: cabecera fija, longitud del topic, topic y payload. Con los 256 bytes por defecto, un documento JSON de 220 caracteres en un topic de 40 caracteres simplemente falla, y publish() devuelve false sin más explicación. Los mensajes entrantes demasiado grandes se descartan en silencio antes de que se llegue a llamar a tu callback.

Reglas prácticas:

  • Dimensiona el buffer para tu topic más largo más el payload, más unos 10 bytes de cabecera.
  • Llama a setBufferSize() antes de connect() y comprueba su valor de retorno: devuelve false si la reserva de memoria ha fallado.
  • En el ESP8266 mantén un buffer moderado, porque la RAM se comparte con TLS y la Wi-Fi.

Entramos en detalle, incluido el envío por streaming de payloads grandes con beginPublish(), en tamaño del buffer de PubSubClient: solucionar fallos con mensajes grandes.

Keep alive y timeout del socket

El keep alive indica al broker cuánto tiempo puede permanecer en silencio el cliente. PubSubClient envía un PINGREQ cuando no se ha enviado nada más durante el intervalo, pero solo si se está llamando a loop(). Si tu código se bloquea más de aproximadamente una vez y media el intervalo de keep alive, el broker cierra la sesión y se dispara tu Last Will.

  • setKeepAlive(30) es un valor por defecto sensato para dispositivos alimentados por red con una Wi-Fi decente.
  • Súbelo (de 60 a 120 segundos) en enlaces celulares, donde cada paquete cuesta energía o datos.
  • setSocketTimeout(10) limita cuánto pueden bloquear connect() y las lecturas. Valores más bajos evitan que una red inestable congele tu dispositivo 15 segundos cada vez.

Leer el estado de la conexión

Cuando connect() falla, mqtt.state() te dice por qué. Los valores negativos son problemas a nivel de red; los positivos son rechazos del broker.

state() Significado
-4 Timeout de conexión, no llegó el CONNACK a tiempo
-3 Conexión perdida
-2 Fallo de conexión de red (DNS, puerto, TLS, enrutamiento)
-1 Desconectado limpiamente
0 Conectado
1 Versión de protocolo incorrecta
2 Client ID rechazado
3 Servidor no disponible
4 Usuario o contraseña incorrectos
5 No autorizado

El más habitual con diferencia es -2. Nuestra guía específica sobre PubSubClient rc=-2 y otros códigos de estado repasa cada valor con sus soluciones.

Pasar a TLS y credenciales reales

El servidor público es perfecto para aprender y prototipar, pero los dispositivos en producción necesitan cifrado y credenciales propias. Con un broker MQTT gestionado de ConnectMQ creas un broker en el portal y recibes un usuario, una contraseña y un client ID generados. El cambio en el sketch es pequeño:

#include <WiFiClientSecure.h>

WiFiClientSecure netClient;   // sustituye a WiFiClient
PubSubClient mqtt(netClient);

// ISRG Root X1, la raíz de Let's Encrypt que usa broker.connectmq.com
const char* ROOT_CA = R"EOF(
-----BEGIN CERTIFICATE-----
...paste ISRG Root X1 PEM here...
-----END CERTIFICATE-----
)EOF";

void setup() {
  // ... Wi-Fi como antes ...
  netClient.setCACert(ROOT_CA);
  mqtt.setServer("broker.connectmq.com", 8883);
}

bool connectMqtt() {
  return mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD",
                      topicStatus, 1, true, "offline");
}

ConnectMQ vincula cada client ID a las credenciales que emitió, así que usa exactamente el client ID del portal. En un broker privado, los prefijos de topic los diseñas tú; el prefijo público connectmq/demo/... ya no es necesario. Para más ideas de refuerzo, consulta buenas prácticas de seguridad MQTT.

Lista de comprobación para resolver problemas

  1. Se conecta y se desconecta cada pocos segundos: dos dispositivos comparten un client ID. El broker desconecta la sesión más antigua cada vez que se conecta la segunda.
  2. publish() devuelve false: el payload más el topic superan el buffer, o el cliente no está conectado.
  3. El callback nunca se ejecuta: olvidaste mqtt.loop(), la suscripción se hizo antes de que existiera la conexión o el mensaje es mayor que el buffer.
  4. Desconexiones aleatorias con carga: código bloqueante largo en loop(); divídelo o muévelo a un temporizador.
  5. Funciona en 1883 y falla en 8883: certificado raíz incorrecto, o el reloj no está configurado en placas que comprueban las fechas de validez (habitual en ESP8266).

Si estás eligiendo entre librerías, la guía de la librería ArduinoMqttClient y el cliente MQTT asíncrono para ESP32 cubren las principales alternativas, y el tutorial de IoT con ESP32 reúne todas las piezas en un proyecto de sensores completo.

Conecta tus dispositivos PubSubClient a ConnectMQ

Cuando tu sketch funcione con el broker público, pasarlo a un broker MQTT en la nube privado es un cambio de dos líneas. ConnectMQ te ofrece MQTT sobre TCP, TLS en el 8883 y WebSockets, un cliente MQTT en el navegador para pruebas, un explorador de topics en vivo y reglas de alerta por umbral. Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito, y tendrás credenciales en un par de minutos.

Preguntas frecuentes

¿PubSubClient es compatible con MQTT 5?

No. PubSubClient habla MQTT 3.1.1, que todos los brokers habituales, incluido ConnectMQ, siguen admitiendo junto a MQTT 5. Si necesitas propiedades de MQTT 5 en el dispositivo, elige otra librería cliente.

¿Puede PubSubClient publicar con QoS 1?

No. publish() siempre envía con QoS 0. Puedes suscribirte con QoS 1, de modo que el broker entrega de forma fiable los comandos enviados al dispositivo con QoS 1.

¿Por qué mi payload de PubSubClient muestra caracteres extraños?

El buffer del payload no termina en nulo. Copia length bytes en tu propio buffer y añade un \0 final antes de tratarlo como una cadena.

¿Cuál es el tamaño máximo de mensaje en PubSubClient?

El tamaño del buffer es un valor de 16 bits, así que el techo práctico está justo por debajo de 64 KB, pero el límite real es la RAM. Mantén los mensajes pequeños en placas con recursos limitados y recuerda que el plan de tu broker también tiene un tamaño máximo de mensaje.

Artículos relacionados

Ejemplo de ESP-IDF MQTT: crea un cliente esp-mqtt desde cero

Construye un proyecto ESP-IDF desde una carpeta vacía con el componente esp-mqtt: configuración de Wi-Fi en modo estación, el manejador de eventos MQTT, publicación y suscripción, propiedades de MQTT 5 y TLS con el bundle de certificados.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.