ESP32 e Arduino

Reconexão MQTT no ESP32: Lógica Não Bloqueante que Resiste a Quedas de Rede

Substitua o loop de reconexão bloqueante da maioria dos exemplos por uma máquina de estados não bloqueante para Wi-Fi e MQTT, com backoff exponencial, jitter, nova inscrição e buffer offline, e comprove tudo reiniciando o roteador.

Roteador e placa ESP32 recuperando a conexão em um teste de reconexão MQTT no ESP32
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

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:

  1. 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.
  2. 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 online e refaça as inscrições. Em caso de falha, dobre o atraso até um teto e adicione jitter aleatório.
  3. 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 / offline retido. O Last Will avisa a todos que o dispositivo caiu; o online retido 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) < 0 continua funcionando quando millis() 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:

  1. Grave o sketch e abra o monitor serial.
  2. 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/#"
  1. Confirme que você vê status online e uma mensagem de telemetria a cada dez segundos.
  2. Desligue e religue o roteador. Seu computador também perde a conexão, então acompanhe pelo monitor serial.
  3. 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.
  4. Observe o log serial: Wi-Fi lost, depois nada enquanto o roteador inicializa (nenhuma tentativa MQTT com falha, porque o Wi-Fi está fora), depois Wi-Fi up e então MQTT connected (#2).
  5. No assinante, verifique que status online volta, a telemetria em buffer chega em rajada e reconnects aumentou em um.
  6. Envie um comando para connectmq/demo/esp32-a1b2c3/cmd/test e 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.

Artigos relacionados

Exemplo de MQTT com ESP-IDF: Monte um Cliente esp-mqtt do Zero

Monte um projeto ESP-IDF a partir de uma pasta vazia com o componente esp-mqtt: configuração do Wi-Fi em modo station, o handler de eventos MQTT, publicação e inscrição, propriedades do MQTT 5 e TLS com o bundle de certificados.

8 min de leitura

Usando os Recursos do MQTT 5 do esp-mqtt no ESP32 com ESP-IDF

Um projeto ESP-IDF prático que ativa o MQTT 5.0 no esp-mqtt e usa user properties, expiração de mensagens, topic aliases, expiração de sessão e response topics com um broker MQTT 5 real.

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.