Tutoriales

Tutorial de MQTT con ESP32 y Arduino (PubSubClient)

Tutorial de MQTT con ESP32, Arduino y PubSubClient: conecta al WiFi y a un broker, publica JSON de sensores, suscríbete a comandos, reconecta bien y activa TLS.

Actualizado · 8 min de lectura

El ESP32 es el microcontrolador de referencia para proyectos MQTT: WiFi integrado, RAM de sobra para TLS y un soporte de Arduino de primera. Este tutorial construye un sketch completo con la librería PubSubClient que publica lecturas de sensores en JSON, escucha en un topic de comandos, se reconecta solo y — en el último paso — pasa a TLS.

Qué necesitas

  • Una placa de desarrollo ESP32 y un cable USB.
  • Arduino IDE 2.x (o PlatformIO) con el paquete de placas esp32 by Espressif Systems instalado desde el Gestor de placas.
  • La librería PubSubClient de Nick O'Leary, instalada desde el Gestor de librerías.
  • Un broker. Usamos el broker público de HiveMQ en el puerto 1883 para las pruebas — vale para una demo, pero cualquiera puede leer los topics.

Estructura de topics

Una estructura de topics limpia hace que los dispositivos sean fáciles de gestionar. Cada placa tiene su propio prefijo basado en un ID de dispositivo único:

TopicDirecciónPropósito
testmqtt/esp32/<id>/telemetryESP32 al brokerJSON del sensor cada 10 segundos
testmqtt/esp32/<id>/statusESP32 al brokeronline / offline (retenido, Last Will)
testmqtt/esp32/<id>/cmdBroker al ESP32Comandos como led:on

Consulta topics y comodines MQTT para ver convenciones de nombres.

El sketch completo

esp32_mqtt.ino
#include <WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID     = "your-ssid";
const char* WIFI_PASSWORD = "your-wifi-password";

const char* MQTT_HOST = "broker.hivemq.com";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = nullptr;   // set for brokers that require auth
const char* MQTT_PASS = nullptr;

const int LED_PIN = 2;             // on-board LED on many dev boards

WiFiClient net;
PubSubClient mqtt(net);

char deviceId[24];
char topicTelemetry[64];
char topicStatus[64];
char topicCmd[64];

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

void onMessage(char* topic, byte* payload, unsigned int length) {
  String msg;
  for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
  Serial.printf("Message on %s: %s\n", topic, msg.c_str());

  if (msg == "led:on")  digitalWrite(LED_PIN, HIGH);
  if (msg == "led:off") digitalWrite(LED_PIN, LOW);
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nWiFi connected, IP %s\n", WiFi.localIP().toString().c_str());
}

bool connectMqtt() {
  // Last Will: broker publishes "offline" (retained) if we drop unexpectedly
  bool ok = mqtt.connect(deviceId, MQTT_USER, MQTT_PASS,
                         topicStatus, 1, true, "offline");
  if (!ok) {
    Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
    return false;
  }
  Serial.println("MQTT connected");
  mqtt.publish(topicStatus, "online", true);
  mqtt.subscribe(topicCmd, 1);
  return true;
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);

  // Unique client ID from the chip's MAC address
  uint64_t mac = ESP.getEfuseMac();
  snprintf(deviceId, sizeof(deviceId), "esp32-%04X%08X",
           (uint16_t)(mac >> 32), (uint32_t)mac);
  snprintf(topicTelemetry, sizeof(topicTelemetry), "testmqtt/esp32/%s/telemetry", deviceId);
  snprintf(topicStatus, sizeof(topicStatus), "testmqtt/esp32/%s/status", deviceId);
  snprintf(topicCmd, sizeof(topicCmd), "testmqtt/esp32/%s/cmd", deviceId);
  Serial.printf("Device ID: %s\n", deviceId);

  connectWiFi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);   // default is 256 bytes for the whole packet
  mqtt.setKeepAlive(30);
}

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

  if (!mqtt.connected()) {
    // Non-blocking reconnect: try every 5 seconds without stalling loop()
    unsigned long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      if (connectMqtt()) lastReconnectAttempt = 0;
    }
    return;
  }

  mqtt.loop();   // must run often: handles keep-alive and incoming messages

  unsigned long now = millis();
  if (now - lastPublish > 10000) {
    lastPublish = now;
    float temperature = temperatureRead();   // ESP32 internal sensor (rough)
    char payload[128];
    snprintf(payload, sizeof(payload),
             "{\"device\":\"%s\",\"temp\":%.1f,\"rssi\":%d,\"uptime\":%lu}",
             deviceId, temperature, WiFi.RSSI(), now / 1000);
    bool sent = mqtt.publish(topicTelemetry, payload);
    Serial.printf("Publish %s: %s\n", sent ? "ok" : "FAILED", payload);
  }
}

