Combinar ESP32, ArduinoJson e MQTT é a forma mais comum de enviar dados de sensores estruturados a partir de um microcontrolador. Neste tutorial você vai montar telemetria JSON com o ArduinoJson 7, publicá-la de forma eficiente com o PubSubClient, interpretar comandos JSON no callback de mensagens e conhecer os limites de buffer que fazem mensagens sumirem sem nenhum erro.
Por que usar payloads JSON no MQTT
O MQTT não se importa com o conteúdo de um payload; ele só transporta bytes. Essa liberdade significa que você precisa escolher um formato. O JSON vence na maioria dos projetos com ESP32 porque qualquer dashboard, fluxo do Node-RED, script Python ou importador de banco de dados consegue lê-lo sem um decodificador personalizado. Uma mensagem como {"temp":22.4,"hum":51,"rssi":-63} se descreve sozinha, é fácil de estender com novos campos e pode ser lida em qualquer cliente MQTT enquanto você depura.
Os custos são o tamanho e o trabalho de interpretação, ambos desprezíveis nas taxas típicas de sensores em um ESP32. Se você envia centenas de mensagens por segundo ou tem um orçamento de bateria apertado, chaves compactas ajudam mais do que trocar de formato.
O ArduinoJson é a biblioteca padrão de fato para isso. A versão 7 eliminou a necessidade de escolher a capacidade do documento: o JsonDocument cresce conforme necessário no heap. Todo o código abaixo usa o ArduinoJson 7; se você está na versão 6, substitua JsonDocument por StaticJsonDocument<N> ou DynamicJsonDocument. A documentação oficial do ArduinoJson cobre os detalhes da migração.
O que você precisa
- Uma placa ESP32 e a Arduino IDE ou o PlatformIO.
- Bibliotecas: PubSubClient, de Nick O'Leary, e ArduinoJson, de Benoit Blanchon (7.x).
mosquitto_pubemosquitto_subno seu computador para enviar comandos e acompanhar a saída.
Os exemplos usam a área pública do broker.connectmq.com na porta 1883 com o prefixo de tópico connectmq/demo/a8e2d1. Troque esse prefixo por uma string aleatória sua e lembre-se de que tópicos públicos podem ser lidos por qualquer pessoa.
| Tópico | Direção | Payload |
|---|---|---|
connectmq/demo/a8e2d1/telemetry |
ESP32 publica | {"device":"...","temp":22.4,"hum":51,"uptime":120} |
connectmq/demo/a8e2d1/cmd |
ESP32 se inscreve | {"action":"set_interval","value":10} |
connectmq/demo/a8e2d1/ack |
ESP32 publica | {"ok":true,"action":"set_interval"} |
Sketch completo de ESP32 ArduinoJson MQTT
O sketch publica telemetria em um intervalo configurável e aceita comandos JSON para alterar esse intervalo ou ligar e desligar um LED.
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* TOPIC_TELEMETRY = "connectmq/demo/a8e2d1/telemetry";
const char* TOPIC_CMD = "connectmq/demo/a8e2d1/cmd";
const char* TOPIC_ACK = "connectmq/demo/a8e2d1/ack";
const int LED_PIN = 2;
WiFiClient net;
PubSubClient mqtt(net);
String deviceId;
unsigned long intervalMs = 5000;
unsigned long lastPublish = 0;
void publishAck(const char* action, bool ok, const char* error = nullptr) {
JsonDocument doc;
doc["ok"] = ok;
doc["action"] = action;
if (error) doc["error"] = error;
char buf[128];
size_t n = serializeJson(doc, buf, sizeof(buf));
mqtt.publish(TOPIC_ACK, (const uint8_t*)buf, n, false);
}
void onMessage(char* topic, byte* payload, unsigned int length) {
JsonDocument doc;
DeserializationError err = deserializeJson(doc, payload, length);
if (err) {
publishAck("unknown", false, err.c_str());
return;
}
const char* action = doc["action"] | "";
if (strcmp(action, "set_interval") == 0) {
int seconds = doc["value"] | 0;
if (seconds < 1 || seconds > 3600) {
publishAck(action, false, "value out of range");
return;
}
intervalMs = seconds * 1000UL;
publishAck(action, true);
} else if (strcmp(action, "led") == 0) {
bool on = doc["value"] | false;
digitalWrite(LED_PIN, on ? HIGH : LOW);
publishAck(action, true);
} else {
publishAck(action, false, "unknown action");
}
}
void publishTelemetry() {
JsonDocument doc;
doc["device"] = deviceId;
doc["temp"] = serialized(String(20.0 + random(0, 50) / 10.0, 1));
doc["hum"] = 45 + random(0, 15);
doc["uptime"] = millis() / 1000;
JsonObject network = doc["net"].to<JsonObject>();
network["rssi"] = WiFi.RSSI();
network["ip"] = WiFi.localIP().toString();
size_t len = measureJson(doc);
mqtt.beginPublish(TOPIC_TELEMETRY, len, false);
serializeJson(doc, mqtt); // stream straight into the MQTT packet
mqtt.endPublish();
}
void connectMqtt() {
while (!mqtt.connected()) {
if (mqtt.connect(deviceId.c_str())) {
mqtt.subscribe(TOPIC_CMD);
} else {
Serial.printf("MQTT failed, state=%d\n", mqtt.state());
delay(3000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
deviceId = "esp32-json-" + String((uint32_t)ESP.getEfuseMac(), HEX);
mqtt.setServer("broker.connectmq.com", 1883);
mqtt.setBufferSize(1024); // default is 256 bytes, too small for most JSON
mqtt.setCallback(onMessage);
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (millis() - lastPublish >= intervalMs) {
lastPublish = millis();
publishTelemetry();
}
}
Acompanhe a saída e envie comandos a partir do seu computador:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-a8e2d1 -t 'connectmq/demo/a8e2d1/#' -v
mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-a8e2d1-pub \
-t connectmq/demo/a8e2d1/cmd -m '{"action":"set_interval","value":10}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-a8e2d1-pub \
-t connectmq/demo/a8e2d1/cmd -m '{"action":"led","value":true}'
Serialização: três maneiras de colocar JSON em uma publicação
Em um buffer fixo
serializeJson(doc, buf, sizeof(buf)) escreve em um array de char e retorna o número de bytes escritos. É a opção mais simples e funciona bem para mensagens pequenas e previsíveis, como a confirmação acima. Passe o tamanho retornado para publish(), usando a sobrecarga que recebe um ponteiro de bytes e um tamanho, em vez de depender de strlen().
Transmitindo direto para o cliente MQTT
O PubSubClient herda da classe Print do Arduino, então o ArduinoJson pode escrever diretamente nele. measureJson(doc) fornece primeiro o tamanho exato, beginPublish() escreve o cabeçalho MQTT com esse tamanho, serializeJson(doc, mqtt) transmite o corpo e endPublish() finaliza o pacote. Nenhum buffer intermediário é necessário, o que torna essa a opção mais eficiente em memória para documentos maiores. Você ainda precisa de setBufferSize() para chamadas normais de publish() e para mensagens recebidas, que sempre são lidas para dentro do buffer.
Em uma String
serializeJson(doc, someString) é prático, mas aloca no heap a cada mensagem. Em um dispositivo que roda por semanas, alocações repetidas de String podem fragmentar a memória. Prefira as duas primeiras opções em firmware de longa duração.
Controlando a precisão de floats
O ArduinoJson imprime floats com dígitos suficientes para preservar o valor exato, o que pode gerar valores como 22.399999. Envolver uma string formatada em serialized() a insere literalmente, como na função de telemetria acima. Outra opção é arredondar o número antes de atribuí-lo: doc["temp"] = round(t * 10) / 10.0;.
Interpretando comandos JSON com segurança
O callback MQTT recebe um ponteiro de bytes e um tamanho, e o payload não termina em nulo. Passe sempre o tamanho: deserializeJson(doc, payload, length). Chamar apenas com (char*)payload lê além do fim da mensagem.
Mais três hábitos tornam a interpretação robusta:
- Verifique o erro. O
DeserializationErrorinforma se a entrada estava vazia, incompleta, inválida ou profunda demais. Reporte-o de volta em um tópico de confirmação, como o sketch faz, para que o remetente saiba que o comando falhou. - Use valores padrão com o operador
|.doc["value"] | 0retorna 0 se o campo estiver ausente ou tiver o tipo errado, então um comando malformado não consegue travar o dispositivo nem deixar uma variável sem inicialização. - Valide as faixas. O JSON ser interpretado com sucesso não significa que os valores façam sentido. Rejeite explicitamente intervalos de zero segundo ou valores de brilho de 5000.
Se você só precisa de alguns campos de um documento recebido grande, use um filtro para que o ArduinoJson ignore o resto:
JsonDocument filter;
filter["action"] = true;
filter["value"] = true;
JsonDocument doc;
deserializeJson(doc, payload, length, DeserializationOption::Filter(filter));
Armadilhas de buffer e memória
| Sintoma | Causa | Solução |
|---|---|---|
publish() retorna false e nada chega |
Pacote maior que o buffer padrão de 256 bytes do PubSubClient | mqtt.setBufferSize(1024) antes de connect() |
| O JSON recebido nunca chega ao callback | Mensagem recebida maior que o buffer | O mesmo: aumente o buffer para caber o maior comando esperado |
Erros IncompleteInput |
Interpretação com strlen() em um payload sem terminador |
Passe length para deserializeJson() |
| Reboots aleatórios após dias | Fragmentação do heap por serialização baseada em String | Serialize em buffers fixos ou transmita direto para o cliente |
| Valores impressos com muitas casas decimais | Formatação padrão de floats | serialized() ou arredondamento antes da atribuição |
Tenha em mente também os limites do broker. A área pública do broker.connectmq.com aceita até 64 KB por mensagem, e o plano Serverless gratuito do ConnectMQ até 256 KB, muito além do que um sketch de ESP32 deveria enviar em uma única mensagem. Se o seu JSON está ficando grande, divida-o em tópicos como .../telemetry e .../diagnostics. O guia de tamanho do buffer do PubSubClient explica a aritmética do buffer em detalhes.
Projetando o esquema JSON
Algumas convenções evitam dor de cabeça quando há mais de um dispositivo e mais de um consumidor envolvidos:
- Inclua um ID do dispositivo ou coloque-o no tópico, mas não os dois de forma inconsistente.
- Use unidades nos nomes das chaves (
temp_c,power_w) ou documente-as uma única vez; não misture unidades entre dispositivos. - Adicione um timestamp somente se o dispositivo tiver sincronizado a hora. Caso contrário, deixe o consumidor registrar a hora de chegada; veja como adicionar timestamps NTP a mensagens MQTT do ESP32.
- Mantenha comandos e telemetria em tópicos separados, para que um dispositivo nunca interprete as próprias mensagens.
- Versione o payload com um campo como
"v":1se o formato puder mudar.
Usando com sensores reais
Substitua os valores aleatórios por leituras reais e o resto do sketch continua igual. O tutorial de ESP32 com DHT22 e MQTT mostra o lado do sensor, e o tutorial de IoT com ESP32 monta um projeto completo de dispositivo em torno do mesmo padrão JSON.
Envie sua telemetria JSON para o ConnectMQ
Testar contra o servidor MQTT público (gratuito) é a forma mais rápida de iterar sobre um formato de payload. Quando os dados importam, migre para um broker privado: o ConnectMQ é um broker MQTT gerenciado cujo portal mostra suas mensagens JSON ao vivo no explorador de tópicos e permite criar regras de alerta por limite em campos numéricos. Cadastre-se no plano Serverless gratuito em app.connectmq.com, sem cartão de crédito, e depois mude o sketch para WiFiClientSecure na porta 8883 com YOUR_CLIENT_ID, YOUR_USERNAME e YOUR_PASSWORD.
Perguntas frequentes
Por que o PubSubClient não consegue publicar meu JSON?
O buffer padrão é de 256 bytes, incluindo o tópico e o cabeçalho. Chame mqtt.setBufferSize() com um valor maior antes de conectar e verifique o valor de retorno de publish().
Devo usar o ArduinoJson 6 ou 7?
Use a versão 7 em projetos novos. Ela substitui os documentos de capacidade fixa por um único JsonDocument que se dimensiona sozinho, eliminando uma fonte comum de saída truncada.
O JSON é pesado demais para um ESP32 via MQTT?
Não. Nas taxas típicas de sensores, o custo de interpretação e serialização é mínimo. Mantenha as chaves curtas e os payloads enxutos se estiver usando bateria ou enviando muitas mensagens por segundo.
Como interpreto o payload se ele não termina em nulo?
Passe o tamanho para o parser: deserializeJson(doc, payload, length). Assim o ArduinoJson lê exatamente os bytes da mensagem.



