Tutoriais

MQTT com ESP-IDF (esp-mqtt) no ESP32: configuração, eventos e TLS

Use o cliente esp-mqtt do ESP-IDF no ESP32: esp_mqtt_client_config_t, o handler de eventos, publish e subscribe, Last Will, TLS na 8883 e como testar tudo.

Atualizado · 9 min de leitura

MQTT com ESP-IDF (esp-mqtt) no ESP32: configuração, eventos e TLS

esp-mqtt é o cliente MQTT que vem com o framework ESP-IDF da Espressif. Ele roda na própria task do FreeRTOS, reconecta automaticamente, suporta MQTT 3.1.1 e MQTT 5 e fala TCP, TLS, WebSocket e WebSocket seguro com uma única URI. Você o configura com uma struct esp_mqtt_client_config_t, registra um handler de eventos e chama esp_mqtt_client_start().

Este tutorial monta passo a passo uma aplicação completa para ESP32: primeiro TCP puro, depois eventos, publicação, assinaturas, um Last Will e, por fim, TLS na porta 8883. Em cada etapa você verifica o resultado pelo navegador, para saber que o problema está no dispositivo, e não no seu teste, quando algo quebrar.

Preparando o projeto

Você precisa do ESP-IDF 5.x instalado (idf.py --version deve funcionar no terminal). Parta do helper de Wi-Fi usado pelos exemplos oficiais para que o tutorial foque em MQTT:

Create the project
idf.py create-project mqtt_demo
cd mqtt_demo
idf.py set-target esp32

Torne o componente de Wi-Fi de exemplo visível no CMakeLists.txt da raiz (antes da linha include(...project.cmake)) e declare as dependências do main. O componente mqtt faz parte do ESP-IDF, então #include "mqtt_client.h" funciona sem downloads extras nas versões 5.x.

CMakeLists.txt (project root)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
main/CMakeLists.txt
idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS "."
                       PRIV_REQUIRES mqtt esp_wifi esp_netif nvs_flash protocol_examples_common)

Depois rode idf.py menuconfig, abra Example Connection Configuration e informe o SSID e a senha do seu Wi-Fi.

A configuração do cliente

A configuração mínima é uma URI do broker. O esquema escolhe o transporte:

URITransportePorta
mqtt://broker.connectmq.com:1883TCP1883
mqtts://broker.connectmq.com:8883TLS8883
ws://broker.connectmq.com:8083/mqttWebSocket8083
wss://broker.connectmq.com:8084/mqttWebSocket seguro8084

Uma configuração realista também define client ID, credenciais, keep alive e um Last Will:

main/main.c — configuration
#include "esp_log.h"
#include "mqtt_client.h"

static const char *TAG = "mqtt_demo";
static esp_mqtt_client_handle_t client;

#define BASE_TOPIC "testmqtt/espidf/dev01"

static esp_mqtt_client_handle_t mqtt_create(void)
{
    const esp_mqtt_client_config_t cfg = {
        .broker.address.uri = "mqtt://broker.connectmq.com:1883",
        .credentials.client_id = "espidf-dev01",
        .credentials.username = "username",
        .credentials.authentication.password = "password",
        .session.keepalive = 30,
        .session.last_will = {
            .topic = BASE_TOPIC "/status",
            .msg = "offline",
            .qos = 1,
            .retain = 1,
        },
        .network.reconnect_timeout_ms = 5000,
    };
    return esp_mqtt_client_init(&cfg);
}

Todo cliente precisa de um client ID único: dois dispositivos com o mesmo ID derrubam um ao outro do broker em um loop infinito. Em produção, derive-o do endereço MAC, ou gere um com o gerador de client ID durante os testes. Mantenha as credenciais fora do controle de versão movendo-as para opções do Kconfig.projbuild (CONFIG_MQTT_USER, CONFIG_MQTT_PASS) e definindo-as no menuconfig ou no sdkconfig.defaults.

Tratando os eventos

O esp-mqtt informa tudo pelo event loop do ESP. Um único handler recebe todos os eventos; event_data aponta para um esp_mqtt_event_t:

