ESP32 e Arduino

Cliente MQTT Assíncrono no ESP32: MQTT Não Bloqueante com AsyncMqttClient

Veja como o AsyncMqttClient mantém o firmware do ESP32 responsivo: callbacks orientados a eventos, reconexão automática com timers do FreeRTOS, publicação com QoS 1 e 2 e tratamento de payloads fragmentados.

Placa ESP32 com setas de eventos ilustrando um cliente MQTT assíncrono no ESP32 enviando mensagens sem bloquear
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

Um cliente MQTT assíncrono no ESP32 permite que o seu firmware continue lendo sensores, controlando displays e servindo páginas web enquanto o tráfego MQTT é tratado em segundo plano. Neste guia, você vai montar um sketch completo com AsyncMqttClient, com callbacks de eventos, reconexão automática e publicação com QoS 1 em um broker real. Também vai conhecer as armadilhas da biblioteca para decidir quando ela é a escolha certa.

Por Que Usar um Cliente MQTT Assíncrono

Bibliotecas bloqueantes como o PubSubClient funcionam bem, mas dependem de você chamar loop() com frequência, e connect() pode congelar o sketch até o timeout do socket quando a rede está ruim. Em um dispositivo que também aciona o motor de uma cortina de aviário em uma fazenda, atualiza uma fita de LED ou hospeda uma página de configuração, essas pausas ficam visíveis.

O AsyncMqttClient, de Marvin Roger, adota outra abordagem. Ele roda sobre a biblioteca AsyncTCP, que executa a rede em sua própria tarefa do FreeRTOS. O seu código registra callbacks para eventos (conectado, desconectado, mensagem recebida, publicação confirmada) e a biblioteca os chama quando algo acontece. Não há loop() para chamar.

Aspecto PubSubClient AsyncMqttClient
Modelo de programação Polling, chamar loop() Callbacks de eventos
Comportamento de connect() Bloqueia até o CONNACK ou o timeout Retorna imediatamente
QoS de publicação 0 0, 1 e 2
QoS de inscrição 0 e 1 0, 1 e 2
Payloads grandes recebidos Precisam caber no buffer Entregues em partes
TLS no ESP32 Sim, via WiFiClientSecure Inviável com o AsyncTCP padrão
Dependências Nenhuma AsyncTCP

A última linha importa. O suporte a TLS do AsyncMqttClient depende de uma compilação do AsyncTCP com SSL, que não está disponível na biblioteca AsyncTCP padrão do ESP32. Na prática, esta biblioteca é melhor para MQTT puro na porta 1883 dentro de uma rede confiável, ou para prototipagem. Se você precisa de TLS com uma API orientada a eventos, a biblioteca espMqttClient, mantida ativamente, oferece um estilo de callbacks muito parecido com suporte a TLS no ESP32. Tratamos dessas escolhas no final.

Instale as Bibliotecas

  1. Instale o pacote de placas "esp32 by Espressif Systems" na Arduino IDE.
  2. Instale o AsyncTCP para ESP32. Existem vários forks mantidos no Gerenciador de Bibliotecas; escolha um compatível com a versão do seu core ESP32.
  3. Instale o AsyncMqttClient. Se ele não aparecer no Gerenciador de Bibliotecas, baixe o ZIP da release no repositório do projeto e use Sketch → Include Library → Add .ZIP Library.
  4. Com PlatformIO, adicione os dois a lib_deps no platformio.ini.
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
    marvinroger/AsyncMqttClient
    esp32async/AsyncTCP

O Sketch Completo do Cliente MQTT Assíncrono no ESP32

Este exemplo se conecta ao broker.connectmq.com, o servidor MQTT público (gratuito) do ConnectMQ para testes e protótipos, na porta 1883. Ele reconecta o Wi-Fi e o MQTT com timers do FreeRTOS, publica telemetria com QoS 1 a cada dez segundos e alterna o LED da placa quando chega um comando.

#include <WiFi.h>
#include <AsyncMqttClient.h>

extern "C" {
  #include "freertos/FreeRTOS.h"
  #include "freertos/timers.h"
}

#define WIFI_SSID     "YOUR_WIFI"
#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD"

#define MQTT_HOST "broker.connectmq.com"
#define MQTT_PORT 1883

#define LED_PIN 2

static const char* DEVICE_ID = "async-4b81d2";   // torne-o único
String topicTelemetry = String("connectmq/demo/") + DEVICE_ID + "/telemetry";
String topicCommand   = String("connectmq/demo/") + DEVICE_ID + "/led/set";
String topicStatus    = String("connectmq/demo/") + DEVICE_ID + "/status";

AsyncMqttClient mqttClient;
TimerHandle_t mqttReconnectTimer;
TimerHandle_t wifiReconnectTimer;

unsigned long lastPublish = 0;

void connectToWifi() {
  Serial.println("Connecting to Wi-Fi...");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}

void connectToMqtt() {
  Serial.println("Connecting to MQTT...");
  mqttClient.connect();
}

