
Reconexión automática de MQTT sin bloquear el bucle
El bucle while de todos los tutoriales bloquea el firmware cuando cae el broker. El patrón de guardia con millis(), cadencias separadas y códigos rc.
Requisitos de hardware:
- Placa ESP32 ou ESP32-S3
- Rede Wi-Fi de 2,4 GHz
- Um broker MQTT: Mosquitto local, HiveMQ ou equivalente
Descarga el material de este tutorial:
- Projeto completo (ZIP)138 KBFirmware Arduino com o padrão de guarda, cadências separadas de Wi-Fi e MQTT, client id derivado do MAC, Last Will e tradução dos códigos rc. README e manual em PDF.
- Manual em PDF (8 páginas)167 KBPor que o laço trava, o padrão correto, tabela completa dos códigos rc, o roteiro de teste derrubando broker e Wi-Fi, e checklist de treze itens.
¿Quieres recibir nuevos manuales y esquemas industriales? Únete a nuestro canal de Telegram / WhatsApp
El bucle que todos los tutoriales enseñan y que no debes usar
Busca reconexión MQTT en el ESP32 y encontrarás alguna variante de esto:
| 1 | while (!client.connected()) { |
| 2 | Serial.print("Conectando ao broker..."); |
| 3 | if (client.connect("esp32")) { |
| 4 | Serial.println("conectado"); |
| 5 | } else { |
| 6 | Serial.print("falhou, rc="); |
| 7 | Serial.println(client.state()); |
| 8 | delay(5000); |
| 9 | } |
| 10 | } |
Funciona en la mesa de trabajo, con el broker en el mismo ordenador. En campo es una trampa.
Mientras se ejecuta este bucle, todo el firmware está detenido. El servidor web no responde, no se lee el botón físico, no se consulta Modbus y la máquina de estados del proceso no avanza. Si el broker está fuera de servicio durante diez minutos, el dispositivo queda muerto diez minutos; no desconectado, muerto, sin hacer nada aparte de intentar y dormir cinco segundos.
Peor aún: en muchos proyectos esto alimenta el watchdog de forma irregular y el ESP32 se reinicia solo. El diagnóstico acaba siendo "el dispositivo se está reiniciando" y nadie relaciona el reinicio con la caída del broker.
La inversión: una guarda en lugar de un bucle
El patrón correcto no tiene ningún bucle. Llama a la función de conexión en cada vuelta de loop() y deja que ella decida si debe hacer algo:
| 1 | void connectMqtt() { |
| 2 | if (config.mqttHost.isEmpty() || mqtt.connected() || WiFi.status() != WL_CONNECTED) { |
| 3 | return; |
| 4 | } |
| 5 | if (millis() - lastMqttAttemptMs < 5000) { |
| 6 | return; |
| 7 | } |
| 8 | lastMqttAttemptMs = millis(); |
| 9 | mqtt.setServer(config.mqttHost.c_str(), config.mqttPort); |
| 10 | mqtt.setCallback(onMqttMessage); |
| 11 | if (mqtt.connect(config.deviceId.c_str(), config.deviceId.c_str(), config.deviceToken.c_str())) { |
| 12 | mqtt.subscribe(topicFor("commands").c_str()); |
| 13 | } else { |
| 14 | Serial.printf("Falha MQTT rc=%d\\n", mqtt.state()); |
| 15 | } |
| 16 | } |
Lee las dos guardas con atención, porque ahí está toda la diferencia.
La primera abandona en tres casos: no hay broker configurado, ya está conectado o el Wi-Fi está caído. Intentar MQTT sin red es un desperdicio garantizado; la llamada se bloqueará hasta agotar el tiempo de espera del socket sin motivo.
La segunda es el corazón del patrón. Si el último intento fue hace menos de cinco segundos, vuelve inmediatamente. Así se sustituye delay(5000) sin detener el resto del firmware: en lugar de dormir cinco segundos, la función retorna en microsegundos y loop() sigue haciendo todo lo demás. Cuando pasan los cinco segundos, la siguiente vuelta encuentra la guarda liberada y vuelve a intentar.
El efecto es el mismo, un intento cada cinco segundos, pero con coste cero.
Wi-Fi y MQTT tienen cadencias diferentes a propósito
Observa que la reconexión Wi-Fi usa la misma estructura con un intervalo mucho mayor:
| 1 | void ensureWiFi() { |
| 2 | if (config.networkMode == "ap") { |
| 3 | return; |
| 4 | } |
| 5 | if (WiFi.status() == WL_CONNECTED) { |
| 6 | return; |
| 7 | } |
| 8 | if (millis() - lastWifiAttemptMs < 30000) { |
| 9 | return; |
| 10 | } |
| 11 | lastWifiAttemptMs = millis(); |
| 12 | connectWiFiOnce(5000); |
| 13 | } |
Treinta segundos para Wi-Fi, cinco para MQTT. No es arbitrario.
Volver a asociarse al Wi-Fi enciende la radio, busca redes, negocia la autenticación y espera DHCP. Consume energía y tarda. Intentar cada cinco segundos cuando el router está apagado gasta batería y no acelera nada.
La conexión MQTT, en cambio, es un handshake TCP sobre una red que ya funciona. Es barata. Y cuando el broker vuelve quieres reconectar rápido, porque cada segundo desconectado es telemetría que no llega.
La regla general es: cuanto más costosa sea la operación, mayor debe ser el intervalo entre intentos.
El orden dentro de loop importa
| 1 | void loop() { |
| 2 | handleSerialCommands(); |
| 3 | webServer.handleClient(); |
| 4 | updateConfigAccessPoint(); |
| 5 | ensureWiFi(); |
| 6 | connectMqtt(); |
| 7 | if (mqtt.connected()) { |
| 8 | mqtt.loop(); |
| 9 | } |
| 10 | runProgramTick(); |
| 11 | unsigned long now = millis(); |
| 12 | if (now - lastTelemetryMs >= 10000) { |
| 13 | lastTelemetryMs = now; |
| 14 | sendTelemetry(); |
| 15 | } |
| 16 | } |
Wi-Fi va antes que MQTT porque el segundo depende del primero. Y mqtt.loop() solo se llama cuando hay conexión, protegido por el if.
Este mqtt.loop() es obligatorio y se olvida con frecuencia. Procesa los mensajes recibidos y, sobre todo, responde al keepalive del broker. Si no se llama con regularidad, el broker considera muerto al cliente y corta la conexión; se crea un ciclo de reconexión que parece un problema de red, pero es un problema de código.
Observa también que no existe ningún delay en todo el bucle. Toda la planificación se hace comparando millis().
Los códigos rc y qué significa cada uno
Cuando falla la conexión, mqtt.state() devuelve un número. Es la información de diagnóstico más útil y casi todos los tutoriales lo imprimen sin explicarlo:
| 1 | -4 tiempo agotado: broker inalcanzable, IP o puerto incorrectos |
| 2 | -3 conexión perdida: la red cayó en mitad del proceso |
| 3 | -2 fallo de conexión: DNS no resolvió o TCP fue rechazado |
| 4 | -1 desconectado por una llamada local |
| 5 | 0 conectado |
| 6 | 1 versión de protocolo rechazada por el broker |
| 7 | 2 client id rechazado |
| 8 | 3 broker no disponible |
| 9 | 4 usuario o contraseña incorrectos |
| 10 | 5 no autorizado |
La lectura práctica es: los valores negativos indican un problema de red o de dirección; los positivos indican rechazo del broker, que recibió la conexión y respondió que no.
El -2 suele ser un nombre de host incorrecto. 4 y 5 son credenciales y, en un broker autenticado con token, casi siempre un token caducado. El 2 aparece cuando el client id contiene un carácter que el broker no acepta o es demasiado largo.
Client id duplicado y el diagnóstico falso de red
Merece una sección propia porque produce un síntoma engañoso.
El protocolo MQTT exige que cada cliente tenga un identificador único. Si dos dispositivos se conectan con el mismo id, el broker desconecta al primero para aceptar al segundo. El primero vuelve a conectar y desconecta al segundo, y así indefinidamente.
El resultado es una pareja de dispositivos que se conecta y cae en ciclo, sin otro error en los registros que las desconexiones. Es fácil confundirlo con un Wi-Fi inestable.
El firmware anterior lo evita: el client id es deviceId, único para cada dispositivo. Si utilizas un literal como "esp32", el valor que aparece en casi todos los ejemplos, el problema aparece el día que entra en operación la segunda placa.
Si no existe un identificador natural, deriva uno de la dirección MAC, que es única de fábrica.
Last Will: avisar que murió
Hay un caso que la reconexión no resuelve: el dispositivo pierde la alimentación. No puede avisar a nadie y el sistema del otro lado sigue mostrando el último valor recibido como si fuera actual.
MQTT lo resuelve con el Last Will and Testament. En el momento de la conexión registras un mensaje que el broker publica por ti si la conexión cae sin despedida:
| 1 | mqtt.connect(clientId, usuario, senha, |
| 2 | topicoStatus, 1, true, "offline"); |
Los parámetros adicionales son el tópico del testamento, la calidad de servicio, el indicador de retención y el mensaje. En cuanto conecte, publica online en el mismo tópico, también retenido:
| 1 | mqtt.publish(topicoStatus, "online", true); |
El resultado es un tópico de presencia fiable. Mientras el dispositivo está vivo dice online; si desaparece, el propio broker publica offline cuando vence el keepalive. El sistema puede distinguir datos antiguos de un dispositivo caído, una diferencia que ahorra guardias de soporte.
El firmware de este tutorial todavía no usa Last Will; es una mejora que vale la pena aplicar y el kit incluye el ejemplo listo.
Tutorial: de cero a la reconexión automática
Paso 1: preparar el proyecto
Crea el proyecto en PlatformIO y declara las únicas dependencias externas:
| 1 | [env:esp32-mqtt] |
| 2 | platform = espressif32 |
| 3 | board = esp32dev |
| 4 | framework = arduino |
| 5 | monitor_speed = 115200 |
| 6 | lib_deps = |
| 7 | knolleary/PubSubClient@^2.8 |
| 8 | bblanchon/ArduinoJson@^7.2.1 |
Paso 2: ajustar los parámetros
En la parte superior del firmware del kit, informa de la red, el broker y el identificador del dispositivo. Usa un identificador distinto en cada placa; así evitas el ciclo de desconexiones descrito arriba.
Si tu broker no exige autenticación, deja vacíos el usuario y la contraseña.
Paso 3: grabar y observar el comportamiento normal
| 1 | pio run -t upload |
| 2 | pio device monitor -b 115200 |
Debes ver la conexión Wi-Fi, después la conexión MQTT y la telemetría llegando con un intervalo regular. Hasta aquí es el caso sencillo.
Paso 4: la prueba importante: detener el broker
Con todo funcionando, detén el broker. Si es un Mosquitto local, detén el servicio.
Observa el monitor serie. El comportamiento correcto es que el dispositivo detecte la caída, intente cada cinco segundos, imprima el rc de cada intento y continúe haciendo todo lo demás: la telemetría sigue generándose y el servidor web continúa respondiendo.
Si el firmware se bloquea aquí, el patrón de guardas no está aplicado.
Paso 5: volver a iniciar el broker
Reinicia el broker y observa. La reconexión debe producirse en un máximo de cinco segundos, sin intervención y sin reiniciar la placa.
Paso 6: detener el Wi-Fi
Ahora apaga el router o el punto de acceso. La guarda de WiFi.status() debe impedir los intentos MQTT mientras no haya red; no deberías ver una sucesión de rc negativos, sino silencio en MQTT mientras Wi-Fi intenta cada treinta segundos.
Vuelve a encender el router. La secuencia correcta es primero Wi-Fi y después MQTT.
Paso 7: comprobar el keepalive
Déjalo funcionando veinte minutos sin publicar nada. Si la conexión cae sola durante ese intervalo, mqtt.loop() no se está llamando con regularidad; busca algún delay largo en el bucle.
Errores comunes y cómo resolverlos
El dispositivo se bloquea cuando cae el broker. Todavía hay un bucle while o un delay en la rutina de conexión. Aplica el patrón de guardas.
Reconecta y vuelve a caer en ciclo, sin un error claro. Client id duplicado. Confirma que cada placa utiliza un identificador diferente.
Cae solo después de unos minutos inactivo. Falta llamar a mqtt.loop() con regularidad o hay un delay largo bloqueando el keepalive.
rc=-2 constante. El nombre del broker no se resuelve. Prueba con la IP directamente para separar un problema de DNS de un problema de dirección.
rc=4 o rc=5. Credenciales. En un broker con token, comprueba su validez antes de sospechar del código.
Publica pero no recibe nada. Faltó volver a suscribirse a los tópicos después de reconectar. La suscripción no sobrevive a la caída; por eso subscribe está dentro del bloque de éxito de connect.
Funciona con un dispositivo y falla con varios. Además del client id, revisa el límite de conexiones del broker y el tamaño del búfer de PubSubClient, que por defecto es pequeño y trunca los mensajes grandes.