Abre el Monitor Serie a 115200 baudios para ver el ID del dispositivo y luego observa cómo llega la telemetría al navegador suscribiéndote a testmqtt/esp32/#. Publica led:on en el topic cmd y el LED se encenderá.

Mira la telemetría de tu ESP32 en directo

Cómo funciona el sketch

Client ID únicos

Los brokers MQTT permiten solo una conexión por client ID. Si dos placas graban el mismo ID fijo, como ESP32Client, se desconectan mutuamente en un bucle infinito. Derivar el ID de la dirección MAC de la eFuse hace que sea único y estable entre reinicios. El generador de client ID explica las reglas si quieres otro esquema.

Un bucle de reconexión no bloqueante

Muchos ejemplos usan un bucle while (!client.connected()) con delay(5000). Eso bloquea todo lo demás en la placa. El patrón con millis() de arriba reintenta cada cinco segundos mientras el resto de loop() sigue ejecutándose. mqtt.state() te dice por qué falló una conexión: -2 es un fallo de red, 4 credenciales incorrectas y 5 no autorizado. Compáralo con la lista completa de nuestra guía de errores de conexión MQTT.

Tamaño del buffer y MQTT_MAX_PACKET_SIZE

El buffer de PubSubClient debe contener el paquete entero: cabecera fija, topic y payload. Por defecto son 256 bytes, y un payload JSON con un topic largo lo supera enseguida — entonces publish() devuelve false en silencio. Los tutoriales antiguos te dicen que edites MQTT_MAX_PACKET_SIZE en la cabecera de la librería; desde la versión 2.8 basta con llamar a setBufferSize() antes de conectar. La calculadora de tamaño de paquete MQTT te dice cuánto ocupan realmente tus paquetes.

Limitaciones de PubSubClient

  • Solo MQTT 3.1.1 — sin propiedades de MQTT 5.
  • Publica solo con QoS 0; las suscripciones pueden usar QoS 0 o 1.
  • Se admiten publicaciones retenidas y Last Will, como se muestra arriba.

Estado online con Last Will y mensajes retenidos

La llamada a connect() registra un Last Will: si la placa se queda sin alimentación o sin WiFi sin una desconexión limpia, el broker publica offline en el topic de estado tras el timeout de keep-alive. Justo después de conectarse, el sketch publica online en el mismo topic. Ambos son retenidos, así que un dashboard que se suscriba más tarde sigue viendo el estado actual de inmediato. Este patrón se explica en Last Will and Testament y en mensajes retenidos.

Probar desde la línea de comandos

No necesitas un segundo dispositivo para probar el topic de comandos. Con los clientes de Mosquitto instalados, envía un comando y observa la telemetría (sustituye el ID por el que aparece en el Monitor Serie):

bash
# watch everything your board publishes
mosquitto_sub -h broker.hivemq.com -t 'testmqtt/esp32/#' -v