void onWifiEvent(WiFiEvent_t event) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      Serial.print("Wi-Fi connected, IP ");
      Serial.println(WiFi.localIP());
      connectToMqtt();
      break;
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
      Serial.println("Wi-Fi lost");
      xTimerStop(mqttReconnectTimer, 0);   // sem MQTT enquanto o Wi-Fi estiver fora
      xTimerStart(wifiReconnectTimer, 0);
      break;
    default:
      break;
  }
}

void onMqttConnect(bool sessionPresent) {
  Serial.printf("MQTT connected, session present: %d\n", sessionPresent);
  mqttClient.subscribe(topicCommand.c_str(), 1);
  mqttClient.publish(topicStatus.c_str(), 1, true, "online");
}

void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
  Serial.printf("MQTT disconnected, reason %d\n", static_cast<int>(reason));
  if (WiFi.isConnected()) {
    xTimerStart(mqttReconnectTimer, 0);
  }
}

void onMqttSubscribe(uint16_t packetId, uint8_t qos) {
  Serial.printf("Subscribe ack, packetId %u, granted QoS %u\n", packetId, qos);
}

void onMqttPublish(uint16_t packetId) {
  Serial.printf("Publish ack (PUBACK), packetId %u\n", packetId);
}

void onMqttMessage(char* topic, char* payload,
                   AsyncMqttClientMessageProperties properties,
                   size_t len, size_t index, size_t total) {
  // o payload não termina em nulo e pode ser apenas uma parte de uma mensagem maior
  if (index != 0 || len != total) {
    Serial.println("Fragmented message ignored in this demo");
    return;
  }
  String msg;
  msg.reserve(len);
  for (size_t i = 0; i < len; i++) msg += payload[i];

  Serial.printf("Message on %s (QoS %u): %s\n", topic, properties.qos, msg.c_str());

  if (msg == "ON")  digitalWrite(LED_PIN, HIGH);
  if (msg == "OFF") digitalWrite(LED_PIN, LOW);
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);

  mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE,
                                    nullptr, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
  wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE,
                                    nullptr, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));

  WiFi.onEvent(onWifiEvent);

  mqttClient.onConnect(onMqttConnect);
  mqttClient.onDisconnect(onMqttDisconnect);
  mqttClient.onSubscribe(onMqttSubscribe);
  mqttClient.onPublish(onMqttPublish);
  mqttClient.onMessage(onMqttMessage);

  mqttClient.setServer(MQTT_HOST, MQTT_PORT);
  mqttClient.setClientId(DEVICE_ID);              // o ponteiro precisa continuar válido
  mqttClient.setKeepAlive(30);
  mqttClient.setWill(topicStatus.c_str(), 1, true, "offline");

  connectToWifi();
}

void loop() {
  // O código da sua aplicação roda livremente aqui; o MQTT é tratado em segundo plano.
  if (mqttClient.connected() && millis() - lastPublish > 10000) {
    lastPublish = millis();
    char payload[96];
    snprintf(payload, sizeof(payload), "{\"uptime\":%lu,\"rssi\":%d,\"heap\":%u}",
             millis() / 1000, WiFi.RSSI(), (unsigned)ESP.getFreeHeap());
    uint16_t id = mqttClient.publish(topicTelemetry.c_str(), 1, false, payload);
    Serial.printf("Queued publish, packetId %u\n", id);
  }
}

Acompanhe o tráfego e envie comandos a partir do seu computador:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-4b81d2 -t "connectmq/demo/async-4b81d2/#" -v -q 1
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-4b81d2 -t "connectmq/demo/async-4b81d2/led/set" -m ON -q 1

Como Funciona o Fluxo de Eventos

Entender a ordem dos eventos evita a maioria dos bugs:

  1. connectToWifi() inicia a conexão Wi-Fi e retorna na hora.
  2. Quando a estação obtém um endereço IP, onWifiEvent chama connectToMqtt().
  3. mqttClient.connect() retorna imediatamente. Depois, a tarefa do AsyncTCP recebe o CONNACK e a biblioteca chama onMqttConnect.
  4. Dentro de onMqttConnect, você faz as inscrições e publica o status "online". Este é o lugar certo, porque as inscrições em uma clean session precisam ser renovadas a cada reconexão.
  5. Se o broker ou a rede derrubar você, onMqttDisconnect agenda um timer de disparo único, que chama connectToMqtt() dois segundos depois.

publish() retorna um packet ID para mensagens QoS 1 e 2, 1 para uma mensagem QoS 0 colocada na fila e 0 quando a mensagem não pôde ser enviada. O onMqttPublish(packetId) correspondente é disparado quando o broker confirma a entrega, o que permite acompanhar se mensagens importantes realmente chegaram.

Armadilhas que Aparecem em Produção

Os Callbacks Rodam em Outra Tarefa

Seus callbacks são executados na tarefa do AsyncTCP, e não em loop(). Não chame delay() nem faça I/O lento ali; isso bloqueia todo o tráfego de rede e pode disparar o watchdog de tarefas. Copie os dados para uma variável ou fila e trate-os em loop(). Se loop() e um callback compartilham uma variável, proteja-a, por exemplo com uma fila do FreeRTOS ou com uma flag volatile nos casos simples.

Tempo de Vida das Strings

