ESP32 e Arduino

Tutorial da Biblioteca PubSubClient para Arduino: Configuração, Callbacks, Buffers e Keep Alive

Um tutorial prático de PubSubClient para ESP32 e ESP8266: instale a biblioteca, conecte-se a um broker MQTT, trate callbacks, dimensione o buffer, ajuste o keep alive e migre para TLS com credenciais reais.

Placa de microcontrolador executando a biblioteca PubSubClient e trocando mensagens MQTT com um broker na nuvem
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

PubSubClient é a biblioteca MQTT mais usada em placas compatíveis com Arduino, e ela é pequena o suficiente para rodar em quase qualquer coisa que tenha uma pilha de rede. Neste tutorial você vai instalá-la, conectar um ESP32 a um broker real, publicar e assinar mensagens e conhecer as três configurações que causam a maioria dos problemas em campo: tamanho do buffer, keep alive e timeout do socket. Ao final, você terá um sketch resistente a reconexões que poderá reutilizar em todos os seus projetos.

O Que É a PubSubClient (e o Que Ela Não É)

A PubSubClient, escrita por Nick O'Leary, é um cliente MQTT 3.1.1 leve. Ela não é dona da conexão de rede. Em vez disso, ela encapsula qualquer objeto que implemente a interface Client do Arduino: WiFiClient no ESP32 e no ESP8266, EthernetClient em um shield cabeado ou WiFiClientSecure quando você quer TLS. É por causa desse design que a mesma biblioteca roda em tantas placas.

Ela também é minimalista de propósito. Conhecer os limites desde o início economiza horas de depuração:

Recurso Comportamento da PubSubClient
Versão do protocolo MQTT 3.1.1 (MQTT 3.1 opcional em tempo de compilação), sem MQTT 5
QoS de publicação Apenas QoS 0
QoS de assinatura QoS 0 ou QoS 1
Buffer de pacote padrão 256 bytes (topic + payload + cabeçalho)
Keep alive padrão 15 segundos
Timeout de socket padrão 15 segundos
Last Will Suportado em connect()
Publicação retida Suportada
Modelo de threads Single-thread, você precisa chamar loop() com frequência

Se você precisa de propriedades do MQTT 5 ou de publicação com QoS 2 a partir do dispositivo, procure outro cliente. Para a grande maioria dos sensores e atuadores, telemetria com QoS 0 mais assinaturas de comandos com QoS 1 é exatamente o que você quer. Nosso guia sobre níveis de QoS do MQTT explica por que essa combinação funciona bem.

Instale a PubSubClient e Prepare a Placa

  1. Na Arduino IDE, abra Tools → Manage Libraries, procure por "PubSubClient" e instale a biblioteca de Nick O'Leary (versão 2.8 ou mais recente).
  2. Instale o pacote de placas de que você precisa: "esp32 by Espressif Systems" para placas ESP32 ou "esp8266 by ESP8266 Community" para placas no estilo NodeMCU.
  3. Selecione sua placa e a porta e, em seguida, envie uma vez o sketch Blink vazio para confirmar que o toolchain funciona.
  4. Escolha um ID de dispositivo exclusivo, por exemplo pubsub-7f3a9c. Você vai usá-lo tanto como client ID MQTT quanto como prefixo de topic.

No primeiro teste, você vai usar a área pública de broker.connectmq.com, o servidor MQTT público gratuito operado pela ConnectMQ. Ele aceita conexões anônimas na porta 1883, desde que o client ID não esteja vazio. Tudo o que é publicado ali pode ser lido por qualquer pessoa, então nunca envie credenciais reais ou dados pessoais para ele.

O Exemplo Completo de PubSubClient

O sketch abaixo se conecta ao Wi-Fi, conecta-se ao broker com um Last Will, publica uma mensagem JSON a cada cinco segundos, assina um topic de comandos e se reconecta sem bloquear o restante do seu código.

#include <WiFi.h>          // use <ESP8266WiFi.h> no ESP8266
#include <PubSubClient.h>

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* DEVICE_ID = "pubsub-7f3a9c";   // torne este valor exclusivo

char topicTelemetry[64];
char topicCommand[64];
char topicStatus[64];

WiFiClient netClient;
PubSubClient mqtt(netClient);

unsigned long lastPublish = 0;
unsigned long lastReconnectAttempt = 0;

void onMessage(char* topic, byte* payload, unsigned int length) {
  // o payload NÃO termina em nulo: copie-o usando o length
  char msg[64];
  unsigned int n = length < sizeof(msg) - 1 ? length : sizeof(msg) - 1;
  memcpy(msg, payload, n);
  msg[n] = '\0';

  Serial.printf("Message on %s: %s\n", topic, msg);

  if (strcmp(msg, "ping") == 0) {
    mqtt.publish(topicStatus, "pong");
  }
}

void connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nIP address: %s\n", WiFi.localIP().toString().c_str());
}

