ESP32 y Arduino

Cliente MQTT asíncrono en ESP32: MQTT sin bloqueos con AsyncMqttClient

Descubre cómo AsyncMqttClient mantiene ágil el firmware del ESP32: callbacks basados en eventos, reconexión automática con timers de FreeRTOS, publicación con QoS 1 y 2 y gestión de payloads fragmentados.

Placa ESP32 con flechas de eventos que ilustran un async MQTT client en ESP32 enviando mensajes sin bloquear
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

Un cliente MQTT asíncrono en el ESP32 permite que tu firmware siga leyendo sensores, refrescando pantallas y sirviendo páginas web mientras el tráfico MQTT se gestiona en segundo plano. En esta guía construirás un sketch completo con AsyncMqttClient, con callbacks de eventos, reconexión automática y publicación con QoS 1 contra un broker real. También conocerás las trampas de la librería para que puedas decidir cuándo es la opción adecuada.

Por qué usar un cliente MQTT asíncrono

Las librerías bloqueantes como PubSubClient funcionan bien, pero dependen de que llames a loop() con frecuencia, y connect() puede congelar tu sketch hasta que venza el tiempo de espera del socket cuando la red va mal. En un dispositivo que además mueve un motor, refresca una tira LED o aloja una página de configuración, esas pausas se notan.

AsyncMqttClient, de Marvin Roger, sigue otro enfoque. Funciona sobre la librería AsyncTCP, que ejecuta la red en su propia tarea de FreeRTOS. Tu código registra callbacks para los eventos (conectado, desconectado, mensaje recibido, publicación confirmada) y la librería los llama cuando ocurre algo. No hay ningún loop() que llamar.

Aspecto PubSubClient AsyncMqttClient
Modelo de programación Sondeo, llamar a loop() Callbacks de eventos
Comportamiento de connect() Bloquea hasta el CONNACK o el timeout Retorna de inmediato
QoS de publicación 0 0, 1 y 2
QoS de suscripción 0 y 1 0, 1 y 2
Payloads entrantes grandes Deben caber en el buffer Se entregan por fragmentos
TLS en ESP32 Sí, mediante WiFiClientSecure Poco práctico con AsyncTCP estándar
Dependencias Ninguna AsyncTCP

Esa última fila importa. El soporte TLS de AsyncMqttClient depende de una compilación de AsyncTCP con SSL, que no está disponible en la librería AsyncTCP estándar para ESP32. En la práctica, esta librería es ideal para MQTT sin cifrar en el puerto 1883 dentro de una red de confianza, o para prototipos. Si necesitas TLS con una API basada en eventos, la librería espMqttClient, que se mantiene activamente, ofrece un estilo de callbacks muy parecido con soporte TLS en ESP32. Al final analizamos las ventajas e inconvenientes.

Instalar las librerías

  1. Instala el paquete de placas "esp32 by Espressif Systems" en el Arduino IDE.
  2. Instala AsyncTCP para ESP32. En el Gestor de librerías hay varios forks mantenidos; elige uno que encaje con la versión de tu núcleo ESP32.
  3. Instala AsyncMqttClient. Si no aparece en el Gestor de librerías, descarga el ZIP de la release desde el repositorio del proyecto y usa Programa → Incluir librería → Añadir biblioteca .ZIP.
  4. Con PlatformIO, añade ambas a lib_deps en platformio.ini.
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
    marvinroger/AsyncMqttClient
    esp32async/AsyncTCP

El sketch completo de async MQTT client para ESP32

Este ejemplo se conecta a broker.connectmq.com, el servidor MQTT público gratuito de ConnectMQ, en el puerto 1883. Úsalo para pruebas y prototipos, no para datos de producción. Reconecta el Wi-Fi y MQTT con timers de FreeRTOS, publica telemetría con QoS 1 cada diez segundos y conmuta el LED integrado cuando llega un comando.

#include <WiFi.h>
#include <AsyncMqttClient.h>

extern "C" {
  #include "freertos/FreeRTOS.h"
  #include "freertos/timers.h"
}

#define WIFI_SSID     "YOUR_WIFI"
#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD"

#define MQTT_HOST "broker.connectmq.com"
#define MQTT_PORT 1883

#define LED_PIN 2

static const char* DEVICE_ID = "async-4b81d2";   // hazlo único
String topicTelemetry = String("connectmq/demo/") + DEVICE_ID + "/telemetry";
String topicCommand   = String("connectmq/demo/") + DEVICE_ID + "/led/set";
String topicStatus    = String("connectmq/demo/") + DEVICE_ID + "/status";

AsyncMqttClient mqttClient;
TimerHandle_t mqttReconnectTimer;
TimerHandle_t wifiReconnectTimer;

unsigned long lastPublish = 0;

void connectToWifi() {
  Serial.println("Connecting to Wi-Fi...");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}

void connectToMqtt() {
  Serial.println("Connecting to MQTT...");
  mqttClient.connect();
}