setClientId(), setCredentials(), setServer() com nome de host e setWill() armazenam ponteiros, não cópias. Passar o c_str() de uma String temporária faz com que lixo seja enviado na reconexão. Use constantes globais ou objetos String globais, como no exemplo.

Payloads Fragmentados

Mensagens grandes chegam em várias chamadas de onMessage, com index crescente e o mesmo total. Se você espera payloads de mais de algumas centenas de bytes, acumule as partes em um buffer de tamanho total e processe quando index + len == total.

Sem Reconexão Embutida

A biblioteca não se reconecta sozinha. O padrão com timers mostrado acima é a abordagem usual. Sempre pare o timer do MQTT quando o Wi-Fi cair; caso contrário, você vai disparar tentativas de conexão que não têm como dar certo.

Sessões Persistentes para Comandos Perdidos

Por padrão, o cliente se conecta com clean session, então os comandos enviados enquanto o dispositivo estava offline se perdem. Chame mqttClient.setCleanSession(false) antes de conectar e inscreva-se com QoS 1: o broker então mantém a sessão e enfileira as mensagens QoS 1 para o dispositivo até que ele volte. A sessão não dura para sempre; os brokers a expiram depois de um tempo (no servidor MQTT público, as sessões expiram em no máximo 30 minutos). Verifique sessionPresent em onMqttConnect para saber se o broker lembrou de você e refaça as inscrições quando não lembrou.

Os Limites do Broker Continuam Valendo

Assíncrono não significa ilimitado. O servidor MQTT público permite 10 mensagens por segundo por cliente, 10 inscrições por cliente e 64 KB por mensagem. Um loop apertado chamando publish() vai atingir esses limites rapidamente.

Escolhendo Entre Clientes Assíncronos e Bloqueantes

Use este checklist para decidir:

  1. Precisa de TLS na porta 8883? Escolha o PubSubClient com WiFiClientSecure ou o cliente seguro do espMqttClient. O AsyncMqttClient padrão não é uma boa opção.
  2. Precisa publicar com QoS 1 ou 2 a partir do dispositivo? AsyncMqttClient ou espMqttClient.
  3. Vai rodar um servidor web (ESPAsyncWebServer) no mesmo dispositivo? O AsyncMqttClient compartilha muito bem a pilha AsyncTCP.
  4. Também vai usar o ESP8266? O AsyncMqttClient funciona nele com o ESPAsyncTCP, embora a memória seja mais apertada. Veja o tutorial de MQTT com ESP8266.
  5. Quer o código mais simples para um projeto de iniciante? O tutorial do PubSubClient é um começo mais tranquilo.

Se a sua preocupação é garantia de entrega, leia níveis de QoS do MQTT explicados antes de trocar de biblioteca só por causa do QoS 2; muitas cargas de trabalho de sensores ficam melhor com QoS 0 ou 1. O tutorial IoT com ESP32, mais amplo, mostra como o MQTT se encaixa em um projeto completo de sensores.

Configuração de Produção com o ConnectMQ

Para implantações fora do seu laboratório, use no dispositivo um broker MQTT na nuvem dedicado, em vez do servidor MQTT público. No ConnectMQ, você cria um broker no portal e recebe um usuário, uma senha e um client ID. Com o AsyncMqttClient, você os define assim, lembrando que os client IDs ficam vinculados às credenciais emitidas:

mqttClient.setServer("broker.connectmq.com", 1883);
mqttClient.setClientId("YOUR_CLIENT_ID");
mqttClient.setCredentials("YOUR_USERNAME", "YOUR_PASSWORD");

Como as credenciais na porta 1883 trafegam sem criptografia, prefira uma biblioteca com suporte a TLS para dispositivos fora de uma rede confiável.

Comece a Usar o ConnectMQ

O ConnectMQ é um broker MQTT gerenciado com MQTT 3.1.1 e 5.0, TLS, WebSockets, um explorador de topics ao vivo e alertas por limite no portal, para você acompanhar as publicações do seu ESP32 em tempo real. Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito, e conecte seu primeiro dispositivo assíncrono em minutos.

Perguntas frequentes

O AsyncMqttClient funciona com TLS no ESP32?

Não com a biblioteca AsyncTCP padrão, que não tem compilação com SSL para o ESP32. Use o PubSubClient com WiFiClientSecure ou uma biblioteca assíncrona mantida, como o espMqttClient, quando precisar da porta 8883.

Por que preciso de timers do FreeRTOS para reconectar?

O AsyncMqttClient não tem reconexão automática. Timers de disparo único permitem tentar de novo após um pequeno intervalo sem bloquear loop() nem a tarefa de rede.

Posso chamar delay() dentro de onMqttMessage?

Não. Os callbacks rodam na tarefa do AsyncTCP, e bloquear ali trava todo o tráfego de rede. Guarde a mensagem e processe-a em loop().

O AsyncMqttClient ainda é mantido?

A biblioteca original não recebe desenvolvimento ativo há algum tempo, embora continue muito usada. Verifique a situação do repositório antes de iniciar um produto novo de longa duração e considere o espMqttClient como alternativa mantida.

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.