bool connectMqtt() {
  // connect(clientId, user, pass, willTopic, willQos, willRetain, willMessage)
  bool ok = mqtt.connect(DEVICE_ID, nullptr, nullptr,
                         topicStatus, 1, true, "offline");
  if (ok) {
    mqtt.publish(topicStatus, "online", true);
    mqtt.subscribe(topicCommand, 1);
    Serial.println("MQTT connected");
  } else {
    Serial.printf("MQTT connect failed, rc=%d\n", mqtt.state());
  }
  return ok;
}

void setup() {
  Serial.begin(115200);

  snprintf(topicTelemetry, sizeof(topicTelemetry), "connectmq/demo/%s/telemetry", DEVICE_ID);
  snprintf(topicCommand,   sizeof(topicCommand),   "connectmq/demo/%s/cmd", DEVICE_ID);
  snprintf(topicStatus,    sizeof(topicStatus),    "connectmq/demo/%s/status", DEVICE_ID);

  connectWifi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);   // espaço para payloads JSON maiores
  mqtt.setKeepAlive(30);     // segundos
  mqtt.setSocketTimeout(10); // segundos
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWifi();
  }

  if (!mqtt.connected()) {
    unsigned long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      connectMqtt();
    }
    return;
  }

  mqtt.loop();

  if (millis() - lastPublish > 5000) {
    lastPublish = millis();
    char payload[96];
    snprintf(payload, sizeof(payload),
             "{\"uptime\":%lu,\"rssi\":%d,\"heap\":%u}",
             millis() / 1000, WiFi.RSSI(), ESP.getFreeHeap());
    if (!mqtt.publish(topicTelemetry, payload)) {
      Serial.println("Publish failed");
    }
  }
}

Acompanhe a saída a partir do seu computador com as ferramentas de linha de comando do Mosquitto:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-7f3a9c -t "connectmq/demo/pubsub-7f3a9c/#" -v

Depois, envie um comando para a placa:

mosquitto_pub -h broker.connectmq.com -p 1883 -i sender-7f3a9c -t "connectmq/demo/pubsub-7f3a9c/cmd" -m "ping"

Você deverá ver pong aparecer no topic de status em menos de um segundo.

Como Funcionam os Callbacks da PubSubClient

A assinatura do callback é fixa: void callback(char* topic, byte* payload, unsigned int length). Três detalhes importam.

Primeiro, o payload é um ponteiro para o buffer interno da biblioteca e não termina em nulo. Tratá-lo como uma string C com Serial.println((char*)payload) imprime lixo depois da mensagem real. Sempre copie usando length, como faz o exemplo.

Segundo, o callback roda dentro de mqtt.loop(). Se você publicar de dentro do callback, a biblioteca reutiliza o mesmo buffer, então os ponteiros originais topic e payload deixam de ser válidos depois disso. Copie tudo o que precisar antes de chamar publish().

Terceiro, mantenha os callbacks curtos. Um delay(2000) dentro do callback impede que loop() atenda a conexão e pode disparar um timeout de keep alive. Defina uma flag ou armazene o valor e aja sobre ele no seu loop principal.

Tamanho do Buffer: A Armadilha Mais Comum da PubSubClient

Desde a versão 2.8, você pode chamar setBufferSize() em tempo de execução, em vez de editar MQTT_MAX_PACKET_SIZE no cabeçalho da biblioteca. O buffer precisa conter o pacote MQTT inteiro: cabeçalho fixo, tamanho do topic, topic e payload. Com o padrão de 256 bytes, um documento JSON de 220 caracteres em um topic de 40 caracteres simplesmente falha, e publish() retorna false sem qualquer outra explicação. Mensagens recebidas grandes demais são descartadas silenciosamente antes mesmo de o seu callback ser chamado.

Regras práticas:

  • Dimensione o buffer para o seu maior topic mais o payload, mais cerca de 10 bytes de cabeçalho.
  • Chame setBufferSize() antes de connect() e verifique o valor de retorno: ele retorna false se a alocação falhar.
  • No ESP8266, mantenha o buffer modesto, porque a RAM é compartilhada com o TLS e o Wi-Fi.

Entramos em detalhes, incluindo o envio de payloads grandes em streaming com beginPublish(), em tamanho do buffer da PubSubClient: corrigindo falhas com mensagens grandes.

Keep Alive e Timeout de Socket

O keep alive informa ao broker por quanto tempo o cliente pode ficar em silêncio. A PubSubClient envia um PINGREQ quando nada mais foi enviado durante o intervalo, mas apenas se loop() estiver sendo chamado. Se o seu código bloquear por mais de aproximadamente uma vez e meia o intervalo de keep alive, o broker derruba a sessão e o seu Last Will é disparado.

  • setKeepAlive(30) é um padrão sensato para dispositivos alimentados pela rede elétrica com um Wi-Fi razoável.
  • Use valores maiores (de 60 a 120 segundos) em links celulares, onde cada pacote custa energia ou dados.
  • setSocketTimeout(10) limita por quanto tempo connect() e as leituras podem bloquear. Valores menores evitam que uma rede instável congele seu dispositivo por 15 segundos de cada vez.

Lendo o Estado da Conexão

Quando connect() falha, mqtt.state() diz o motivo. Valores negativos são problemas no nível da rede; valores positivos são recusas do broker.

