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
- Instala el paquete de placas "esp32 by Espressif Systems" en el Arduino IDE.
- 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.
- 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.
- Con PlatformIO, añade ambas a
lib_depsenplatformio.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:
connectToWifi()inicia la conexión Wi-Fi y retorna al instante.- Cuando la estación obtiene una dirección IP,
onWifiEventllama aconnectToMqtt(). mqttClient.connect()retorna de inmediato. Más tarde, la tarea de AsyncTCP recibe el CONNACK y la librería llama aonMqttConnect.- Dentro de
onMqttConnectte suscribes y publicas tu estado "online". Es el lugar adecuado, porque las suscripciones de una sesión limpia deben renovarse tras cada reconexión. - Si el broker o la red te desconectan,
onMqttDisconnectprograma un timer de un solo disparo, que llama aconnectToMqtt()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:
- ¿Necesitas TLS en el puerto 8883? Elige PubSubClient con
WiFiClientSecureo el cliente seguro de espMqttClient. AsyncMqttClient estándar no encaja bien. - ¿Necesitas publicar con QoS 1 o 2 desde el dispositivo? AsyncMqttClient o espMqttClient.
- ¿Ejecutas un servidor web (ESPAsyncWebServer) en el mismo dispositivo? AsyncMqttClient comparte muy bien la pila AsyncTCP.
- ¿También apuntas a ESP8266? AsyncMqttClient funciona allí con ESPAsyncTCP, aunque la memoria es más justa. Consulta el tutorial de ESP8266 MQTT.
- ¿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.



