
MQTT com reconexão automática sem travar o loop
O laço while que todo tutorial ensina trava o firmware inteiro quando o broker cai. O padrão de guarda com millis(), cadências separadas e os 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
Baixe o material deste 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.
Quer receber novos manuais e esquemas industriais? Entre no nosso canal do Telegram / WhatsApp
O laço que todo tutorial ensina, e que você não deve usar
Procure por reconexão MQTT no ESP32 e você vai encontrar alguma variação disto:
| 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 na bancada, com o broker no mesmo computador. Em campo, é uma armadilha.
Enquanto esse laço roda, o firmware inteiro está parado. O servidor web não responde, o botão físico não é lido, o Modbus não é consultado, a máquina de estados do processo não avança. Se o broker estiver fora do ar por dez minutos, seu dispositivo fica dez minutos morto — não desconectado, morto, sem fazer nada além de tentar e dormir cinco segundos.
Pior: em muitos projetos isso alimenta o watchdog de forma irregular e o ESP32 reinicia sozinho. Aí o diagnóstico vira "o dispositivo está reiniciando", e ninguém liga o reset à queda do broker.
A inversão: guarda em vez de laço
O padrão correto não tem laço nenhum. Você chama a função de conexão a cada volta do loop(), e ela decide sozinha se deve fazer 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 | } |
Leia as duas guardas com atenção, porque é nelas que está toda a diferença.
A primeira desiste em três casos: sem broker configurado, já conectado, ou Wi-Fi caído. Tentar MQTT sem rede é desperdício garantido — a chamada vai bloquear até estourar o tempo limite do socket, por nada.
A segunda é o coração do padrão. Se a última tentativa foi há menos de cinco segundos, volta imediatamente. É isso que substitui o delay(5000) sem parar o resto do firmware: em vez de dormir cinco segundos, a função retorna em microssegundos e o loop() segue fazendo tudo o mais. Quando os cinco segundos tiverem passado, a próxima volta encontra a guarda liberada e tenta de novo.
O efeito é o mesmo — uma tentativa a cada cinco segundos — mas o custo é zero.
Wi-Fi e MQTT têm cadências diferentes, de propósito
Repare que a reconexão de Wi-Fi usa a mesma estrutura com um intervalo bem maior:
| 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 | } |
Trinta segundos para o Wi-Fi, cinco para o MQTT. Não é arbitrário.
Reassociar ao Wi-Fi liga o rádio, faz varredura, negocia autenticação e espera DHCP. É caro em energia e demora. Tentar de cinco em cinco segundos quando o roteador está desligado gasta bateria e não acelera nada.
Já a conexão MQTT é um handshake TCP sobre uma rede que já está de pé. É barata. E quando o broker volta, você quer reconectar rápido, porque cada segundo desconectado é telemetria que não sobe.
A regra geral: quanto mais cara a operação, maior o intervalo entre tentativas.
A ordem no 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 | } |
O Wi-Fi vem antes do MQTT porque o segundo depende do primeiro. E o mqtt.loop() só é chamado quando há conexão, protegido pelo if.
Esse mqtt.loop() é obrigatório e frequentemente esquecido. É ele que processa mensagens recebidas e, principalmente, responde ao keepalive do broker. Sem chamá-lo com regularidade, o broker considera o cliente morto e derruba a conexão — produzindo um ciclo de reconexão que parece problema de rede e é problema de código.
Repare também que não existe nenhum delay no laço inteiro. Todo o agendamento é feito comparando millis().
Os códigos rc, e o que cada um quer dizer
Quando a conexão falha, o mqtt.state() devolve um número. Ele é a informação mais útil do diagnóstico, e quase todo tutorial imprime sem explicar:
| 1 | -4 tempo limite estourado: broker inalcancavel, IP ou porta errados |
| 2 | -3 conexao perdida: rede caiu no meio |
| 3 | -2 falha na conexao: DNS nao resolveu ou recusa de TCP |
| 4 | -1 desconectado por chamada local |
| 5 | 0 conectado |
| 6 | 1 versao de protocolo recusada pelo broker |
| 7 | 2 client id rejeitado |
| 8 | 3 broker indisponivel |
| 9 | 4 usuario ou senha incorretos |
| 10 | 5 nao autorizado |
A leitura prática: valores negativos são problema de rede ou de endereço; valores positivos são recusa do broker, que recebeu sua conexão e disse não.
O -2 costuma ser nome de host errado. O 4 e o 5 são credencial, e no caso de broker com autenticação por token, quase sempre token expirado. O 2 aparece quando o client id tem caractere que o broker não aceita, ou é longo demais.
O client id duplicado, e o falso diagnóstico de rede
Este merece seção própria porque produz um sintoma enganoso.
O protocolo MQTT exige que cada cliente tenha identificador único. Se dois dispositivos conectam com o mesmo id, o broker derruba o primeiro para aceitar o segundo. O primeiro reconecta e derruba o segundo. E assim indefinidamente.
O resultado é um par de dispositivos que fica conectando e caindo em ciclo, sem erro nenhum nos logs além de desconexões. É facilmente confundido com Wi-Fi instável.
A prevenção está no firmware acima: o client id é o deviceId, que é único por dispositivo. Se você usa um literal como "esp32" — o valor que aparece em quase todo exemplo — o problema aparece no dia em que a segunda placa entrar em operação.
Se não houver identificador natural, derive do endereço MAC, que é único de fábrica.
Last Will: avisar que morreu
Há um caso que a reconexão não resolve: o dispositivo perde energia. Ele não tem como avisar ninguém, e o sistema do outro lado continua exibindo o último valor recebido como se fosse atual.
O MQTT resolve isso com o Last Will and Testament. Você registra, no momento da conexão, uma mensagem que o broker publica por você se a conexão cair sem despedida:
| 1 | mqtt.connect(clientId, usuario, senha, |
| 2 | topicoStatus, 1, true, "offline"); |
Os parâmetros extras são o tópico do testamento, a qualidade de serviço, o flag de retenção e a mensagem. Assim que conectar, publique online no mesmo tópico, também retido:
| 1 | mqtt.publish(topicoStatus, "online", true); |
O efeito é um tópico de presença confiável. Enquanto o dispositivo estiver vivo ele diz online; se sumir, o próprio broker publica offline depois do keepalive expirar. O painel do outro lado passa a distinguir dado velho de dispositivo caído, que é uma distinção que salva plantão.
O firmware deste tutorial ainda não usa Last Will — é uma melhoria que vale aplicar, e o kit traz o exemplo pronto.
Tutorial: do zero ao reconecta sozinho
Passo 1: preparar o projeto
Crie o projeto no PlatformIO e declare a única dependência externa:
| 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 |
Passo 2: ajustar os parâmetros
No topo do firmware do kit, informe rede, broker e o identificador do dispositivo. Use um identificador diferente em cada placa — é o que evita o ciclo de derrubada descrito acima.
Se o seu broker não exige autenticação, deixe usuário e senha vazios.
Passo 3: gravar e observar o comportamento normal
| 1 | pio run -t upload |
| 2 | pio device monitor -b 115200 |
Você deve ver a conexão do Wi-Fi, depois a do MQTT, e a telemetria subindo em intervalo regular. Até aqui é o caso fácil.
Passo 4: o teste que importa — derrubar o broker
Com tudo funcionando, pare o broker. Se for Mosquitto local, encerre o serviço.
Observe o monitor serial. O comportamento correto é: o dispositivo detecta a queda, passa a tentar de cinco em cinco segundos, imprime o rc de cada tentativa, e continua fazendo todo o resto — a telemetria segue sendo gerada, o servidor web continua respondendo.
Se o firmware travar aqui, o padrão de guarda não está aplicado.
Passo 5: subir o broker de volta
Reinicie o broker e observe. A reconexão deve acontecer em até cinco segundos, sem intervenção e sem reiniciar a placa.
Passo 6: derrubar o Wi-Fi
Agora desligue o roteador ou o ponto de acesso. A guarda de WiFi.status() deve impedir tentativas de MQTT enquanto não houver rede — você não deve ver rc negativos em sequência, e sim silêncio do lado MQTT enquanto o Wi-Fi tenta a cada trinta segundos.
Religue o roteador. A sequência correta é Wi-Fi primeiro, MQTT logo depois.
Passo 7: verificar o keepalive
Deixe rodando por vinte minutos sem publicar nada. Se a conexão cair sozinha nesse intervalo, o mqtt.loop() não está sendo chamado com regularidade — procure por algum delay longo no seu laço.
Erros comuns e como resolver
O dispositivo trava quando o broker cai. Ainda há um laço while ou um delay na rotina de conexão. Aplique o padrão de guarda.
Reconecta e cai em ciclo, sem erro claro. Client id duplicado. Confirme que cada placa usa um identificador diferente.
Cai sozinho depois de alguns minutos parado. Falta chamar mqtt.loop() com regularidade, ou existe um delay longo no laço bloqueando o keepalive.
rc=-2 constante. Nome do broker não resolve. Teste com o IP direto para separar problema de DNS de problema de endereço.
rc=4 ou rc=5. Credencial. Em broker com token, verifique validade antes de suspeitar do código.
Publica mas não recebe nada. Faltou reinscrever nos tópicos depois de reconectar. A inscrição não sobrevive à queda: por isso o subscribe está dentro do bloco de sucesso do connect.
Funciona com um dispositivo e falha com vários. Além do client id, confira o limite de conexões do broker e o tamanho do buffer do PubSubClient, que por padrão é pequeno e trunca mensagens maiores.
