O ESP32 é o microcontrolador preferido para projetos MQTT: WiFi integrado, RAM de sobra para TLS e suporte de primeira linha ao Arduino. Este tutorial monta um sketch completo com a biblioteca PubSubClient que publica leituras de sensores em JSON, escuta um tópico de comandos, reconecta sozinho e — no último passo — passa a usar TLS.
O que você precisa
- Uma placa de desenvolvimento ESP32 e um cabo USB.
- Arduino IDE 2.x (ou PlatformIO) com o pacote de placas esp32 by Espressif Systems instalado pelo Boards Manager.
- A biblioteca PubSubClient, de Nick O'Leary, instalada pelo Library Manager.
- Um broker. Usamos o broker público da HiveMQ na porta
1883para testes — serve para uma demo, mas qualquer pessoa pode ler os tópicos.
Estrutura de tópicos
Uma estrutura de tópicos organizada facilita o gerenciamento dos dispositivos. Cada placa ganha seu próprio prefixo com base em um ID de dispositivo único:
| Tópico | Direção | Finalidade |
|---|---|---|
testmqtt/esp32/<id>/telemetry | ESP32 para o broker | JSON do sensor a cada 10 segundos |
testmqtt/esp32/<id>/status | ESP32 para o broker | online / offline (retida, Last Will) |
testmqtt/esp32/<id>/cmd | Broker para o ESP32 | Comandos como led:on |
Veja tópicos e curingas MQTT para convenções de nomes.
O sketch completo
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "your-ssid";
const char* WIFI_PASSWORD = "your-wifi-password";
const char* MQTT_HOST = "broker.hivemq.com";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = nullptr; // set for brokers that require auth
const char* MQTT_PASS = nullptr;
const int LED_PIN = 2; // on-board LED on many dev boards
WiFiClient net;
PubSubClient mqtt(net);
char deviceId[24];
char topicTelemetry[64];
char topicStatus[64];
char topicCmd[64];
unsigned long lastReconnectAttempt = 0;
unsigned long 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("Message on %s: %s\n", topic, msg.c_str());
if (msg == "led:on") digitalWrite(LED_PIN, HIGH);
if (msg == "led:off") digitalWrite(LED_PIN, LOW);
}
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nWiFi connected, IP %s\n", WiFi.localIP().toString().c_str());
}
bool connectMqtt() {
// Last Will: broker publishes "offline" (retained) if we drop unexpectedly
bool ok = mqtt.connect(deviceId, MQTT_USER, MQTT_PASS,
topicStatus, 1, true, "offline");
if (!ok) {
Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
return false;
}
Serial.println("MQTT connected");
mqtt.publish(topicStatus, "online", true);
mqtt.subscribe(topicCmd, 1);
return true;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
// Unique client ID from the chip's MAC address
uint64_t mac = ESP.getEfuseMac();
snprintf(deviceId, sizeof(deviceId), "esp32-%04X%08X",
(uint16_t)(mac >> 32), (uint32_t)mac);
snprintf(topicTelemetry, sizeof(topicTelemetry), "testmqtt/esp32/%s/telemetry", deviceId);
snprintf(topicStatus, sizeof(topicStatus), "testmqtt/esp32/%s/status", deviceId);
snprintf(topicCmd, sizeof(topicCmd), "testmqtt/esp32/%s/cmd", deviceId);
Serial.printf("Device ID: %s\n", deviceId);
connectWiFi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setBufferSize(512); // default is 256 bytes for the whole packet
mqtt.setKeepAlive(30);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}
if (!mqtt.connected()) {
// Non-blocking reconnect: try every 5 seconds without stalling loop()
unsigned long now = millis();
if (now - lastReconnectAttempt > 5000) {
lastReconnectAttempt = now;
if (connectMqtt()) lastReconnectAttempt = 0;
}
return;
}
mqtt.loop(); // must run often: handles keep-alive and incoming messages
unsigned long now = millis();
if (now - lastPublish > 10000) {
lastPublish = now;
float temperature = temperatureRead(); // ESP32 internal sensor (rough)
char payload[128];
snprintf(payload, sizeof(payload),
"{\"device\":\"%s\",\"temp\":%.1f,\"rssi\":%d,\"uptime\":%lu}",
deviceId, temperature, WiFi.RSSI(), now / 1000);
bool sent = mqtt.publish(topicTelemetry, payload);
Serial.printf("Publish %s: %s\n", sent ? "ok" : "FAILED", payload);
}
}Abra o Serial Monitor em 115200 baud para ver o ID do dispositivo e acompanhe a telemetria chegando no navegador assinando testmqtt/esp32/#. Publique led:on no tópico cmd e o LED acende.
Veja a telemetria do seu ESP32 ao vivo
Como o sketch funciona
Client IDs únicos
Brokers MQTT permitem apenas uma conexão por client ID. Se duas placas forem gravadas com o mesmo ID fixo, como ESP32Client, elas se derrubam mutuamente em um loop infinito. Derivar o ID do endereço MAC do eFuse o mantém único e estável entre reinicializações. O gerador de client ID explica as regras se você quiser outro esquema.
Um loop de reconexão não bloqueante
Muitos exemplos usam um loop while (!client.connected()) com delay(5000). Isso bloqueia todo o resto da placa. O padrão com millis() acima tenta de novo a cada cinco segundos enquanto o restante do loop() continua rodando. mqtt.state() diz por que uma conexão falhou: -2 é falha de rede, 4 credenciais inválidas e 5 não autorizado. Compare com a lista completa no nosso guia de erros de conexão MQTT.
Tamanho do buffer e MQTT_MAX_PACKET_SIZE
O buffer do PubSubClient precisa comportar o pacote inteiro: cabeçalho fixo, tópico e payload. O padrão é 256 bytes, e um payload JSON com um tópico longo ultrapassa isso rapidamente — publish() então retorna false sem alarde. Tutoriais antigos mandam editar MQTT_MAX_PACKET_SIZE no header da biblioteca; desde a versão 2.8, basta chamar setBufferSize() antes de conectar. A calculadora de tamanho de pacote MQTT mostra o tamanho real dos seus pacotes.
Limitações do PubSubClient
- Apenas MQTT 3.1.1 — sem propriedades do MQTT 5.
- Publica apenas com QoS 0; as assinaturas podem usar QoS 0 ou 1.
- Publicações retidas e Last Will são suportados, como mostrado acima.
Status online com Last Will e mensagens retidas
A chamada connect() registra um Last Will: se a placa perder energia ou WiFi sem um disconnect limpo, o broker publica offline no tópico de status após o timeout de keep-alive. Logo depois de conectar, o sketch publica online no mesmo tópico. Ambas são retidas, então um dashboard que assine depois ainda vê o estado atual imediatamente. Esse padrão é explicado em Last Will and Testament e mensagens retidas (retained).
Testando pela linha de comando
Você não precisa de um segundo dispositivo para testar o tópico de comandos. Com os clientes do Mosquitto instalados, envie um comando e acompanhe a telemetria (substitua o ID pelo que aparece no Serial Monitor):
# watch everything your board publishes
mosquitto_sub -h broker.hivemq.com -t 'testmqtt/esp32/#' -v
# switch the LED on
mosquitto_pub -h broker.hivemq.com -t 'testmqtt/esp32/esp32-ABCD12345678/cmd' -m 'led:on'Mais receitas de linha de comando estão em exemplos de mosquitto_pub e mosquitto_sub.
Montando JSON com ArduinoJson
snprintf resolve para poucos campos, mas para dados aninhados ou para interpretar comandos em JSON, a biblioteca ArduinoJson (versão 7) é mais segura:
#include <ArduinoJson.h>
void publishTelemetry() {
JsonDocument doc;
doc["device"] = deviceId;
doc["temp"] = temperatureRead();
doc["rssi"] = WiFi.RSSI();
char payload[256];
size_t n = serializeJson(doc, payload, sizeof(payload));
mqtt.publish(topicTelemetry, (const uint8_t*)payload, n, false);
}Mantenha os payloads pequenos e planos: chaves curtas e números em vez de strings economizam RAM, tempo de rádio e banda do broker.
Solução de problemas pelos códigos de estado
mqtt.state() | Significado | Solução típica |
|---|---|---|
-4 | Timeout de conexão | Verifique o keep-alive e se o loop() roda com frequência |
-2 | Falha na conexão de rede | Host ou porta errados, firewall ou falha no handshake TLS |
2 | Client ID rejeitado | Use um client ID mais curto ou único |
4 | Usuário ou senha inválidos | Confira as credenciais, incluindo maiúsculas e minúsculas |
5 | Não autorizado | As regras de acesso do broker negam este cliente |
Adicionando TLS com WiFiClientSecure
A porta simples 1883 envia suas credenciais e dados em texto puro. Mudar para TLS na porta 8883 exige três alterações: incluir WiFiClientSecure.h, trocar o cliente de rede e fornecer o certificado da CA do seu broker:
#include <WiFiClientSecure.h>
// PEM of the root CA that signed your broker's certificate
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the root CA certificate here...
-----END CERTIFICATE-----
)EOF";
WiFiClientSecure net;
PubSubClient mqtt(net);
const uint16_t MQTT_PORT = 8883;
// in setup(), before mqtt.setServer():
net.setCACert(ROOT_CA);
// net.setInsecure(); // skips verification - local testing only!Muitos brokers públicos usam certificados da Let's Encrypt, cuja raiz é a ISRG Root X1; confira a documentação do seu broker para saber qual é a certa. Em algumas configurações, o TLS também precisa da hora correta para validar os certificados, e ele usa bem mais RAM — um ESP32 dá conta tranquilamente, um ESP8266 sofre. Para o panorama completo, leia nossas boas práticas de segurança MQTT.
Quer este sketch com o host, a porta e as credenciais do seu broker já preenchidos? O gerador de código MQTT o gera com as configurações do seu próprio broker.
Perguntas frequentes
Por que o PubSubClient não publica mensagens maiores que 256 bytes?
O PubSubClient usa um buffer de 256 bytes por padrão, e esse buffer precisa comportar o pacote MQTT inteiro, incluindo o tópico. publish() retorna false quando o pacote não cabe. Chame client.setBufferSize(1024) ou mais antes de conectar.
Por que meu ESP32 fica desconectando do broker MQTT?
As causas mais comuns são um client ID que outro dispositivo também usa, chamadas longas de delay() que impedem client.loop() de rodar e fazem perder os pings de keep-alive, e WiFi fraco. Use um client ID único por dispositivo e mantenha o loop() não bloqueante.
O PubSubClient suporta QoS 1 e MQTT 5?
O PubSubClient fala apenas MQTT 3.1.1. Ele pode assinar com QoS 0 ou 1, mas publica apenas com QoS 0. Se você precisa publicar com QoS 1 ou de recursos do MQTT 5, considere bibliotecas como AsyncMqttClient ou o componente esp-mqtt do ESP-IDF.