void onWifiEvent(WiFiEvent_t event) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      Serial.print("Wi-Fi connected, IP ");
      Serial.println(WiFi.localIP());
      connectToMqtt();
      break;
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
      Serial.println("Wi-Fi lost");
      xTimerStop(mqttReconnectTimer, 0);   // nada de MQTT mientras el Wi-Fi está caído
      xTimerStart(wifiReconnectTimer, 0);
      break;
    default:
      break;
  }
}

void onMqttConnect(bool sessionPresent) {
  Serial.printf("MQTT connected, session present: %d\n", sessionPresent);
  mqttClient.subscribe(topicCommand.c_str(), 1);
  mqttClient.publish(topicStatus.c_str(), 1, true, "online");
}

void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
  Serial.printf("MQTT disconnected, reason %d\n", static_cast<int>(reason));
  if (WiFi.isConnected()) {
    xTimerStart(mqttReconnectTimer, 0);
  }
}

void onMqttSubscribe(uint16_t packetId, uint8_t qos) {
  Serial.printf("Subscribe ack, packetId %u, granted QoS %u\n", packetId, qos);
}

void onMqttPublish(uint16_t packetId) {
  Serial.printf("Publish ack (PUBACK), packetId %u\n", packetId);
}

void onMqttMessage(char* topic, char* payload,
                   AsyncMqttClientMessageProperties properties,
                   size_t len, size_t index, size_t total) {
  // el payload no termina en nulo y puede ser un fragmento de un mensaje mayor
  if (index != 0 || len != total) {
    Serial.println("Fragmented message ignored in this demo");
    return;
  }
  String msg;
  msg.reserve(len);
  for (size_t i = 0; i < len; i++) msg += payload[i];

  Serial.printf("Message on %s (QoS %u): %s\n", topic, properties.qos, msg.c_str());

  if (msg == "ON")  digitalWrite(LED_PIN, HIGH);
  if (msg == "OFF") digitalWrite(LED_PIN, LOW);
}

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

  mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE,
                                    nullptr, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
  wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE,
                                    nullptr, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));

  WiFi.onEvent(onWifiEvent);

  mqttClient.onConnect(onMqttConnect);
  mqttClient.onDisconnect(onMqttDisconnect);
  mqttClient.onSubscribe(onMqttSubscribe);
  mqttClient.onPublish(onMqttPublish);
  mqttClient.onMessage(onMqttMessage);

  mqttClient.setServer(MQTT_HOST, MQTT_PORT);
  mqttClient.setClientId(DEVICE_ID);              // el puntero debe seguir siendo válido
  mqttClient.setKeepAlive(30);
  mqttClient.setWill(topicStatus.c_str(), 1, true, "offline");

  connectToWifi();
}

void loop() {
  // Tu código de aplicación se ejecuta libremente aquí; MQTT se gestiona en segundo plano.
  if (mqttClient.connected() && millis() - lastPublish > 10000) {
    lastPublish = millis();
    char payload[96];
    snprintf(payload, sizeof(payload), "{\"uptime\":%lu,\"rssi\":%d,\"heap\":%u}",
             millis() / 1000, WiFi.RSSI(), (unsigned)ESP.getFreeHeap());
    uint16_t id = mqttClient.publish(topicTelemetry.c_str(), 1, false, payload);
    Serial.printf("Queued publish, packetId %u\n", id);
  }
}

Observa el tráfico y envía comandos desde tu PC:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-4b81d2 -t "connectmq/demo/async-4b81d2/#" -v -q 1
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-4b81d2 -t "connectmq/demo/async-4b81d2/led/set" -m ON -q 1

Cómo funciona el flujo de eventos

Entender el orden de los eventos evita la mayoría de los errores:

  1. connectToWifi() inicia la conexión Wi-Fi y retorna al instante.
  2. Cuando la estación obtiene una dirección IP, onWifiEvent llama a connectToMqtt().
  3. mqttClient.connect() retorna de inmediato. Más tarde, la tarea de AsyncTCP recibe el CONNACK y la librería llama a onMqttConnect.
  4. Dentro de onMqttConnect te suscribes y publicas tu estado "online". Es el lugar adecuado, porque las suscripciones de una sesión limpia deben renovarse tras cada reconexión.
  5. Si el broker o la red te desconectan, onMqttDisconnect programa un timer de un solo disparo, que llama a connectToMqtt() dos segundos después.

publish() devuelve un packet ID para los mensajes con QoS 1 y 2, 1 para un mensaje QoS 0 que se ha encolado y 0 cuando el mensaje no se pudo enviar. El onMqttPublish(packetId) correspondiente se dispara cuando el broker confirma la entrega, lo que te permite comprobar si los mensajes importantes llegaron de verdad.

Trampas que aparecen en producción

Los callbacks se ejecutan en otra tarea

Tus callbacks se ejecutan en la tarea de AsyncTCP, no en loop(). No llames a delay() ni hagas E/S lenta ahí; hacerlo bloquea todo el tráfico de red y puede disparar el watchdog de tareas. Copia los datos a una variable o una cola y procésalos en loop(). Si loop() y un callback comparten una variable, protégela, por ejemplo con una cola de FreeRTOS o, en casos sencillos, con un flag volatile.

Vida útil de las cadenas

