Sala de operação escura com telas exibindo métricas e gráficos em verde — telemetria em tempo real
Telemetria de verdade: o sensor publica, o broker roteia, o dashboard mostra — e ninguém pergunta nada a ninguém

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.

ConceitoO que éNo nosso projeto
BrokerO servidor central que recebe tudo e roteia pra quem assinou.Mosquitto no Docker
TópicoEndereço hierárquico da mensagem, separado por barras.rhodium/sala/esp32-sala-01/telemetria
PublishEnviar uma mensagem pra um tópico.O ESP32, a cada 10 s
SubscribeAssinar um tópico (ou um padrão) e receber tudo que chegar nele.O Node-RED (e o ESP32, pro comando)
RetainedO broker guarda a última mensagem do tópico e entrega na hora pra quem assinar depois.O status online/offline
Last WillMensagem que o broker publica sozinho se o cliente cair sem se despedir."offline" no tópico de status
QoSGarantia 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ívelGarantia (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.

docker-compose.yml
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
mosquitto/config/mosquitto.conf
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):

terminal — criar usuários e subir
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 — assinar / terminal 2 — publicar
# 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).

O sketch completo

firmware/mqtt_telemetry.ino — C++ (Arduino framework)
#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:

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:

flow.json — importar no Node-RED (Menu → Import)
[
  { "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:

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

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
R

Sobre o autor

Rhodion Souza Araújo é fundador e CEO da Rhodium Tecnologia, em breve completando 30 anos de trajetória em tecnologia e mais de 100 automações entregues em produção desde 2016 — de apps e agentes de IA a hardware de venda autônoma com mais de 800 unidades em campo. LinkedIn