Este exemplo de MQTT com ESP-IDF monta um cliente esp-mqtt funcional a partir de uma pasta vazia: Wi-Fi em modo station, um handler MQTT orientado a eventos, publicação e inscrição, propriedades de conexão do MQTT 5 e, por fim, TLS. No final você terá um firmware que conversa com o broker.connectmq.com, o servidor MQTT público (gratuito) do ConnectMQ, e uma visão clara de como o componente esp-mqtt é estruturado.
Por que usar ESP-IDF em vez de Arduino
Bibliotecas Arduino como o PubSubClient são rápidas para começar, mas suportam apenas MQTT 3.1.1 e são majoritariamente bloqueantes. O componente esp-mqtt do ESP-IDF é mantido pela Espressif, roda em sua própria task FreeRTOS, suporta MQTT 3.1, 3.1.1 e 5.0 e trata os transportes TCP, TLS, WebSocket e WebSocket seguro por meio de uma única URI. Reconexão, keepalive e gerenciamento da outbox já vêm prontos, e o seu código reage a eventos em vez de fazer polling.
A contrapartida é mais configuração: CMake, menuconfig e inicialização explícita do Wi-Fi. Este guia mantém essa configuração no mínimo possível. Ele mira o ESP-IDF 5.x, em que a struct de configuração usa campos aninhados como .broker.address.uri. Código escrito para o ESP-IDF 4.x usava campos planos e não compila sem alterações. A referência oficial da API é a documentação de MQTT do ESP-IDF.
Estrutura do projeto
Crie esta estrutura:
connectmq-idf/
CMakeLists.txt
sdkconfig.defaults
main/
CMakeLists.txt
main.c
O CMakeLists.txt de nível superior:
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(connectmq_idf)
main/CMakeLists.txt:
idf_component_register(SRCS "main.c" INCLUDE_DIRS ".")
O componente main depende automaticamente de todos os componentes do framework, então você não precisa listar mqtt, esp_wifi ou nvs_flash explicitamente. No ESP-IDF 5.x, o esp-mqtt vem com o framework. Se a sua versão do ESP-IDF o tiver movido para o registro de componentes, adicione-o com idf.py add-dependency "espressif/mqtt".
O sdkconfig.defaults ativa o suporte a MQTT 5, que vem desligado por padrão:
CONFIG_MQTT_PROTOCOL_5=y
O exemplo completo de MQTT com ESP-IDF
Aqui está o main/main.c completo. Ele conecta ao Wi-Fi, inicia o cliente MQTT contra o servidor MQTT público em broker.connectmq.com, se inscreve em um tópico de comando e publica um contador a cada cinco segundos.
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "mqtt_client.h"
#define WIFI_SSID "YOUR_WIFI"
#define WIFI_PASS "YOUR_WIFI_PASSWORD"
#define BROKER_URI "mqtt://broker.connectmq.com:1883"
#define TOPIC_PREFIX "connectmq/demo/c19d44"
static const char *TAG = "connectmq";
static EventGroupHandle_t wifi_events;
#define WIFI_CONNECTED_BIT BIT0
static esp_mqtt_client_handle_t client;
static bool mqtt_connected = false;
static void wifi_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
xEventGroupClearBits(wifi_events, WIFI_CONNECTED_BIT);
ESP_LOGW(TAG, "Wi-Fi lost, reconnecting");
esp_wifi_connect();
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *ev = (ip_event_got_ip_t *)data;
ESP_LOGI(TAG, "Got IP " IPSTR, IP2STR(&ev->ip_info.ip));
xEventGroupSetBits(wifi_events, WIFI_CONNECTED_BIT);
}
}
static void wifi_init(void)
{
wifi_events = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_handler, NULL));
wifi_config_t wc = { 0 };
strncpy((char *)wc.sta.ssid, WIFI_SSID, sizeof(wc.sta.ssid));
strncpy((char *)wc.sta.password, WIFI_PASS, sizeof(wc.sta.password));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc));
ESP_ERROR_CHECK(esp_wifi_start());
xEventGroupWaitBits(wifi_events, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
}
static void mqtt_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)data;
switch ((esp_mqtt_event_id_t)id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT connected");
mqtt_connected = true;
esp_mqtt_client_subscribe(client, TOPIC_PREFIX "/cmd", 1);
esp_mqtt_client_publish(client, TOPIC_PREFIX "/status", "online", 0, 1, 1);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "MQTT disconnected");
mqtt_connected = false;
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "Subscribed, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "Topic %.*s: %.*s",
event->topic_len, event->topic, event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
ESP_LOGE(TAG, "Transport error, errno=%d",
event->error_handle->esp_transport_sock_errno);
} else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
ESP_LOGE(TAG, "Connection refused, code=0x%x",
event->error_handle->connect_return_code);
}
break;
default:
break;
}
}
static void mqtt_start(void)
{
static char client_id[32];
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
snprintf(client_id, sizeof(client_id), "idf-%02x%02x%02x", mac[3], mac[4], mac[5]);
esp_mqtt_client_config_t cfg = {
.broker.address.uri = BROKER_URI,
.credentials.client_id = client_id,
.session.protocol_ver = MQTT_PROTOCOL_V_5,
.session.keepalive = 30,
.session.last_will = {
.topic = TOPIC_PREFIX "/status",
.msg = "offline",
.qos = 1,
.retain = 1,
},
};
client = esp_mqtt_client_init(&cfg);
esp_mqtt5_connection_property_config_t conn_props = {
.session_expiry_interval = 600, /* seconds */
};
esp_mqtt5_client_set_connect_property(client, &conn_props);
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_handler, NULL);
esp_mqtt_client_start(client);
}
void app_main(void)
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
wifi_init();
mqtt_start();
int counter = 0;
char payload[64];
while (true) {
if (mqtt_connected) {
int len = snprintf(payload, sizeof(payload), "{\"count\":%d}", counter++);
esp_mqtt_client_publish(client, TOPIC_PREFIX "/telemetry", payload, len, 0, 0);
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
Compile, grave e abra o monitor:
idf.py set-target esp32
idf.py build flash monitor
Depois acompanhe o tráfego a partir do seu computador:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-c19d44 -t 'connectmq/demo/c19d44/#' -v
mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-c19d44-pub -t connectmq/demo/c19d44/cmd -m hello
O log do ESP32 mostra o comando hello chegando por meio de MQTT_EVENT_DATA.
Como funciona o modelo de eventos do esp-mqtt
O cliente roda em sua própria task, criada por esp_mqtt_client_start(). Tudo o que ele quer comunicar chega como um evento no handler que você registrou:
| Evento | Quando dispara | Ação típica |
|---|---|---|
MQTT_EVENT_CONNECTED |
CONNACK recebido | Inscrever-se, publicar mensagem de nascimento |
MQTT_EVENT_DISCONNECTED |
Conexão perdida | Atualizar o estado; o cliente reconecta automaticamente |
MQTT_EVENT_SUBSCRIBED |
SUBACK recebido | Confirmar as inscrições |
MQTT_EVENT_PUBLISHED |
PUBACK/PUBCOMP para QoS 1/2 | Acompanhar a entrega |
MQTT_EVENT_DATA |
Mensagem recebida | Copiar e despachar o payload |
MQTT_EVENT_ERROR |
Erro de transporte ou de protocolo | Registrar o tipo e os códigos de erro |
Três detalhes costumam pegar as pessoas de surpresa:
- Tópico e dados não terminam em nulo. Use sempre
topic_lenedata_len, como faz o formato%.*sacima. - Mensagens grandes chegam em pedaços. Se um payload for maior que o buffer do cliente (
.buffer.size, 1024 bytes por padrão), você recebe vários eventosMQTT_EVENT_DATA. Verifiqueevent->current_data_offseteevent->total_data_lene remonte a mensagem. - Inscreva-se em
MQTT_EVENT_CONNECTED. Com clean session, as inscrições somem após uma reconexão. Fazê-las no handler de conexão as restabelece todas as vezes.
O handler roda na task MQTT, então mantenha-o curto. Trabalho pesado pertence a uma task separada alimentada por uma fila, como descrito no guia de ESP32 com FreeRTOS e MQTT.
Publish versus enqueue
esp_mqtt_client_publish() é bloqueante: envia o pacote a partir da task que chamou e, para QoS 1 e 2, o mantém na outbox até ser confirmado. Chamá-la de dentro do handler de eventos não tem problema. Se você publica a partir de uma task com restrições de tempo, esp_mqtt_client_enqueue(client, topic, data, len, qos, retain, true) coloca a mensagem na outbox e retorna imediatamente; a task MQTT a envia depois. Ambas retornam um ID de mensagem, ou um valor negativo em caso de falha, então verifique o retorno.
Usando recursos do MQTT 5
Definir .session.protocol_ver = MQTT_PROTOCOL_V_5 e ativar CONFIG_MQTT_PROTOCOL_5 muda o cliente para MQTT 5. O exemplo já define um intervalo de expiração de sessão por meio de esp_mqtt5_client_set_connect_property(). No servidor MQTT público em broker.connectmq.com, a expiração de sessão pode ser de até 30 minutos; os 10 minutos usados aqui cabem com folga.
As propriedades de publicação são definidas antes da chamada de publish:
esp_mqtt5_publish_property_config_t pub_props = {
.message_expiry_interval = 60,
.content_type = "application/json",
};
esp_mqtt5_client_set_publish_property(client, &pub_props);
esp_mqtt_client_publish(client, TOPIC_PREFIX "/telemetry", payload, len, 1, 0);
A expiração de mensagem impede que leituras desatualizadas sejam entregues a um assinante que volta muito tempo depois. Para um tour mais profundo por response topics, user properties e reason codes neste componente, veja esp-mqtt e MQTT 5 e, para o lado do protocolo, recursos do MQTT 5.
Escolhendo QoS, retain e keepalive
O exemplo usa três configurações de publicação diferentes de propósito. A telemetria de rotina sai com QoS 0 sem retain: se uma leitura se perder, a próxima chega cinco segundos depois, e nada fica esperando confirmação. A mensagem de status usa QoS 1 com retain, então é confirmada pelo broker e qualquer dashboard que se inscreva depois vê se o dispositivo está online. O Last Will espelha esse tópico de status com offline, que o broker publica se o dispositivo sumir sem uma desconexão limpa.
O keepalive de 30 segundos é um meio-termo sensato. O esp-mqtt envia pacotes PINGREQ por conta própria, então, ao contrário das bibliotecas Arduino, você não precisa chamar uma função de loop para manter a sessão viva. Um keepalive mais curto detecta dispositivos mortos mais rápido, mas custa mais tempo de rádio; um mais longo é mais gentil com nós a bateria. Se as garantias de entrega são novidade para você, níveis de QoS MQTT explicados mostra o que cada nível custa na rede.
Migrando para TLS na porta 8883
Para qualquer coisa além de uma demo, criptografe a conexão. O ESP-IDF traz um bundle de certificados com as CAs raiz mais comuns, incluindo a raiz Let's Encrypt que o ConnectMQ usa, então você não precisa colar um arquivo PEM. Mude o esquema da URI e anexe o bundle:
#include "esp_crt_bundle.h"
esp_mqtt_client_config_t cfg = {
.broker.address.uri = "mqtts://broker.connectmq.com:8883",
.broker.verification.crt_bundle_attach = esp_crt_bundle_attach,
.credentials.client_id = "YOUR_CLIENT_ID",
.credentials.username = "YOUR_USERNAME",
.credentials.authentication.password = "YOUR_PASSWORD",
.session.protocol_ver = MQTT_PROTOCOL_V_5,
};
Garanta que CONFIG_MBEDTLS_CERTIFICATE_BUNDLE esteja ativado (vem ativado por padrão). Use o client ID, o usuário e a senha gerados para o seu broker no portal; o client ID é vinculado a essas credenciais, então um ID inventado é rejeitado. A mesma abordagem por URI funciona para os transportes WebSocket: ws://broker.connectmq.com:8083/mqtt e wss://broker.connectmq.com:8084/mqtt, se CONFIG_MQTT_TRANSPORT_WEBSOCKET estiver ativado.
Checklist de solução de problemas
- Erros de compilação sobre
.uriou.host: você está misturando a sintaxe de configuração do ESP-IDF 4.x e 5.x. Use os campos aninhados mostrados aqui. - Funções do MQTT 5 não definidas:
CONFIG_MQTT_PROTOCOL_5não está ativado. Apague osdkconfigdepois de adicionar osdkconfig.defaultsou ative-o noidf.py menuconfig. MQTT_ERROR_TYPE_TCP_TRANSPORTlogo após iniciar: o Wi-Fi ainda não subiu, o DNS falhou ou o esquema e a porta não combinam (mqtts://precisa da 8883).- Conexão recusada com credenciais: confira o client ID exato do portal e se o campo de senha é
.credentials.authentication.password. - Payloads cortados: aumente
.buffer.sizeou remonte os eventosMQTT_EVENT_DATAem pedaços. - Stack overflow na task MQTT: aumente
.task.stack_size, principalmente com TLS e logs detalhados.
Se você está lidando com falhas de conexão em geral, o guia sobre o que fazer quando o ESP32 não conecta ao broker MQTT cobre verificações de rede que também se aplicam ao ESP-IDF.
Leve seu firmware esp-mqtt para produção com o ConnectMQ
Você acabou de rodar este exemplo de MQTT com ESP-IDF contra o servidor MQTT público, que serve para testes e protótipos, não para dados de produção. Quando estiver pronto para credenciais privadas, por exemplo para sensores de uma planta em Campinas, o ConnectMQ oferece um broker MQTT gerenciado com MQTT 5, TLS na 8883 e um certificado Let's Encrypt em que o bundle do ESP-IDF já confia. Crie um no plano Serverless gratuito em app.connectmq.com, sem cartão de crédito, e mude apenas a URI e as credenciais na sua configuração. Para a visão geral de projetos com dispositivos, veja o tutorial de IoT com ESP32.
Perguntas frequentes
O esp-mqtt reconecta automaticamente?
Sim. Após uma desconexão, o cliente tenta de novo sozinho usando .network.reconnect_timeout_ms. Refaça as inscrições em MQTT_EVENT_CONNECTED para que elas voltem junto com a conexão.
Qual versão do ESP-IDF este exemplo exige?
Ele mira o ESP-IDF 5.x, em que a configuração do cliente usa campos aninhados como .broker.address.uri. O ESP-IDF 4.x usava uma struct plana e precisa de pequenas alterações.
Posso usar MQTT 5 no plano gratuito do ConnectMQ?
Sim. Todo broker da plataforma suporta MQTT 3.1.1 e 5.0. O plano Serverless gratuito suporta QoS até 1, então use QoS 0 ou 1 nas suas publicações ali.
Por que o tópico recebido aparece com lixo no final?
Os ponteiros de tópico e dados em MQTT_EVENT_DATA não terminam em nulo. Imprima-os com %.*s e os campos de tamanho, ou copie-os para um buffer terminado.



