Tutoriais

Tutorial de MQTT no ESP8266 com PubSubClient

Conecte um NodeMCU ou Wemos D1 mini a um broker MQTT com PubSubClient: sketch completo, reconexão sem bloqueio, Last Will e TLS com fingerprint do certificado.

Atualizado · 9 min de leitura

Tutorial de MQTT no ESP8266 com PubSubClient

O ESP8266 continua sendo uma das formas mais baratas de colocar um sensor no Wi-Fi, e o PubSubClient é a biblioteca MQTT que a maioria dos sketches para ESP8266 usa. Este tutorial entrega um sketch completo para NodeMCU ou Wemos D1 mini que conecta a um broker MQTT, publica telemetria, recebe comandos, reconecta sem bloquear e se anuncia com um Last Will; depois migra a conexão para TLS com o fingerprint do certificado e explica quando usar uma CA raiz no lugar.

Ambiente testado: Arduino IDE 2.x ou arduino-cli, core ESP8266 3.1.x, PubSubClient 2.8.

Instale o toolchain

  1. No Arduino IDE, abra Preferências e adicione esta URL do gerenciador de placas: https://arduino.esp8266.com/stable/package_esp8266com_index.json.
  2. Gerenciador de Placas → instale esp8266 by ESP8266 Community.
  3. Gerenciador de Bibliotecas → instale PubSubClient de Nick O'Leary.
  4. Selecione NodeMCU 1.0 (ESP-12E Module) ou LOLIN(WEMOS) D1 R2 & mini como placa.

Com o arduino-cli, os mesmos passos são:

Configuração com arduino-cli
arduino-cli config add board_manager.additional_urls \
  https://arduino.esp8266.com/stable/package_esp8266com_index.json
arduino-cli core update-index
arduino-cli core install esp8266:esp8266
arduino-cli lib install PubSubClient

O ESP8266 só suporta Wi-Fi de 2,4 GHz. Se o seu roteador transmite um único SSID combinado para as duas bandas, normalmente funciona; uma rede apenas 5 GHz nunca vai funcionar.

O sketch completo

esp8266_mqtt.ino
#include <ESP8266WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID = "your-ssid";
const char* WIFI_PASS = "your-wifi-password";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = "username";
const char* MQTT_PASS = "password";

WiFiClient net;
PubSubClient mqtt(net);

String clientId, baseTopic;
unsigned long lastAttempt = 0, retryDelay = 1000, lastPublish = 0;

void onMessage(char* topic, byte* payload, unsigned int length) {
  String msg;
  for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
  Serial.printf("[%s] %s\n", topic, msg.c_str());
  if (msg == "on")  digitalWrite(LED_BUILTIN, LOW);    // o LED embutido é ativo em nível baixo
  if (msg == "off") digitalWrite(LED_BUILTIN, HIGH);
}

bool connectMqtt() {
  String status = baseTopic + "/status";
  // connect(id, user, pass, willTopic, willQos, willRetain, willMessage)
  if (!mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS,
                    status.c_str(), 1, true, "offline")) {
    Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
    return false;
  }
  mqtt.publish(status.c_str(), "online", true);              // retida
  mqtt.subscribe((baseTopic + "/led").c_str(), 1);
  Serial.println("MQTT connected as " + clientId);
  return true;
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);

  WiFi.persistent(false);              // não regravar a flash a cada boot
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  clientId  = "esp8266-" + String(ESP.getChipId(), HEX);
  baseTopic = "testmqtt/esp8266/" + clientId;

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);             // o padrão é 256 bytes, incluindo topic e cabeçalho
  mqtt.setKeepAlive(30);
  mqtt.setSocketTimeout(5);            // um connect que falha bloqueia no máximo ~5 s
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    delay(10);                         // o core reconecta o Wi-Fi sozinho
    return;
  }

  if (!mqtt.connected()) {
    unsigned long now = millis();
    if (now - lastAttempt >= retryDelay) {
      lastAttempt = now;
      retryDelay = connectMqtt() ? 1000 : min(retryDelay * 2, 60000UL);
    }
    return;
  }

  mqtt.loop();                         // precisa rodar com frequência: recebe mensagens, envia pings

  if (millis() - lastPublish >= 10000) {
    lastPublish = millis();
    char payload[96];
    snprintf(payload, sizeof(payload),
             "{\"rssi\":%d,\"heap\":%u,\"uptime\":%lu}",
             WiFi.RSSI(), ESP.getFreeHeap(), millis() / 1000);
    mqtt.publish((baseTopic + "/telemetry").c_str(), payload);
  }
}