state() Significado
-4 Timeout de conexão, nenhum CONNACK a tempo
-3 Conexão perdida
-2 Falha na conexão de rede (DNS, porta, TLS, roteamento)
-1 Desconectado de forma limpa
0 Conectado
1 Versão de protocolo inválida
2 Client ID rejeitado
3 Servidor indisponível
4 Usuário ou senha inválidos
5 Não autorizado

De longe, o mais comum é o -2. Nosso guia dedicado sobre PubSubClient rc=-2 e outros códigos de estado percorre cada valor com as respectivas correções.

Migrando para TLS e Credenciais Reais

O servidor MQTT público é perfeito para aprender, mas dispositivos em produção precisam de criptografia e de credenciais próprias. Com um broker MQTT gerenciado da ConnectMQ, você cria um broker no portal e recebe um username, uma senha e um client ID gerados. A mudança no sketch é pequena:

#include <WiFiClientSecure.h>

WiFiClientSecure netClient;   // substitui o WiFiClient
PubSubClient mqtt(netClient);

// ISRG Root X1, a raiz Let's Encrypt usada por broker.connectmq.com
const char* ROOT_CA = R"EOF(
-----BEGIN CERTIFICATE-----
...paste ISRG Root X1 PEM here...
-----END CERTIFICATE-----
)EOF";

void setup() {
  // ... Wi-Fi como antes ...
  netClient.setCACert(ROOT_CA);
  mqtt.setServer("broker.connectmq.com", 8883);
}

bool connectMqtt() {
  return mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD",
                      topicStatus, 1, true, "offline");
}

A ConnectMQ vincula cada client ID às credenciais que emitiu, então use exatamente o client ID do portal. Em um broker privado, os prefixos de topic ficam a seu critério; o prefixo público connectmq/demo/... deixa de ser necessário. Para mais ideias de proteção, veja boas práticas de segurança MQTT.

Checklist de Solução de Problemas

  1. Conecta e depois cai a cada poucos segundos: dois dispositivos compartilham o mesmo client ID. O broker desconecta a sessão mais antiga sempre que o segundo se conecta.
  2. publish() retorna false: o payload mais o topic excede o buffer, ou o cliente não está conectado.
  3. O callback nunca é disparado: você esqueceu o mqtt.loop(), a assinatura foi feita antes de a conexão existir ou a mensagem é maior que o buffer.
  4. Desconexões aleatórias sob carga: código bloqueante longo dentro de loop(); divida-o ou mova-o para um timer.
  5. Funciona na 1883, falha na 8883: certificado raiz errado, ou o relógio não está ajustado em placas que verificam as datas de validade (comum no ESP8266).

Se você está escolhendo entre bibliotecas, o guia da biblioteca ArduinoMqttClient e o cliente MQTT assíncrono para ESP32 cobrem as principais alternativas, e o tutorial de IoT com ESP32 junta todas as peças em um projeto de sensor completo.

Conecte Seus Dispositivos PubSubClient à ConnectMQ

Depois que o seu sketch estiver rodando no servidor MQTT público, migrá-lo para um broker MQTT na nuvem privado é uma mudança de duas linhas. A ConnectMQ oferece MQTT via TCP, TLS na porta 8883 e WebSockets, um cliente MQTT no navegador para testes, um explorador de topics ao vivo e regras de alerta por limite. Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito, e você terá credenciais em poucos minutos.

Perguntas frequentes

A PubSubClient suporta MQTT 5?

Não. A PubSubClient fala MQTT 3.1.1, que todos os brokers mais usados, incluindo a ConnectMQ, ainda suportam junto com o MQTT 5. Se você precisa de propriedades do MQTT 5 no dispositivo, escolha outra biblioteca cliente.

A PubSubClient consegue publicar com QoS 1?

Não. publish() sempre envia com QoS 0. Você pode assinar com QoS 1, de modo que os comandos enviados ao dispositivo com QoS 1 sejam entregues de forma confiável pelo broker.

Por que o payload da PubSubClient imprime caracteres estranhos?

O buffer do payload não termina em nulo. Copie length bytes para o seu próprio buffer e adicione um \0 de terminação antes de tratá-lo como string.

Qual é o tamanho máximo de mensagem na PubSubClient?

O tamanho do buffer é um valor de 16 bits, então o teto prático fica um pouco abaixo de 64 KB, mas a RAM é o limite real. Mantenha as mensagens pequenas em placas com recursos limitados e lembre-se de que o plano do seu broker também tem um tamanho máximo de mensagem.

Artigos relacionados

Exemplo de MQTT com ESP-IDF: Monte um Cliente esp-mqtt do Zero

Monte um projeto ESP-IDF a partir de uma pasta vazia com o componente esp-mqtt: configuração do Wi-Fi em modo station, o handler de eventos MQTT, publicação e inscrição, propriedades do MQTT 5 e TLS com o bundle de certificados.

8 min de leitura

Usando os Recursos do MQTT 5 do esp-mqtt no ESP32 com ESP-IDF

Um projeto ESP-IDF prático que ativa o MQTT 5.0 no esp-mqtt e usa user properties, expiração de mensagens, topic aliases, expiração de sessão e response topics com um broker MQTT 5 real.

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.