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
- No Arduino IDE, abra Preferências e adicione esta URL do gerenciador de placas:
https://arduino.esp8266.com/stable/package_esp8266com_index.json. - Gerenciador de Placas → instale esp8266 by ESP8266 Community.
- Gerenciador de Bibliotecas → instale PubSubClient de Nick O'Leary.
- Selecione NodeMCU 1.0 (ESP-12E Module) ou LOLIN(WEMOS) D1 R2 & mini como placa.
Com o arduino-cli, os mesmos passos são:
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 PubSubClientO 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
#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
offlineem.../statusquando 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.comtambém não guardam sessões, por isso o sketch sempre assina de novo emconnectMqtt(). 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()retornafalsesem nenhuma outra pista. A calculadora de tamanho de pacote mostra o tamanho real do seu pacote.setSocketTimeout(5): o próprioconnect()é 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:
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:
#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 MHzO 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étodo | Como | Compromisso |
|---|---|---|
setFingerprint() | Fixa o SHA-1 do certificado final | Menor 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ção | Criptografado, 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:
#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:
| state | Constante | Causa típica |
|---|---|---|
| -4 | MQTT_CONNECTION_TIMEOUT | Sem resposta ao ping dentro do keep-alive; Wi-Fi fraco |
| -3 | MQTT_CONNECTION_LOST | A rede caiu ou o broker fechou o socket |
| -2 | MQTT_CONNECT_FAILED | Falha de TCP/TLS: host ou porta errados, DNS, firewall, fingerprint desatualizado |
| -1 | MQTT_DISCONNECTED | Desconexão limpa, ou nunca conectou |
| 1 | MQTT_CONNECT_BAD_PROTOCOL | O broker recusa MQTT 3.1.1 |
| 2 | MQTT_CONNECT_BAD_CLIENT_ID | Client ID rejeitado |
| 3 | MQTT_CONNECT_UNAVAILABLE | Broker indisponível |
| 4 | MQTT_CONNECT_BAD_CREDENTIALS | Usuário ou senha errados |
| 5 | MQTT_CONNECT_UNAUTHORIZED | Autenticado, 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.