Por que ele é escrito assim

  • Reconexão sem bloqueio. O exemplo clássico usa while (!client.connected()) { ...; delay(5000); }, que congela todo o resto (botões, sensores, até a pilha Wi-Fi) enquanto o broker está inacessível. Aqui, loop() retorna na hora e tenta de novo com backoff exponencial (1 s, 2 s, 4 s… até 60 s), para que uma frota não martele um broker que está reiniciando.
  • Client ID estável a partir de ESP.getChipId(). Duas placas com o mesmo ID derrubam uma à outra num loop infinito; veja as boas práticas de client ID.
  • Last Will + status retido. Se a placa perde energia, o broker publica offline em .../status quando o keep-alive expira (1,5 × 30 s aqui). Quem assinar depois vê o estado atual na hora.
  • Assinar depois de cada conexão. O PubSubClient usa clean session, então as assinaturas não sobrevivem a uma reconexão. Endpoints de teste compartilhados como broker.connectmq.com também não guardam sessões, por isso o sketch sempre assina de novo em connectMqtt().
  • setBufferSize(512): o buffer padrão do PubSubClient é de 256 bytes para o pacote inteiro, topic incluído. Um topic longo mais um payload JSON passam disso rápido, e aí publish() retorna false sem nenhuma outra pista. A calculadora de tamanho de pacote mostra o tamanho real do seu pacote.
  • setSocketTimeout(5): o próprio connect() é bloqueante. O timeout padrão é de 15 segundos, o bastante para disparar resets do watchdog ou travar a interface.

Teste o sketch

Grave a placa e abra o Monitor Serial a 115200 baud. Você deve ver MQTT connected as esp8266-xxxxxx. Se aparecer state=-2 ou outro código, vá para a tabela de erros mais abaixo.

Depois abra o cliente MQTT online, conecte em broker.connectmq.com e assine testmqtt/esp8266/#. Primeiro aparece o status online retido, depois um JSON de telemetria a cada 10 segundos. Publique on em testmqtt/esp8266/<client-id>/led e o LED acende.

Veja o seu ESP8266 no navegador

Para testar o Last Will, desligue a placa e espere cerca de 45 segundos: o topic de status muda para offline. Ligue de novo e ele volta para online: é o seu caminho de reconexão funcionando.

Adicionando TLS com fingerprint

A porta 1883 envia a senha MQTT em texto puro. O core do ESP8266 inclui o BearSSL, então TLS na porta 8883 só exige outra classe de cliente. A forma mais leve de verificar o servidor é o pinning do certificado pelo fingerprint SHA-1.

Obtenha o fingerprint do certificado atual do broker pelo seu computador:

Ler o fingerprint SHA-1
openssl s_client -connect broker.connectmq.com:8883 \
  -servername broker.connectmq.com </dev/null 2>/dev/null \
  | openssl x509 -noout -fingerprint -sha1 \
  | sed 's/.*=//; s/:/ /g'

Depois mude três coisas no sketch:

Mudanças para TLS
#include <WiFiClientSecure.h>

const uint16_t MQTT_PORT = 8883;
const char* FINGERPRINT = "AA BB CC DD EE FF 00 11 22 33 44 55 66 77 88 99 AA BB CC DD";

BearSSL::WiFiClientSecure net;         // substitui o WiFiClient
PubSubClient mqtt(net);

// no setup(), antes de mqtt.setServer():
net.setFingerprint(FINGERPRINT);
mqtt.setSocketTimeout(10);             // o handshake TLS é lento num chip de 80 MHz

O handshake demora bem mais que um connect TCP comum e consome uma boa parte da pouca RAM do ESP8266. Acompanhe ESP.getFreeHeap() na telemetria; se ficar apertado, o BearSSL pode usar buffers menores com net.setBufferSizes(1024, 1024), mas só quando o broker suporta a extensão TLS max fragment length; confira antes com BearSSL::WiFiClientSecure::probeMaxFragmentLength(MQTT_HOST, 8883, 1024).

Fingerprint x CA raiz

