La función ESP32 MQTT Last Will permite que el broker anuncie en nombre de tu dispositivo que se ha quedado offline, incluso cuando el dispositivo se colgó o perdió la alimentación y no pudo despedirse por sí mismo. En este tutorial configurarás un Last Will retenido, un mensaje de nacimiento a juego y un mensaje de apagado limpio, para que todos los dashboards y scripts vean en todo momento un estado online/offline preciso.
Qué hace el Last Will and Testament
Cuando un cliente MQTT se conecta, puede entregar al broker un mensaje para publicarlo más tarde: un topic, un payload, un nivel de QoS y un flag retain. Eso es el Last Will and Testament (LWT). El broker lo guarda y solo lo publica si el cliente desaparece sin enviar un paquete DISCONNECT como es debido. Eso cubre exactamente los casos que te interesan:
- El dispositivo perdió la alimentación o sufrió un brownout.
- El firmware falló, se colgó o lo reinició un watchdog.
- Se cayó el Wi-Fi y expiró el temporizador de keepalive.
- Falló la red entre el dispositivo y el broker.
Si el cliente se desconecta limpiamente, el broker descarta el will. Por eso un apagado limpio necesita su propia publicación "offline", descrita más abajo.
Por sí solo, un will solo anuncia la muerte. Combínalo con un mensaje de nacimiento, una publicación "online" retenida que se envía justo después de cada conexión correcta, y el topic de estado reflejará siempre la verdad actual: online mientras el dispositivo está conectado, offline desde el momento en que el broker detecta que ya no está.
El patrón del topic de estado
| Evento | Quién publica | Topic | Payload | Retenido |
|---|---|---|---|---|
| El dispositivo se conecta | ESP32 (nacimiento) | connectmq/demo/f5c820/<device>/status |
online |
Sí |
| El dispositivo muere inesperadamente | Broker (will) | el mismo | offline |
Sí |
| El dispositivo se apaga limpiamente | ESP32 | el mismo | offline |
Sí |
Ambos mensajes deben ser retenidos. Sin el flag retain, un dashboard que arranque después de que el dispositivo se pusiera en línea nunca conocería su estado, porque el mensaje de nacimiento ya habría pasado. Con él, cada nuevo suscriptor recibe de inmediato el último estado.
Usa el mismo topic para el nacimiento y el will, y mantén los payloads sencillos. Un simple online y offline funciona con los topics de disponibilidad de Home Assistant, los interruptores de Node-RED y la mayoría de los dashboards sin ningún análisis. Si quieres más detalle, usa JSON en ambos, por ejemplo {"state":"online","fw":"1.2.0"}.
ESP32 MQTT Last Will con PubSubClient
PubSubClient establece el will mediante una sobrecarga de connect():
boolean connect(const char* id,
const char* user, const char* pass,
const char* willTopic, uint8_t willQos,
boolean willRetain, const char* willMessage);
Para conexiones anónimas, como el área pública de broker.connectmq.com, existe una variante sin user ni pass. Este es un sketch completo que publica el mensaje de nacimiento, mantiene preciso el estado y se apaga limpiamente al pulsar el botón BOOT:
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const int SHUTDOWN_BUTTON = 0; // botón BOOT
WiFiClient net;
PubSubClient mqtt(net);
String deviceId, statusTopic, telemetryTopic;
unsigned long lastPublish = 0;
void connectMqtt() {
while (!mqtt.connected()) {
bool ok = mqtt.connect(deviceId.c_str(),
statusTopic.c_str(), // topic del will
1, // QoS del will
true, // retain del will
"offline"); // payload del will
if (ok) {
mqtt.publish(statusTopic.c_str(), "online", true); // mensaje de nacimiento
Serial.println("Connected, status=online");
} else {
Serial.printf("Connect failed, state=%d\n", mqtt.state());
delay(3000);
}
}
}
void cleanShutdown() {
mqtt.publish(statusTopic.c_str(), "offline", true);
mqtt.loop();
delay(100);
mqtt.disconnect(); // el broker descarta el will
WiFi.disconnect(true);
Serial.println("Clean shutdown done");
esp_deep_sleep_start(); // dormir hasta el reset
}
void setup() {
Serial.begin(115200);
pinMode(SHUTDOWN_BUTTON, INPUT_PULLUP);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
deviceId = "esp32-lwt-" + String((uint32_t)ESP.getEfuseMac(), HEX);
statusTopic = "connectmq/demo/f5c820/" + deviceId + "/status";
telemetryTopic = "connectmq/demo/f5c820/" + deviceId + "/telemetry";
mqtt.setServer("broker.connectmq.com", 1883);
mqtt.setKeepAlive(15); // segundos; el broker detecta la pérdida tras aproximadamente 1,5 veces este valor
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (digitalRead(SHUTDOWN_BUTTON) == LOW) cleanShutdown();
if (millis() - lastPublish > 10000) {
lastPublish = millis();
String payload = "{\"uptime\":" + String(millis() / 1000) + "}";
mqtt.publish(telemetryTopic.c_str(), payload.c_str());
}
}
Probar los tres casos
Suscríbete desde un equipo a todos los topics de estado bajo tu prefijo:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-f5c820 -t 'connectmq/demo/f5c820/+/status' -v
Después prueba cada escenario:
- Arranque normal. Alimenta la placa. Verás
.../status onlineen pocos segundos. - Fallo o corte de alimentación. Desconecta el cable USB. Tras aproximadamente intervalo y medio de keepalive (unos 20–25 segundos con un keepalive de 15 segundos), el broker publica
.../status offline. - Apagado limpio. Vuelve a encenderla, espera a
onliney pulsa BOOT.offlineaparece de inmediato, publicado por el propio dispositivo.
Detén mosquitto_sub y vuelve a iniciarlo con el dispositivo en cualquier estado. Como ambos mensajes son retenidos, recibes al instante el estado actual. Ese es el comportamiento del que dependen tus dashboards.
Ajustar el keepalive
El intervalo de keepalive determina lo rápido que se dispara el will tras un fallo silencioso. El broker espera aproximadamente 1,5 veces el keepalive sin paquetes del cliente antes de dar por perdida la conexión.
| Keepalive | Tiempo aproximado hasta "offline" | Contrapartida |
|---|---|---|
| 5 s | menos de 10 s | Detección rápida, más tráfico de pings, falsos offline con un Wi-Fi deficiente |
| 15 s (por defecto en PubSubClient) | unos 20–25 s | Buen valor por defecto para dispositivos conectados a la red eléctrica |
| 60 s | unos 90 s | Menos tráfico, detección lenta |
Un keepalive corto solo funciona si mqtt.loop() se ejecuta con frecuencia. Si tu bucle se bloquea durante más tiempo que el keepalive, el broker desconecta un dispositivo sano y dispara el will, y verás el estado oscilar entre online y offline. Las llamadas bloqueantes largas también provocan reinicios, como se explica en cómo solucionar los reinicios del task watchdog del ESP32.
Errores comunes
Olvidar el mensaje de nacimiento. Sin él, el estado sigue en offline tras una reconexión, porque el will del fallo anterior sigue retenido en el topic.
No retener el will. Un will no retenido solo llega a los suscriptores conectados en ese momento. Cualquier cosa que arranque después ve el último valor retenido, probablemente online, y cree que un dispositivo muerto sigue vivo.
Compartir un mismo topic de estado entre dispositivos. Cada dispositivo necesita su propio topic de estado, normalmente con el client ID o el nombre del dispositivo en la ruta. Un topic compartido solo muestra el último dispositivo que habló.
Esperar el will en una desconexión limpia. mqtt.disconnect() indica al broker que el cliente se va a propósito, así que el will se descarta. Publica tú mismo offline antes de desconectar, como hace cleanShutdown().
Client IDs duplicados. Si dos dispositivos usan el mismo client ID, el broker desconecta la conexión más antigua cuando se conecta la nueva, lo que dispara el will de la antigua y genera cambios de estado confusos. Deriva el ID de la dirección MAC.
Dispositivos con deep sleep
Los nodos con batería que despiertan, publican y duermen son un caso especial. Si se desconectan limpiamente antes de dormir, el will nunca se dispara, y un topic de estado cambiaría entre online y offline en cada ciclo. Un enfoque mejor para los dispositivos dormilones es publicar una marca de tiempo last_seen retenida o un contador de uptime, y dejar que el consumidor decida que un dispositivo está offline cuando ese valor tiene más de unos cuantos intervalos de antigüedad. La guía del sensor ESP32 alimentado por batería analiza este equilibrio.
Will delay de MQTT 5 y estado en el lado del broker
PubSubClient habla MQTT 3.1.1. Con un cliente MQTT 5 como esp-mqtt de ESP-IDF, también puedes establecer un will delay interval, de modo que el broker espere unos segundos antes de publicar el will. Si el dispositivo se reconecta dentro de esa ventana, por ejemplo tras un breve corte de Wi-Fi, el will se cancela y el estado no oscila. El ejemplo de ESP-IDF MQTT muestra dónde se configura el will, y las funciones de MQTT 5 explica la propiedad.
En el lado del broker, ConnectMQ también publica el estado de conexión de cada cliente en $SYS/broker/connection/{clientId}/state, algo útil como comprobación cruzada cuando depuras un offline inesperado.
Muestra la disponibilidad de tus dispositivos en ConnectMQ
Todo lo anterior funciona en el servidor MQTT público gratuito, así que puedes probar los modos de fallo sin cuenta. Para una flota real, un broker MQTT gestionado privado mantiene los topics de estado lejos de la vista pública. El explorador de topics en vivo de ConnectMQ muestra de un vistazo el estado retenido de cada dispositivo. Crea un broker en el plan gratuito Serverless en app.connectmq.com, sin tarjeta de crédito, y conéctate con tus YOUR_CLIENT_ID, YOUR_USERNAME y YOUR_PASSWORD generados usando la sobrecarga de connect() con credenciales. Para el proyecto de dispositivo completo, consulta el tutorial de IoT con ESP32.
Preguntas frecuentes
¿Cuándo publica el broker el Last Will?
Cuando el cliente desaparece sin enviar DISCONNECT: corte de alimentación, fallo, caída de la red o timeout del keepalive. Un disconnect() limpio cancela el will.
¿Debe ser retenido el mensaje de Last Will?
Sí, en los topics de estado. Retenerlo significa que cualquier suscriptor que se conecte más tarde seguirá viendo que el dispositivo está offline.
¿Qué QoS debo usar para el will?
QoS 1 es un buen valor por defecto, ya que garantiza la entrega a los suscriptores conectados. El plan gratuito Serverless de ConnectMQ soporta QoS 0 y 1, lo que cubre este patrón.
¿Por qué el estado de mi dispositivo parpadea entre online y offline?
Normalmente mqtt.loop() está bloqueado más tiempo que el keepalive, o dos dispositivos comparten client ID. Elimina las llamadas bloqueantes largas y da a cada dispositivo un ID único.



