ESP32 e Arduino

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.

Terminal executando idf.py monitor ao lado de um ESP32 em um projeto de exemplo de MQTT com ESP-IDF
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

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:

  1. Tópico e dados não terminam em nulo. Use sempre topic_len e data_len, como faz o formato %.*s acima.
  2. 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 eventos MQTT_EVENT_DATA. Verifique event->current_data_offset e event->total_data_len e remonte a mensagem.
  3. 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

  1. Erros de compilação sobre .uri ou .host: você está misturando a sintaxe de configuração do ESP-IDF 4.x e 5.x. Use os campos aninhados mostrados aqui.
  2. Funções do MQTT 5 não definidas: CONFIG_MQTT_PROTOCOL_5 não está ativado. Apague o sdkconfig depois de adicionar o sdkconfig.defaults ou ative-o no idf.py menuconfig.
  3. MQTT_ERROR_TYPE_TCP_TRANSPORT logo 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).
  4. Conexão recusada com credenciais: confira o client ID exato do portal e se o campo de senha é .credentials.authentication.password.
  5. Payloads cortados: aumente .buffer.size ou remonte os eventos MQTT_EVENT_DATA em pedaços.
  6. 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.

Artigos relacionados

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.