# switch the LED on
mosquitto_pub -h broker.hivemq.com -t 'testmqtt/esp32/esp32-ABCD12345678/cmd' -m 'led:on'

Tienes más recetas de línea de comandos en ejemplos de mosquitto_pub y mosquitto_sub.

Construir JSON con ArduinoJson

snprintf sirve para unos pocos campos, pero para datos anidados o para parsear comandos en JSON, la librería ArduinoJson (versión 7) es más segura:

cpp
#include <ArduinoJson.h>

void publishTelemetry() {
  JsonDocument doc;
  doc["device"] = deviceId;
  doc["temp"] = temperatureRead();
  doc["rssi"] = WiFi.RSSI();

  char payload[256];
  size_t n = serializeJson(doc, payload, sizeof(payload));
  mqtt.publish(topicTelemetry, (const uint8_t*)payload, n, false);
}

Mantén los payloads pequeños y planos: claves cortas y números en lugar de cadenas ahorran RAM, tiempo de transmisión y ancho de banda del broker.

Solución de problemas con los códigos de estado

mqtt.state()SignificadoSolución típica
-4Timeout de conexiónRevisa el keep-alive y que loop() se ejecute a menudo
-2Falló la conexión de redHost o puerto incorrectos, firewall o fallo del handshake TLS
2Client ID rechazadoUsa un client ID más corto o único
4Usuario o contraseña incorrectosRevisa las credenciales, incluidas mayúsculas y minúsculas
5No autorizadoLas reglas de acceso del broker deniegan a este cliente

Añadir TLS con WiFiClientSecure

El puerto 1883 sin cifrar envía tus credenciales y datos en texto plano. Pasar a TLS en el puerto 8883 requiere tres cambios: incluir WiFiClientSecure.h, cambiar el cliente de red y darle el certificado de la CA de tu broker:

Cambios para TLS
#include <WiFiClientSecure.h>

// PEM of the root CA that signed your broker's certificate
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the root CA certificate here...
-----END CERTIFICATE-----
)EOF";

WiFiClientSecure net;
PubSubClient mqtt(net);

const uint16_t MQTT_PORT = 8883;

// in setup(), before mqtt.setServer():
net.setCACert(ROOT_CA);
// net.setInsecure();  // skips verification - local testing only!

Muchos brokers públicos usan certificados de Let's Encrypt, cuya raíz es ISRG Root X1; consulta la documentación de tu broker para saber cuál es la correcta. En algunas configuraciones, TLS también necesita la hora correcta para comprobar la validez de los certificados, y consume bastante más RAM — un ESP32 lo lleva bien, un ESP8266 sufre. Para tener la visión completa, lee nuestras buenas prácticas de seguridad MQTT.

¿Quieres este sketch con el host, el puerto y las credenciales de tu broker ya rellenados? El generador de código MQTT lo genera con la configuración de tu propio broker.

Preguntas frecuentes

¿Por qué PubSubClient no publica mensajes de más de 256 bytes?

PubSubClient usa por defecto un buffer de 256 bytes, y ese buffer debe contener el paquete MQTT entero, incluido el topic. publish() devuelve false cuando el paquete no cabe. Llama a client.setBufferSize(1024) o a un valor mayor antes de conectar.

¿Por qué mi ESP32 se desconecta continuamente del broker MQTT?

Las causas más habituales son un client ID que otro dispositivo también usa, llamadas largas a delay() que impiden que se ejecute client.loop() y hacen que se pierdan los pings de keep-alive, y un WiFi débil. Usa un client ID único por dispositivo y mantén loop() no bloqueante.

¿PubSubClient soporta QoS 1 y MQTT 5?

PubSubClient solo habla MQTT 3.1.1. Puede suscribirse con QoS 0 o 1, pero solo publica con QoS 0. Si necesitas publicar con QoS 1 o funciones de MQTT 5, mira librerías como AsyncMqttClient o el componente esp-mqtt de ESP-IDF.