O que vamos construir: um ESP32 com sensor DHT22 publicando temperatura e umidade via MQTT num broker Mosquitto self-hosted (Docker, com autenticação por senha — nada de broker aberto), com presença online/offline resolvida por Last Will e um dashboard em tempo real no Node-RED com gauge e gráfico. De quebra, o caminho de volta: um comando do dashboard acendendo o LED da placa. Todo o código, do docker-compose.yml ao firmware.
MQTT é o protocolo que carrega a Internet das Coisas há duas décadas — o site oficial o define como um transporte de mensagens publish/subscribe "extremamente leve", padronizado pela OASIS e desenhado pra dispositivos pequenos em redes ruins. É o mesmo padrão que roda em automotivo, manufatura e telecom — e é o que você deveria usar no lugar daquela chamada HTTP que o seu ESP32 faz a cada 30 segundos pra uma API que não pode responder de volta.
Este é o segundo tutorial DIY da casa — no primeiro, conectamos um ESP32 a um app React Native por BLE. BLE resolve proximidade; MQTT resolve rede: muitos dispositivos, um servidor, dados fluindo o tempo todo. Você vai precisar de uma placa ESP32 DevKit, um sensor DHT22 (o azulzinho DHT11 também serve, com menos precisão) e qualquer máquina com Docker — um Raspberry Pi na sua rede, o Mac mini da bancada ou um VPS.
MQTT em 5 minutos: o modelo mental
Esqueça requisição e resposta. Em MQTT ninguém fala diretamente com ninguém: todo mundo conversa com o broker, que é um carteiro central. Quem tem dado publica num tópico; quem quer dado assina o tópico. O publicador não sabe quem assina, o assinante não sabe quem publica — e é esse desacoplamento que deixa você plugar um dashboard novo amanhã sem mexer uma linha no firmware.
| Conceito | O que é | No nosso projeto |
|---|---|---|
| Broker | O servidor central que recebe tudo e roteia pra quem assinou. | Mosquitto no Docker |
| Tópico | Endereço hierárquico da mensagem, separado por barras. | rhodium/sala/esp32-sala-01/telemetria |
| Publish | Enviar uma mensagem pra um tópico. | O ESP32, a cada 10 s |
| Subscribe | Assinar um tópico (ou um padrão) e receber tudo que chegar nele. | O Node-RED (e o ESP32, pro comando) |
| Retained | O broker guarda a última mensagem do tópico e entrega na hora pra quem assinar depois. | O status online/offline |
| Last Will | Mensagem que o broker publica sozinho se o cliente cair sem se despedir. | "offline" no tópico de status |
| QoS | Garantia de entrega negociada por mensagem: 0, 1 ou 2. | 0 na telemetria, 1 no comando |
Tópicos e wildcards
Tópico é hierarquia, e desenhar bem a hierarquia é o que separa um projeto que escala de um que vira espaguete. As boas práticas consolidadas pela HiveMQ cabem em quatro regras: nunca comece com barra (cria um nível vazio inútil), não use espaço nem acento, lembre que tópico é case sensitive, e seja específico — um tópico por tipo de dado, com o identificador do dispositivo no caminho. O nosso padrão: empresa/local/dispositivo/assunto.
Na hora de assinar, entram os wildcards: + substitui um nível (rhodium/+/+/telemetria pega a telemetria de todos os dispositivos de todos os locais) e # pega tudo dali pra baixo (rhodium/# — ótimo pra debugar, perigoso pra assinar em produção, porque entope o cliente com tudo).
QoS: a tabela de decisão
| Nível | Garantia (spec OASIS) | Quando usar |
|---|---|---|
| QoS 0 | "No máximo uma vez" — pode perder, nunca duplica. | Telemetria frequente: perdeu uma leitura, a próxima chega em 10 s. É o mais barato em rádio e bateria. |
| QoS 1 | "Pelo menos uma vez" — chega garantido, pode duplicar. | Eventos e comandos. Seu código precisa tolerar duplicata (idempotência). |
| QoS 2 | "Exatamente uma vez" — handshake duplo, o mais caro. | Quase nunca. Prefira consumidor idempotente com QoS 1 a pagar QoS 2 em tudo. |
As definições vêm da especificação oficial OASIS do MQTT 5.0 (publicada em março de 2019), que também define o recurso mais subestimado do protocolo:
Last Will: o dispositivo que avisa a própria morte
O problema clássico de toda telemetria: como saber que o dispositivo caiu? Ele não vai mandar mensagem dizendo "caí" — ele caiu. Em HTTP isso vira gambiarra de timeout no servidor. Em MQTT é recurso de protocolo: na conexão, o cliente registra uma Will Message ("testamento") que o broker guarda e, nas palavras da spec, publica "prontamente após a conexão de rede ser fechada" de forma anormal — queda de energia, Wi-Fi que sumiu, keepalive estourado. Combinando o testamento offline com um publish retido de online ao conectar, qualquer assinante sabe o estado real de cada dispositivo, a qualquer momento, sem perguntar nada. É assim que dashboard de frota mostra bolinha verde e vermelha.
Passo 1 — O broker: Mosquitto no Docker, com senha
O Eclipse Mosquitto é o broker open source de referência (licença EPL/EDL, mantido pela Eclipse Foundation): implementa MQTT 5.0, 3.1.1 e 3.1 e roda em qualquer coisa, "de computador de placa única a servidor completo". Vamos subir a imagem Docker oficial — e já com autenticação, porque broker aberto na internet é convite pra virar infraestrutura gratuita de estranhos. Mesmo em rede local, criar usuário e senha custa dois comandos; faça sempre.
services:
mosquitto:
image: eclipse-mosquitto:2
restart: unless-stopped
ports:
- "1883:1883" # MQTT (TCP)
volumes:
- ./mosquitto/config:/mosquitto/config
- ./mosquitto/data:/mosquitto/data
- ./mosquitto/log:/mosquitto/log
nodered:
image: nodered/node-red:latest
restart: unless-stopped
ports:
- "1880:1880" # editor + dashboard
volumes:
- ./nodered:/data
listener 1883
allow_anonymous false
password_file /mosquitto/config/passwd
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log
log_dest stdout
Crie o arquivo de senhas com a ferramenta mosquitto_passwd que já vem na imagem (a flag -c cria o arquivo — só na primeira vez, porque ela sobrescreve; as senhas são armazenadas com hash, não em texto puro):
mkdir -p mosquitto/config mosquitto/data mosquitto/log nodered
# (crie o mosquitto.conf acima antes de subir)
docker compose up -d mosquitto
# usuário do ESP32 (-c só na PRIMEIRA vez: cria o arquivo)
docker compose exec mosquitto mosquitto_passwd -c /mosquitto/config/passwd esp32
# usuário do dashboard (sem -c: adiciona ao arquivo existente)
docker compose exec mosquitto mosquitto_passwd /mosquitto/config/passwd nodered
# recarrega a config com os usuários novos
docker compose restart mosquitto
Teste antes de envolver hardware
Regra de ouro de integração: prove cada elo da corrente separadamente. A própria imagem traz os clientes de linha de comando mosquitto_sub e mosquitto_pub. Num terminal, assine tudo; no outro, publique:
# terminal 1: assina a árvore inteira (o -v mostra o tópico junto)
docker compose exec mosquitto \
mosquitto_sub -u esp32 -P SUA_SENHA -t 'rhodium/#' -v
# terminal 2: publica uma mensagem de teste
docker compose exec mosquitto \
mosquitto_pub -u esp32 -P SUA_SENHA \
-t 'rhodium/sala/teste/telemetria' -m '{"temperatura":25.1}'
Se a mensagem apareceu no terminal 1, o broker está pronto. Se apareceu Connection error: Connection Refused: not authorised, o problema é usuário/senha — resolva agora, com dois terminais na sua frente, e não depois, olhando um LED piscar sem saber por quê.
Passo 2 — O firmware: ESP32 publicando com Last Will
Bibliotecas (e uma nota de honestidade)
Boa parte dos tutoriais de MQTT + ESP32 na internet usa a PubSubClient. Ela funcionou por uma década, mas o próprio repositório hoje avisa: a biblioteca está descontinuada, e o autor recomenda alternativas mantidas. Por isso este tutorial usa a ArduinoMqttClient, biblioteca oficial da organização Arduino, que cobre o que precisamos com API limpa: Last Will (beginWill/endWill), QoS na assinatura (subscribe(topic, qos)) e keepalive configurável (setKeepAliveInterval). Pro sensor, a DHT sensor library da Adafruit (instale junto a dependência dela, a Adafruit Unified Sensor — o Library Manager oferece).
- Arduino IDE: instale "ArduinoMqttClient", "DHT sensor library" e "Adafruit Unified Sensor" pelo Library Manager. Board: "ESP32 Dev Module".
- PlatformIO:
lib_deps = arduino-libraries/ArduinoMqttClient, adafruit/DHT sensor library, adafruit/Adafruit Unified Sensorcomboard = esp32dev,framework = arduino. - Fiação do DHT22: VCC no 3V3, GND no GND, DATA no GPIO 4 (com resistor de pull-up de 10 kΩ entre DATA e VCC — módulos em plaquinha geralmente já o trazem).
O sketch completo
#include <WiFi.h>
#include <ArduinoMqttClient.h>
#include <DHT.h>
// ---------- ajuste aqui ----------
const char WIFI_SSID[] = "SUA_REDE";
const char WIFI_PASS[] = "SUA_SENHA_WIFI";
const char MQTT_HOST[] = "192.168.0.10"; // IP da máquina do Docker
const int MQTT_PORT = 1883;
const char MQTT_USER[] = "esp32";
const char MQTT_PASS[] = "SUA_SENHA_MQTT";
// ---------------------------------
// um ID único por dispositivo — repetido, o broker derruba o anterior
const char DEVICE_ID[] = "esp32-sala-01";
const char TOPIC_TELEMETRY[] = "rhodium/sala/esp32-sala-01/telemetria";
const char TOPIC_STATUS[] = "rhodium/sala/esp32-sala-01/status";
const char TOPIC_LED[] = "rhodium/sala/esp32-sala-01/comando/led";
const uint32_t PUBLISH_INTERVAL_MS = 10000;
const uint32_t KEEPALIVE_MS = 30000;
const uint8_t DHT_PIN = 4;
const uint8_t LED_PIN = 2; // LED azul onboard na maioria dos DevKits
DHT dht(DHT_PIN, DHT22);
WiFiClient net;
MqttClient mqtt(net);
uint32_t lastPublish = 0;
void connectWiFi() {
if (WiFi.status() == WL_CONNECTED) return;
Serial.print("Wi-Fi: conectando");
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.printf("\nWi-Fi ok: %s\n", WiFi.localIP().toString().c_str());
}
void onMqttMessage(int messageSize) {
String topic = mqtt.messageTopic();
String payload;
while (mqtt.available()) payload += (char)mqtt.read();
Serial.printf("comando em %s: %s\n", topic.c_str(), payload.c_str());
if (topic == TOPIC_LED) {
digitalWrite(LED_PIN, payload == "on" ? HIGH : LOW);
}
}
void connectMqtt() {
mqtt.setId(DEVICE_ID);
mqtt.setUsernamePassword(MQTT_USER, MQTT_PASS);
mqtt.setKeepAliveInterval(KEEPALIVE_MS);
// Last Will: se o ESP32 sumir sem se despedir, o broker
// publica "offline" (retido) no tópico de status por conta própria
mqtt.beginWill(TOPIC_STATUS, true /*retain*/, 1 /*qos*/);
mqtt.print("offline");
mqtt.endWill();
Serial.print("MQTT: conectando");
while (!mqtt.connect(MQTT_HOST, MQTT_PORT)) {
Serial.printf(" (erro %d)", mqtt.connectError());
delay(2000);
}
Serial.println(" ok");
// presença: "online", retido — quem assinar depois recebe na hora
mqtt.beginMessage(TOPIC_STATUS, true /*retain*/, 1 /*qos*/);
mqtt.print("online");
mqtt.endMessage();
mqtt.onMessage(onMqttMessage);
mqtt.subscribe(TOPIC_LED, 1); // comando: QoS 1
}
void publishTelemetry() {
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
if (isnan(temperature) || isnan(humidity)) {
Serial.println("DHT22: leitura falhou, pulando ciclo");
return;
}
char payload[96];
snprintf(payload, sizeof(payload),
"{\"temperatura\":%.1f,\"umidade\":%.1f}",
temperature, humidity);
// telemetria frequente: QoS 0, sem retain
mqtt.beginMessage(TOPIC_TELEMETRY);
mqtt.print(payload);
mqtt.endMessage();
Serial.println(payload);
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
dht.begin();
connectWiFi();
connectMqtt();
}
void loop() {
// reconexão: Wi-Fi primeiro, MQTT depois — nessa ordem
if (WiFi.status() != WL_CONNECTED) connectWiFi();
if (!mqtt.connected()) connectMqtt();
mqtt.poll(); // mantém keepalive e entrega os comandos assinados
if (millis() - lastPublish >= PUBLISH_INTERVAL_MS) {
lastPublish = millis();
publishTelemetry();
}
}
Três decisões do sketch que valem sublinhar, porque são as que faltam nos tutoriais rasos:
- O par testamento + retained. Ao conectar, publicamos
onlineretido; o testamento registraofflineretido. O tópico de status sempre reflete a verdade — inclusive pra um dashboard que abrir três horas depois da queda. - QoS por finalidade. Telemetria vai em QoS 0 (perder uma leitura de 10 s não importa); o comando do LED vai em QoS 1 (comando perdido importa). É exatamente a tabela de decisão de cima aplicada.
mqtt.poll()em todo loop. É ele que mantém o keepalive e recebe as mensagens assinadas. Bloqueou o loop comdelay()longo, o broker te derruba por keepalive estourado — e publica seu testamento.
Grave, abra o Serial Monitor e confira no terminal do mosquitto_sub (que você deixou aberto, certo?): o status online chega retido, e o JSON pinga a cada 10 segundos.
Passo 3 — O dashboard: Node-RED com gauge e gráfico
O Node-RED (projeto open source da OpenJS Foundation) é programação por fluxo: você liga nós numa tela e o dado corre por eles. Tem cliente MQTT nativo e, com o Dashboard 2.0 (@flowfuse/node-red-dashboard) — o sucessor oficial do antigo node-red-dashboard, Apache 2.0, com widgets ui-gauge e ui-chart —, vira painel de telemetria em minutos. O container já subiu no compose do Passo 1; acesse http://IP-DA-MAQUINA:1880 e instale o Dashboard 2.0 em Menu → Manage palette → Install, buscando @flowfuse/node-red-dashboard.
Importe o encanamento do fluxo (Menu → Import, cole o JSON) — ele traz a conexão MQTT, o parse do JSON e a separação de temperatura e umidade:
[
{ "id": "broker1", "type": "mqtt-broker", "name": "mosquitto",
"broker": "mosquitto", "port": "1883", "clientid": "nodered-dash",
"autoConnect": true, "protocolVersion": "4", "keepalive": "60",
"cleansession": true },
{ "id": "in1", "type": "mqtt in", "z": "flow1", "name": "telemetria",
"topic": "rhodium/+/+/telemetria", "qos": "0", "datatype": "auto-detect",
"broker": "broker1", "x": 150, "y": 120, "wires": [["json1"]] },
{ "id": "json1", "type": "json", "z": "flow1", "name": "parse",
"property": "payload", "action": "obj", "x": 330, "y": 120,
"wires": [["temp1", "umid1"]] },
{ "id": "temp1", "type": "change", "z": "flow1", "name": "temperatura",
"rules": [{ "t": "set", "p": "payload", "pt": "msg",
"to": "payload.temperatura", "tot": "msg" }],
"x": 530, "y": 80, "wires": [["debug1"]] },
{ "id": "umid1", "type": "change", "z": "flow1", "name": "umidade",
"rules": [{ "t": "set", "p": "payload", "pt": "msg",
"to": "payload.umidade", "tot": "msg" }],
"x": 530, "y": 160, "wires": [["debug1"]] },
{ "id": "debug1", "type": "debug", "z": "flow1", "name": "saída",
"active": true, "tosidebar": true, "x": 720, "y": 120, "wires": [] },
{ "id": "flow1", "type": "tab", "label": "Telemetria ESP32" }
]
Dois detalhes do fluxo: no config node do broker, o host é mosquitto (nome do serviço no compose — os containers se enxergam pelo nome, não por localhost), e você precisa preencher usuário e senha na aba Security com o usuário nodered criado no Passo 1. Feito o deploy, o painel de debug já mostra os valores chegando.
Os widgets ficam melhores configurados na interface do que colados por JSON (o formato de export do dashboard muda entre versões — honestidade DIY). Com o encanamento rodando, são quatro cliques:
- Arraste um ui-gauge e ligue na saída do nó temperatura: min 0, max 50, unidade °C, e crie o grupo/página do dashboard quando o editor pedir.
- Arraste um ui-chart (tipo linha) e ligue na mesma saída — série temporal da temperatura.
- Repita pro nó umidade (gauge 0–100%).
- Pro caminho de volta: um ui-switch ligado num nó mqtt out com o tópico
rhodium/sala/esp32-sala-01/comando/led, payloadon/off, QoS 1. Deploy, toque no switch — e o LED da placa obedece. Telemetria virou controle.
Dashboard em http://IP-DA-MAQUINA:1880/dashboard. Gauge subindo, gráfico correndo, switch mandando: os três pilares de qualquer sistema de monitoramento, montados numa tarde.
Troubleshooting: o que morde de verdade
- Conecta e cai, conecta e cai: dois clientes com o mesmo client ID. O broker aceita o novo e derruba o antigo — em loop, se os dois reconectam. Todo dispositivo precisa de ID único (por isso o
DEVICE_IDno topo do sketch). Connection Refused: not authorised: usuário/senha errados ouallow_anonymous falsesempassword_filecarregado. Lembre dodocker compose restart mosquittodepois de mexer no passwd.- Publica mas ninguém recebe: tópico é case sensitive e literal —
Rhodium/...≠rhodium/..., e um espaço invisível no fim do tópico conta. Confira commosquitto_sub -t 'rhodium/#' -v: o-vimprime o tópico exato de cada mensagem. - Comando não chega no ESP32: quase sempre é
mqtt.poll()ausente ou umdelay()gigante no loop. O poll é o coração do cliente — sem ele nada entra e o keepalive morre. - Broker derruba o ESP32 a cada ~30 s: keepalive estourado por loop bloqueado (leitura de sensor lenta, espera síncrona). Meça o tempo do seu loop; mantenha-o abaixo de metade do keepalive.
- Payload chega truncado de outra lib: se você insistir na PubSubClient, saiba que o buffer padrão dela é de 256 bytes por pacote (configurável via
setBufferSize()) — JSON maior que isso é descartado silenciosamente. Mais um motivo pra lib mantida. - Wi-Fi voltou mas o MQTT não: reconecte nas duas camadas, na ordem (Wi-Fi → MQTT), como o loop do sketch faz. Cliente MQTT não sobrevive à troca de rede por baixo dele.
Daqui pra produção
O que você montou é a versão de bancada da arquitetura que opera frota de verdade: no nosso ecossistema de venda autônoma e dispositivos conectados — mais de 800 unidades em campo — telemetria, status de máquina e comandos viajam exatamente neste padrão publish/subscribe, com broker autenticado e presença por Last Will. A distância entre o tutorial e o produto está no de sempre: TLS no transporte (o próximo passo natural deste setup), atualização de firmware remota, retenção histórica (aí entra um banco de série temporal atrás do Node-RED, como InfluxDB + Grafana), alarme de dispositivo mudo e o suporte do dia 200. Essa ponte é o que construímos pra clientes — começando, como todo projeto nosso, pela Fase 1: o Diagnóstico de Viabilidade Técnica.
Perguntas frequentes
Preciso de um broker MQTT na nuvem ou posso rodar em casa?
Pode rodar onde quiser: o Mosquitto é leve o bastante pra um Raspberry Pi ou pra um container mínimo num VPS. Broker gerenciado na nuvem faz sentido quando os dispositivos estão espalhados pela internet e você não quer administrar TLS, certificado e disponibilidade — em rede local ou VPN, self-hosted resolve com custo zero.
Quando usar QoS 0, 1 ou 2?
QoS 0 (no máximo uma vez) serve pra telemetria frequente onde perder uma leitura não dói — a próxima chega em segundos. QoS 1 (pelo menos uma vez) é o padrão sensato pra eventos e comandos, aceitando duplicata ocasional que seu código deve tolerar. QoS 2 (exatamente uma vez) é o mais caro em tráfego e raramente necessário — prefira tornar o consumidor idempotente a pagar o handshake duplo em toda mensagem.
Quantos dispositivos um Mosquitto aguenta?
Pra escala de projeto DIY e de pequena frota (dezenas a centenas de dispositivos publicando a cada poucos segundos), um Mosquitto num hardware modesto sobra. O limite real costuma ser a rede e o padrão de tópicos, não o broker. Quando a frota cresce pra milhares de conexões simultâneas com requisitos de alta disponibilidade, aí entram brokers clusterizados — outro campeonato.
Por que MQTT em vez de o ESP32 chamar uma API HTTP?
HTTP funciona pra mandar leitura de vez em quando, mas cada requisição paga handshake e cabeçalhos inteiros, o dispositivo não fica endereçável (o servidor não consegue "chamar" o ESP32 pra mandar um comando) e detectar dispositivo morto vira gambiarra de timeout. MQTT resolve os três: conexão persistente e leve, comando na direção contrária por assinatura de tópico, e Last Will avisando a queda na hora.
Tem uma operação que precisa de telemetria de verdade?
Do sensor na bancada à frota monitorada com alarme, histórico e suporte, o caminho tem decisões que custam caro quando erradas — broker, segurança, retenção, atualização remota. Na Fase 1 — o Diagnóstico de Viabilidade Técnica — colocamos seu cenário na mesa e você sai com o veredicto: arquitetura, custo e prazo. Sessão de até 3 horas, R$ 4.900, abatidos do projeto se você seguir.
Agendar meu Diagnóstico →Referências
- OASIS — MQTT: The Standard for IoT Messaging (site oficial)
- OASIS — MQTT Version 5.0 — OASIS Standard (2019)
- Eclipse Foundation — Eclipse Mosquitto: an open source MQTT broker
- Docker Hub — eclipse-mosquitto (imagem oficial)
- Eclipse Foundation — mosquitto_passwd — manual
- Arduino — ArduinoMqttClient (biblioteca oficial)
- Nick O'Leary — PubSubClient (descontinuada — nota do autor no repositório)
- Adafruit — DHT sensor library (DHT11/DHT22)
- HiveMQ — MQTT Essentials: Topics & Best Practices
- FlowFuse — Node-RED Dashboard 2.0 (@flowfuse/node-red-dashboard)
- OpenJS Foundation — Node-RED: documentação oficial
Falar com a gente →