main/main.c — event handler
static void mqtt_event_handler(void *args, esp_event_base_t base,
                               int32_t event_id, void *event_data)
{
    esp_mqtt_event_handle_t event = event_data;

    switch ((esp_mqtt_event_id_t)event_id) {
    case MQTT_EVENT_CONNECTED:
        ESP_LOGI(TAG, "connected");
        esp_mqtt_client_publish(client, BASE_TOPIC "/status", "online", 0, 1, 1);
        esp_mqtt_client_subscribe(client, BASE_TOPIC "/cmd/#", 1);
        break;
    case MQTT_EVENT_DISCONNECTED:
        ESP_LOGW(TAG, "disconnected, will retry");
        break;
    case MQTT_EVENT_SUBSCRIBED:
        ESP_LOGI(TAG, "subscribed, msg_id=%d", event->msg_id);
        break;
    case MQTT_EVENT_PUBLISHED:
        ESP_LOGI(TAG, "broker acked msg_id=%d", event->msg_id);
        break;
    case MQTT_EVENT_DATA:
        // topic e data NÃO terminam em nulo
        ESP_LOGI(TAG, "%.*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: esp-tls 0x%x, tls stack 0x%x, errno %d",
                     event->error_handle->esp_tls_last_esp_err,
                     event->error_handle->esp_tls_stack_err,
                     event->error_handle->esp_transport_sock_errno);
        } else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
            ESP_LOGE(TAG, "refused, CONNACK code %d",
                     event->error_handle->connect_return_code);
        }
        break;
    default:
        break;
    }
}

Três detalhes pegam quase todo mundo:

  • Assine em MQTT_EVENT_CONNECTED, não depois de start(). O cliente pode reconectar muitas vezes; assinar a cada conexão restaura suas assinaturas mesmo quando o broker não manteve a sessão.
  • topic e data não são strings C. Use sempre os campos _len. Mensagens grandes chegam em vários eventos MQTT_EVENT_DATA; event->current_data_offset e event->total_data_len permitem remontá-las, ou aumente o buffer com .buffer.size.
  • O handler roda na task MQTT. Não bloqueie dentro dele. Repasse trabalho demorado para outra task por meio de uma fila do FreeRTOS.

Inicie tudo a partir de app_main:

main/main.c — app_main
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "protocol_examples_common.h"

void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    ESP_ERROR_CHECK(example_connect());          // bloqueia até o Wi-Fi ter IP

    client = mqtt_create();
    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
    esp_mqtt_client_start(client);
}

Grave o firmware e acompanhe o log com idf.py flash monitor. Você deve ver connected seguido de subscribed.

Publicando telemetria

esp_mqtt_client_publish(client, topic, data, len, qos, retain) retorna um ID de mensagem (0 com QoS 0, -1 em caso de erro). Passe len = 0 para a biblioteca chamar strlen() em um payload de texto. Com QoS 1 e 2 a chamada escreve a partir da task que a invocou e pode esperar a rede; esp_mqtt_client_enqueue() recebe os mesmos argumentos mais um flag store e deixa o envio para a task MQTT:

Telemetry task
static void telemetry_task(void *arg)
{
    char payload[64];
    for (int i = 0;; i++) {
        int len = snprintf(payload, sizeof payload,
                           "{\"seq\":%d,\"heap\":%lu}", i,
                           (unsigned long)esp_get_free_heap_size());
        int id = esp_mqtt_client_enqueue(client, BASE_TOPIC "/telemetry",
                                         payload, len, 1, 0, true);
        if (id < 0) ESP_LOGW(TAG, "outbox full or not connected");
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}
// em app_main, depois de esp_mqtt_client_start():
// xTaskCreate(telemetry_task, "telemetry", 3072, NULL, 5, NULL);

Mensagens enfileiradas enquanto o cliente está offline ficam no outbox (limitadas por CONFIG_MQTT_OUTBOX_EXPIRED_TIMEOUT_MS e pelo heap disponível) e são enviadas após a reconexão. Escolha o QoS com cuidado: veja níveis de QoS no MQTT para entender o custo de cada nível.

Verificando pelo navegador

Abra o cliente MQTT online, conecte em broker.connectmq.com (WebSocket 8084, path /mqtt) e assine testmqtt/espidf/dev01/#. Você deve ver:

  1. testmqtt/espidf/dev01/status = online, marcado como retained.
  2. Uma mensagem de telemetria a cada cinco segundos.

Agora publique reboot em testmqtt/espidf/dev01/cmd/system pelo navegador: o log do ESP32 imprime o topic e o payload. Por fim, puxe o cabo de alimentação. Depois de cerca de 1,5 × keep alive (45 segundos aqui) o broker publica o Last Will e o status muda para offline. Esse único teste prova que conexão, publish, subscribe e will funcionam. Como funciona o Last Will explica os tempos.

Acompanhe seu ESP32 no cliente online

Adicionando TLS na porta 8883

Mudar para TLS exige duas alterações: uma URI mqtts:// e uma forma de verificar o certificado do servidor. O mais simples é o ESP x509 certificate bundle, que contém as CAs raiz públicas mais comuns e vem ativado por padrão (CONFIG_MBEDTLS_CERTIFICATE_BUNDLE):

TLS with the certificate bundle
#include "esp_crt_bundle.h"

const 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 = "espidf-dev01",
    .credentials.username = "username",
    .credentials.authentication.password = "password",
};