MétodoComoCompromisso
setFingerprint()Fixa o SHA-1 do certificado finalMenor e mais rápido, mas quebra a cada renovação do certificado
setTrustAnchors()Fixa a CA raiz (BearSSL::X509List)Sobrevive às renovações; precisa da hora correta via configTime()
setInsecure()Sem verificaçãoCriptografado, mas aceita qualquer servidor: só para depuração

Certificados de CAs públicas são renovados regularmente, muitas vezes a cada poucos meses. Uma frota com fingerprint fixado para de conectar no dia da renovação com state=-2, e você precisa regravar todos os dispositivos. Para qualquer coisa de longa duração, fixe a CA raiz:

CA raiz em vez de fingerprint
#include <time.h>
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...certificado raiz da CA que assina o certificado do seu broker...
-----END CERTIFICATE-----
)EOF";
BearSSL::X509List trustAnchor(ROOT_CA);

// no setup(), depois que o Wi-Fi conectar:
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
while (time(nullptr) < 1700000000) delay(200);   // esperar o NTP
net.setTrustAnchors(&trustAnchor);

Sem um relógio válido, o BearSSL considera todo certificado expirado ou ainda não válido. Mais detalhes sobre TLS, incluindo certificados de cliente, estão no guia de TLS para ESP32: a API do BearSSL é diferente, mas os conceitos são os mesmos.

Códigos de erro

mqtt.state() retorna estes valores:

stateConstanteCausa típica
-4MQTT_CONNECTION_TIMEOUTSem resposta ao ping dentro do keep-alive; Wi-Fi fraco
-3MQTT_CONNECTION_LOSTA rede caiu ou o broker fechou o socket
-2MQTT_CONNECT_FAILEDFalha de TCP/TLS: host ou porta errados, DNS, firewall, fingerprint desatualizado
-1MQTT_DISCONNECTEDDesconexão limpa, ou nunca conectou
1MQTT_CONNECT_BAD_PROTOCOLO broker recusa MQTT 3.1.1
2MQTT_CONNECT_BAD_CLIENT_IDClient ID rejeitado
3MQTT_CONNECT_UNAVAILABLEBroker indisponível
4MQTT_CONNECT_BAD_CREDENTIALSUsuário ou senha errados
5MQTT_CONNECT_UNAUTHORIZEDAutenticado, mas sem permissão para conectar

Valores positivos são códigos de retorno CONNACK do broker; consulte-os na ferramenta de reason codes. Valores negativos vêm da própria biblioteca e quase sempre indicam um problema de rede. O método passo a passo de testar a conexão de um ESP32 com o broker funciona igual num ESP8266.

Próximos passos

  • Vai migrar para um chip mais novo? O tutorial de ESP32 com Arduino usa a mesma API do PubSubClient.
  • Precisa publicar com QoS 1 ou de outro estilo de API? Compare com a biblioteca arduino-mqtt.
  • Payloads maiores? ArduinoJson com MQTT mostra como dimensionar os buffers corretamente.

Um nó MQTT confiável com ESP8266 se resume a alguns hábitos: nunca bloquear o loop(), reconectar com backoff, assinar de novo a cada conexão, dimensionar o buffer para os seus pacotes reais e fixar uma CA raiz em vez de um fingerprint nos dispositivos que precisam continuar funcionando depois da próxima renovação de certificado.

Perguntas frequentes

Por que a conexão TLS do meu ESP8266 para de funcionar depois de algumas semanas?

Se você fixou o certificado com setFingerprint(), o fingerprint muda toda vez que o broker renova o certificado, e o handshake passa a falhar com state -2. Fixe a CA raiz com setTrustAnchors(): ela sobrevive às renovações enquanto a mesma CA assinar o novo certificado.

O PubSubClient consegue publicar com QoS 1 no ESP8266?

Não. O PubSubClient publica apenas com QoS 0. Ele pode assinar com QoS 0 ou 1, então as mensagens recebidas podem ser confirmadas, mas as enviadas são fire-and-forget. Se você precisa publicar com QoS 1, use uma biblioteca como a arduino-mqtt ou adicione uma confirmação na camada de aplicação.

O que significa o state -2 do PubSubClient?

MQTT_CONNECT_FAILED: não foi possível abrir a conexão TCP ou TLS com o broker. O problema está abaixo do MQTT: Wi-Fi, DNS, porta errada, um firewall ou uma falha no handshake TLS, como um fingerprint desatualizado ou um WiFiClient comum na porta 8883.