A lógica de reconexão MQTT do ESP32 é o que separa uma demo que funciona na sua bancada de um dispositivo que roda por meses. Neste guia você vai substituir o loop bloqueante reconnect() encontrado na maioria dos exemplos por uma máquina de estados não bloqueante que trata Wi-Fi e MQTT separadamente, aplica backoff exponencial com jitter, refaz as inscrições automaticamente e guarda leituras em buffer enquanto está offline. No final você vai verificar tudo do jeito honesto: reiniciando o roteador enquanto o ESP32 está conectado ao broker.connectmq.com.
Por que o loop de reconexão clássico falha
Quase todo exemplo com PubSubClient contém alguma versão disto:
void reconnect() {
while (!client.connected()) {
if (client.connect("esp32")) {
client.subscribe("cmd");
} else {
delay(5000);
}
}
}
Funciona até a rede cair. Aí vários problemas aparecem ao mesmo tempo:
| Problema | O que acontece na prática |
|---|---|
Loop while infinito |
Nada mais roda: botões, sensores, displays e controle de motores congelam até o broker voltar |
delay(5000) |
O mesmo de cima, e em algumas configurações o task watchdog pode disparar |
| Nenhuma verificação de Wi-Fi | O sketch martela conexões MQTT enquanto o próprio Wi-Fi está fora, cada uma esperando timeouts de DNS ou TCP |
| Intervalo de nova tentativa fixo | Centenas de dispositivos se recuperando da mesma queda reconectam em sincronia e atingem o broker no mesmo instante |
| Client ID fixo no código | Duas placas com o mesmo firmware derrubam uma à outra para sempre |
A solução é tratar a conectividade como uma preocupação de segundo plano. O loop() deve rodar a cada poucos milissegundos aconteça o que acontecer, e as tentativas de conexão só devem ocorrer quando chegar a hora.
Entenda os códigos de estado do PubSubClient
Antes de melhorar qualquer coisa, registre client.state() em cada falha. Os códigos dizem se o problema é a rede, o broker ou suas credenciais.
| Código | Constante | Significado | Causa típica |
|---|---|---|---|
| -4 | MQTT_CONNECTION_TIMEOUT |
Sem resposta dentro do keep-alive | Rede travada, Wi-Fi fraco |
| -3 | MQTT_CONNECTION_LOST |
Conexão caiu | Reinício do roteador, reinício do broker, client ID duplicado |
| -2 | MQTT_CONNECT_FAILED |
Falha na conexão TCP | Host ou porta errados, falha de DNS, sem internet |
| -1 | MQTT_DISCONNECTED |
Desconectado de forma limpa | Você chamou disconnect() |
| 1 | MQTT_CONNECT_BAD_PROTOCOL |
Protocolo não suportado | Raro com brokers 3.1.1 |
| 2 | MQTT_CONNECT_BAD_CLIENT_ID |
Client ID rejeitado | ID vazio ou inválido |
| 3 | MQTT_CONNECT_UNAVAILABLE |
Servidor indisponível | Broker sobrecarregado ou reiniciando |
| 4 | MQTT_CONNECT_BAD_CREDENTIALS |
Usuário/senha incorretos | Erro de digitação, senha trocada |
| 5 | MQTT_CONNECT_UNAUTHORIZED |
Não autorizado | Credenciais não permitidas para este client ID |
Os códigos 4 e 5 não são transitórios. Tentar de novo a cada poucos segundos não vai resolvê-los e só acumula logins com falha. Muitos brokers, incluindo o ConnectMQ, bloqueiam endereços IP após repetidas falhas de login, então um dispositivo que fica em loop com credenciais erradas pode se trancar do lado de fora. O firmware abaixo aplica um backoff bem mais agressivo nesses códigos. Se você recebe -2 já na primeira tentativa, o problema não é de reconexão; resolva primeiro por que o ESP32 não conecta ao broker MQTT.
Arquitetura de uma reconexão MQTT não bloqueante no ESP32
O firmware separa três camadas:
- Wi-Fi. Deixe a reconexão automática do próprio ESP32 cuidar do rádio e escute os eventos de Wi-Fi para saber quando o link está ativo. Não chame
WiFi.begin()em um loop apertado. - MQTT. Só tente conectar quando o Wi-Fi tiver um endereço IP e o temporizador de backoff tiver expirado. Em caso de sucesso, zere o backoff, publique um status
onlinee refaça as inscrições. Em caso de falha, dobre o atraso até um teto e adicione jitter aleatório. - Aplicação. Leia sensores e controle atuadores em todo loop, independentemente da conectividade. Publique quando estiver conectado; caso contrário, coloque as leituras em um pequeno buffer circular e envie-as depois de reconectar.
Backoff exponencial com jitter é o padrão consagrado para novas tentativas contra um serviço compartilhado. O jitter importa mais do que se imagina: depois de uma queda regional, ele espalha milhares de reconexões ao longo de uma janela em vez de um único pico.
O sketch completo
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
String clientId; // gerado a partir do endereço MAC
String base; // connectmq/demo/<clientId>
WiFiClient net;
PubSubClient mqtt(net);
// ---- Estado do backoff ----
const uint32_t BACKOFF_MIN = 1000; // 1 s
const uint32_t BACKOFF_MAX = 60000; // 60 s
const uint32_t BACKOFF_AUTH = 300000; // 5 min para erros de credencial
uint32_t backoff = BACKOFF_MIN;
unsigned long nextAttempt = 0;
volatile bool wifiUp = false;
uint32_t reconnects = 0;
// ---- Buffer circular offline ----
const int BUF_SIZE = 20;
String pending[BUF_SIZE];
int head = 0, count = 0;
void onWiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
wifiUp = true;
Serial.printf("Wi-Fi up, IP %s\n", WiFi.localIP().toString().c_str());
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
if (wifiUp) Serial.println("Wi-Fi lost");
wifiUp = false;
break;
default:
break;
}
}
void onMessage(char* topic, byte* payload, unsigned int length) {
Serial.printf("cmd on %s: %.*s\n", topic, (int)length, (const char*)payload);
}
void subscribeAll() {
// Chamada após CADA conexão bem-sucedida: as inscrições não sobrevivem
// a uma reconexão com clean session.
mqtt.subscribe((base + "/cmd/#").c_str(), 1);
}
void flushPending() {
while (count > 0 && mqtt.connected()) {
int tail = (head - count + BUF_SIZE) % BUF_SIZE;
if (!mqtt.publish((base + "/telemetry").c_str(), pending[tail].c_str())) break;
count--;
mqtt.loop();
}
}
void scheduleRetry(int state) {
bool authError = (state == MQTT_CONNECT_BAD_CREDENTIALS || state == MQTT_CONNECT_UNAUTHORIZED);
uint32_t wait = authError ? BACKOFF_AUTH : backoff;
uint32_t jitter = random(0, wait / 4 + 1);
nextAttempt = millis() + wait + jitter;
if (!authError) backoff = min(backoff * 2, BACKOFF_MAX);
Serial.printf("MQTT connect failed (state %d), retry in %lu ms\n", state, (unsigned long)(wait + jitter));
}
void maintainMqtt() {
if (mqtt.connected()) {
mqtt.loop();
return;
}
if (!wifiUp || (long)(millis() - nextAttempt) < 0) return;
String status = base + "/status";
bool ok = mqtt.connect(clientId.c_str(), nullptr, nullptr,
status.c_str(), 1, true, "offline");
if (!ok) {
scheduleRetry(mqtt.state());
return;
}
reconnects++;
backoff = BACKOFF_MIN;
Serial.printf("MQTT connected (#%lu)\n", (unsigned long)reconnects);
mqtt.publish(status.c_str(), "online", true);
subscribeAll();
flushPending();
}
void publishReading(const String& json) {
if (mqtt.connected() && mqtt.publish((base + "/telemetry").c_str(), json.c_str())) return;
pending[head] = json; // guarda em buffer enquanto offline
head = (head + 1) % BUF_SIZE;
if (count < BUF_SIZE) count++; // a entrada mais antiga é sobrescrita quando cheio
}
void setup() {
Serial.begin(115200);
uint8_t mac[6];
WiFi.mode(WIFI_STA);
WiFi.macAddress(mac);
char id[24];
snprintf(id, sizeof(id), "esp32-%02x%02x%02x", mac[3], mac[4], mac[5]);
clientId = id;
base = "connectmq/demo/" + clientId;
randomSeed(esp_random());
WiFi.onEvent(onWiFiEvent);
WiFi.setAutoReconnect(true);
WiFi.persistent(false);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
mqtt.setSocketTimeout(5); // limita quanto tempo uma conexão travada pode bloquear
}
unsigned long lastSample = 0;
void loop() {
maintainMqtt();
// O trabalho da aplicação sempre roda, conectado ou não
if (millis() - lastSample >= 10000) {
lastSample = millis();
String json = "{\"uptime\":" + String(millis() / 1000) +
",\"rssi\":" + String(WiFi.RSSI()) +
",\"reconnects\":" + String(reconnects) + "}";
publishReading(json);
}
}
Observações sobre as linhas importantes
- Client ID a partir do endereço MAC. Cada placa recebe um ID estável e único sem editar o sketch. IDs duplicados são a causa número um de loops misteriosos com
-3, porque o broker desconecta a sessão mais antiga sempre que a mais nova se conecta. Com credenciais do portal, use o client ID emitido pelo portal, já que o ConnectMQ vincula client IDs às credenciais. subscribeAll()após cada conexão. O PubSubClient conecta com clean session, então o broker esquece as inscrições quando a conexão cai. Esquecer de se inscrever de novo é o bug clássico do "reconecta, mas para de receber comandos".- Status
online/offlineretido. O Last Will avisa a todos que o dispositivo caiu; oonlineretido corrige isso após a recuperação. O guia de Last Will MQTT no ESP32 aborda esse padrão em profundidade. setSocketTimeout(5). Mesmo um design não bloqueante bloqueia durante a chamada de conexão em si, porque a consulta DNS, o handshake TCP e a espera pelo CONNACK são síncronos no PubSubClient. Manter o timeout curto limita essa pausa.- Temporizador seguro contra overflow.
(long)(millis() - nextAttempt) < 0continua funcionando quandomillis()dá a volta, após cerca de 49 dias.
Teste: reinicie o roteador
Uma estratégia de reconexão que você não testou é uma esperança, não uma estratégia. Aqui está um plano de teste que leva dez minutos:
- Grave o sketch e abra o monitor serial.
- No seu computador, inscreva-se nos tópicos do dispositivo (substitua o ID pelo que aparece no monitor serial):
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-test01 -v -t "connectmq/demo/esp32-a1b2c3/#"
- Confirme que você vê
status onlinee uma mensagem de telemetria a cada dez segundos. - Desligue e religue o roteador. Seu computador também perde a conexão, então acompanhe pelo monitor serial.
- Em cerca de 1,5 vez o keep-alive (45 segundos aqui), o broker publica o Last Will. Você vai vê-lo quando o notebook reconectar.
- Observe o log serial:
Wi-Fi lost, depois nada enquanto o roteador inicializa (nenhuma tentativa MQTT com falha, porque o Wi-Fi está fora), depoisWi-Fi upe entãoMQTT connected (#2). - No assinante, verifique que
status onlinevolta, a telemetria em buffer chega em rajada ereconnectsaumentou em um. - Envie um comando para
connectmq/demo/esp32-a1b2c3/cmd/teste confirme que o dispositivo ainda o recebe. Isso prova que a nova inscrição funciona.
Repita o teste com o broker inacessível, mas o Wi-Fi ativo, por exemplo trocando temporariamente MQTT_HOST por um nome não utilizado. Você deve ver os intervalos entre tentativas crescerem de cerca de um segundo até o teto de 60 segundos, com valores um pouco diferentes a cada vez por causa do jitter.
Solução de problemas de reconexão
- Reconecta a cada poucos segundos com estado -3. Outro dispositivo ou um cliente de teste esquecido está usando o mesmo client ID.
- Conectado, mas os comandos param de chegar após uma reconexão. Falta a nova inscrição.
- Estado -4 com sinal fraco. Aumente o keep-alive moderadamente, melhore o posicionamento da antena e registre o RSSI como mostrado.
- O dispositivo nunca se recupera de uma queda longa sem reiniciar. Algo está bloqueando para sempre, muitas vezes uma chamada HTTP ou uma biblioteca de sensor. Adicione o task watchdog, como descrito em task watchdog do ESP32 com MQTT.
- Uma rajada de mensagens antigas surpreende o dashboard. Inclua um timestamp nas leituras em buffer para que os consumidores distingam dados atrasados de dados ao vivo.
Quando o lado do broker importa
Um bom firmware assume que o broker pode ficar inacessível, mas a frequência com que isso acontece depende de onde o broker roda. Uma instância Mosquitto em um servidor doméstico cai junto com a energia, a atualização do sistema operacional e o certificado expirado. Um broker MQTT gerenciado transfere essas preocupações para uma equipe que opera o serviço por você. O ConnectMQ também expõe $SYS/broker/connection/{clientId}/state, para que um backend acompanhe o estado de conexão de um dispositivo, e o explorador de tópicos ao vivo do portal permite ver os tópicos de status retidos alternando entre online e offline enquanto você testa. Para entender keep-alive, sessões e o Last Will, a visão geral sobre o que um broker MQTT faz é uma ótima companhia. Testar com o servidor MQTT público é suficiente para este exercício; o mesmo firmware migra para credenciais privadas e TLS mudando três constantes e a classe do cliente, como mostrado em ESP32 MQTT com SSL/TLS. Para a jornada completa com ESP32, veja o tutorial de IoT com ESP32.
Comece com o ConnectMQ
Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito. Você recebe credenciais dedicadas, TLS na porta 8883 e um portal que mostra quando cada dispositivo conecta e desconecta, exatamente o que você precisa ao ajustar o comportamento de reconexão.
Perguntas frequentes
Com que frequência um ESP32 deve tentar reconectar ao MQTT?
Comece com cerca de um segundo e dobre a espera após cada falha até um teto de aproximadamente um minuto, adicionando jitter aleatório. Para erros de credencial, espere muito mais ou pare e reporte o problema, porque novas tentativas não vão dar certo.
Preciso me inscrever de novo após uma reconexão MQTT no ESP32?
Com o PubSubClient, sim. Ele usa clean session, então o broker descarta as inscrições quando a conexão termina. Chame sua função de inscrição após cada conexão bem-sucedida.
Devo chamar WiFi.begin() de novo quando o Wi-Fi cai?
Normalmente não. Com WiFi.setAutoReconnect(true) o ESP32 reconecta sozinho. Chamar WiFi.begin() repetidamente pode interromper uma associação que já está em andamento.
Por que meu ESP32 fica desconectando com estado -3?
A causa mais comum são dois clientes compartilhando o mesmo client ID; o broker desconecta o mais antigo toda vez que o outro se conecta. Wi-Fi fraco e um keep-alive maior que o timeout de inatividade de um NAT ou firewall são os próximos suspeitos.