Se o seu broker usa uma CA privada ou você quer fixar uma raiz específica, embuta o arquivo PEM:

main/CMakeLists.txt
idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
                       PRIV_REQUIRES mqtt esp_wifi esp_netif nvs_flash protocol_examples_common
                       EMBED_TXTFILES "ca.pem")
Embedded CA certificate
extern const uint8_t ca_pem_start[] asm("_binary_ca_pem_start");

const esp_mqtt_client_config_t cfg = {
    .broker.address.uri = "mqtts://broker.connectmq.com:8883",
    .broker.verification.certificate = (const char *)ca_pem_start,
};

O TLS precisa do horário correto para checar a validade do certificado e de cerca de 30–40 KB de heap extra durante o handshake; em um ESP32 ocupado, é o primeiro lugar para olhar quando as conexões falham. Nosso guia de TLS no ESP32 cobre certificados de cliente (.credentials.authentication.certificate e .key) para TLS mútuo.

Erros comuns do esp-mqtt

Linha de logCausaCorreção
esp-tls: couldn't get hostname for :broker.connectmq.com:Falha de DNS, geralmente Wi-Fi ainda não conectadoInicie o MQTT só depois de ter IP
mbedtls_ssl_handshake returned -0x2700Falha na verificação do certificadoUse o bundle ou a CA correta; confira o horário do sistema
mbedtls_ssl_setup returned -0x7F00Falta de memória para o TLSLibere heap, reduza outros buffers
Error transport connectPorta bloqueada ou par esquema/porta erradoConfira mqtts:// com 8883 e mqtt:// com 1883
MQTT_ERROR_TYPE_CONNECTION_REFUSED, código 5Não autorizadoVerifique usuário, senha e ACLs

Os códigos numéricos de CONNACK e os reason codes do MQTT 5 estão no buscador de reason codes. Para dispositivos que conectam e caem a cada poucos minutos, leia desconexões de Wi-Fi e MQTT no ESP32.

Ativando o MQTT 5

Ative CONFIG_MQTT_PROTOCOL_5 no menuconfig (Component config → ESP-MQTT Configurations) e defina .session.protocol_ver = MQTT_PROTOCOL_V_5. As chamadas básicas de publish e subscribe continuam iguais; extras do MQTT 5 como user properties, message expiry e response topics são definidos com funções como esp_mqtt5_client_set_publish_property() antes do publish. Conexões recusadas passam a informar reason codes precisos em vez dos seis códigos de retorno do MQTT 3.1.1, o que facilita muito depurar a autenticação.

O esp-mqtt entrega um cliente pronto para produção com pouquíssimo código: uma struct de configuração, um handler de eventos e o transporte escolhido pela URI. Faça funcionar em TCP puro, comprove publish, subscribe e will pelo cliente online, depois mude para mqtts:// e mantenha os mesmos testes como verificação de regressão antes de cada release de firmware.

Perguntas frequentes

Preciso de uma biblioteca extra para MQTT no ESP-IDF?

Na maioria dos projetos, não. O cliente esp-mqtt vem com o ESP-IDF (header mqtt_client.h). Algumas versões mais novas do ESP-IDF o distribuem pelo component registry como espressif/mqtt, então confira a documentação da sua versão se o header não for encontrado.

Por que esp_mqtt_client_publish bloqueia?

Com QoS 1 e 2 ele escreve no socket a partir da task que o chamou e pode ficar esperando a rede. Use esp_mqtt_client_enqueue para colocar a mensagem no outbox e deixar a task MQTT enviá-la, o que é mais seguro em código com requisitos de tempo.

O esp-mqtt suporta MQTT 5?

Sim. Ative o MQTT 5 no menuconfig (Component config → ESP-MQTT Configurations) e defina session.protocol_ver como MQTT_PROTOCOL_V_5. Assim você tem reason codes, user properties e os demais extras do MQTT 5 por meio de APIs dedicadas.