setClientId(), setCredentials(), setServer() con un nombre de host y setWill() guardan punteros, no copias. Pasar el c_str() de un String temporal hace que se envíe basura al reconectar. Usa constantes globales u objetos String globales, como hace el ejemplo.

Payloads fragmentados

Los mensajes entrantes grandes llegan en varias llamadas a onMessage con un index creciente y el mismo total. Si esperas payloads de más de unos cientos de bytes, acumula los fragmentos en un buffer de tamaño total y procésalo cuando index + len == total.

No hay reconexión integrada

La librería no se reconecta sola. El patrón de timers anterior es el enfoque estándar. Detén siempre el timer de MQTT cuando cae el Wi-Fi; de lo contrario, lanzarás intentos de conexión que no pueden tener éxito.

Sesiones persistentes para no perder comandos

Por defecto, el cliente se conecta con una sesión limpia, así que los comandos enviados mientras el dispositivo estaba desconectado se pierden. Llama a mqttClient.setCleanSession(false) antes de conectar y suscríbete con QoS 1: el broker conservará la sesión y encolará los mensajes QoS 1 para el dispositivo hasta que vuelva. La sesión no dura para siempre; los brokers la expiran pasado un tiempo (en el servidor público, las sesiones expiran como máximo a los 30 minutos). Comprueba sessionPresent en onMqttConnect para saber si el broker te recordaba y vuelve a suscribirte si no era así.

Los límites del broker siguen aplicándose

Asíncrono no significa ilimitado. El servidor público permite 10 mensajes por segundo por cliente, 10 suscripciones por cliente y 64 KB por mensaje. Un bucle cerrado que llame a publish() alcanzará esos límites enseguida.

Elegir entre clientes asíncronos y bloqueantes

Usa esta lista para decidir:

  1. ¿Necesitas TLS en el puerto 8883? Elige PubSubClient con WiFiClientSecure o el cliente seguro de espMqttClient. AsyncMqttClient estándar no encaja bien.
  2. ¿Necesitas publicar con QoS 1 o 2 desde el dispositivo? AsyncMqttClient o espMqttClient.
  3. ¿Ejecutas un servidor web (ESPAsyncWebServer) en el mismo dispositivo? AsyncMqttClient comparte muy bien la pila AsyncTCP.
  4. ¿También apuntas a ESP8266? AsyncMqttClient funciona allí con ESPAsyncTCP, aunque la memoria es más justa. Consulta el tutorial de ESP8266 MQTT.
  5. ¿Quieres el código más sencillo para un proyecto de iniciación? El tutorial de PubSubClient es un comienzo más suave.

Si lo que te preocupa son las garantías de entrega, lee los niveles de QoS de MQTT explicados antes de cambiar de librería solo por QoS 2; muchas cargas de trabajo de sensores se resuelven mejor con QoS 0 o 1. El tutorial de IoT con ESP32, más amplio, muestra cómo encaja MQTT en un proyecto de sensores completo.

Configuración de producción con ConnectMQ

Para despliegues fuera de tu laboratorio, conecta el dispositivo a un broker MQTT en la nube dedicado en lugar de al área pública. En ConnectMQ creas un broker en el portal y recibes un usuario, una contraseña y un client ID. Con AsyncMqttClient se configuran así, teniendo en cuenta que los client IDs están vinculados a las credenciales emitidas:

mqttClient.setServer("broker.connectmq.com", 1883);
mqttClient.setClientId("YOUR_CLIENT_ID");
mqttClient.setCredentials("YOUR_USERNAME", "YOUR_PASSWORD");

Como las credenciales en el puerto 1883 viajan sin cifrar, para dispositivos fuera de una red de confianza es preferible una librería con soporte TLS.

Empieza con ConnectMQ

ConnectMQ es un broker MQTT gestionado con MQTT 3.1.1 y 5.0, TLS, WebSockets, un explorador de topics en vivo y alertas por umbral en el portal, para que puedas ver cómo publica tu ESP32 en tiempo real. Crea un broker Serverless gratuito en app.connectmq.com sin tarjeta de crédito y conecta tu primer dispositivo asíncrono en minutos.

Preguntas frecuentes

¿Funciona AsyncMqttClient con TLS en el ESP32?

No con la librería AsyncTCP estándar, que no tiene una compilación con SSL para ESP32. Usa PubSubClient con WiFiClientSecure o una librería asíncrona mantenida como espMqttClient cuando necesites el puerto 8883.

¿Por qué necesito timers de FreeRTOS para reconectar?

AsyncMqttClient no tiene reconexión automática. Los timers de un solo disparo te permiten reintentar tras una breve espera sin bloquear loop() ni la tarea de red.

¿Puedo llamar a delay() dentro de onMqttMessage?

No. Los callbacks se ejecutan en la tarea de AsyncTCP, y bloquearla detiene todo el tráfico de red. Guarda el mensaje y procésalo en loop().

¿Se sigue manteniendo AsyncMqttClient?

La librería original lleva tiempo sin desarrollo activo, aunque sigue siendo muy utilizada. Revisa el estado del repositorio antes de empezar un producto de larga vida y considera espMqttClient como alternativa mantenida.

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.