Tutorials

MQTT mit ESP-IDF (esp-mqtt) auf dem ESP32: Konfiguration, Events und TLS

Den esp-mqtt-Client von ESP-IDF auf dem ESP32 nutzen: esp_mqtt_client_config_t, Event-Handler, Publish und Subscribe, Last Will, TLS auf 8883 und Tests.

Aktualisiert · 9 Min. Lesezeit

MQTT mit ESP-IDF (esp-mqtt) auf dem ESP32: Konfiguration, Events und TLS

esp-mqtt ist der MQTT-Client, der mit Espressifs Framework ESP-IDF ausgeliefert wird. Er läuft in einem eigenen FreeRTOS-Task, verbindet sich automatisch neu, unterstützt MQTT 3.1.1 und MQTT 5 und spricht TCP, TLS, WebSocket und Secure WebSocket über eine einzige URI. Du konfigurierst ihn mit einem esp_mqtt_client_config_t-Struct, registrierst einen Event-Handler und rufst esp_mqtt_client_start() auf.

Dieses Tutorial baut Schritt für Schritt eine vollständige ESP32-Anwendung: zuerst unverschlüsseltes TCP, dann Events, Publishing, Subscriptions, ein Last Will und schließlich TLS auf Port 8883. In jeder Phase prüfst du das Ergebnis im Browser – so weißt du bei einem Fehler, dass das Gerät das Problem ist und nicht dein Test.

Projekt einrichten

Du brauchst eine installierte ESP-IDF 5.x (idf.py --version sollte im Terminal funktionieren). Nutze den Wi-Fi-Helfer der offiziellen Beispiele, damit sich das Tutorial auf MQTT konzentrieren kann:

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

Mach die Beispiel-Wi-Fi-Komponente in der CMakeLists.txt im Projektverzeichnis sichtbar (vor der Zeile include(...project.cmake)) und deklariere die Abhängigkeiten von main. Die Komponente mqtt gehört zu ESP-IDF, daher funktioniert #include "mqtt_client.h" unter 5.x ohne zusätzliche Downloads.

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)

Starte dann idf.py menuconfig, öffne Example Connection Configuration und trage SSID und Passwort deines WLANs ein.

Die Client-Konfiguration

Die minimale Konfiguration ist eine Broker-URI. Das Schema bestimmt den Transport:

URITransportPort
mqtt://broker.connectmq.com:1883TCP1883
mqtts://broker.connectmq.com:8883TLS8883
ws://broker.connectmq.com:8083/mqttWebSocket8083
wss://broker.connectmq.com:8084/mqttSecure WebSocket8084

Eine realistische Konfiguration setzt außerdem Client-ID, Zugangsdaten, Keep Alive und ein 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);
}

Jeder Client braucht eine eindeutige Client-ID: Zwei Geräte mit derselben ID werfen sich gegenseitig in einer Endlosschleife vom Broker. Leite sie in Produktion von der MAC-Adresse ab oder erzeuge beim Testen eine mit dem Client-ID-Generator. Halte Zugangsdaten aus der Versionskontrolle heraus, indem du sie als Optionen in Kconfig.projbuild (CONFIG_MQTT_USER, CONFIG_MQTT_PASS) anlegst und in menuconfig oder sdkconfig.defaults setzt.

Events verarbeiten

esp-mqtt meldet alles über den ESP-Event-Loop. Ein Handler empfängt alle Events; event_data zeigt auf ein 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 und data sind NICHT nullterminiert
        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;
    }
}

Drei Details bringen fast jeden ins Stolpern:

  • Abonniere in MQTT_EVENT_CONNECTED, nicht nach start(). Der Client verbindet sich womöglich oft neu; wenn du bei jedem Connect abonnierst, stellst du die Subscriptions auch dann wieder her, wenn der Broker die Session nicht behalten hat.
  • topic und data sind keine C-Strings. Nutze immer die _len-Felder. Große Nachrichten kommen in mehreren MQTT_EVENT_DATA-Stücken an; mit event->current_data_offset und event->total_data_len setzt du sie zusammen, oder du erhöhst den Puffer mit .buffer.size.
  • Der Handler läuft im MQTT-Task. Blockiere ihn nicht. Gib längere Arbeit über eine FreeRTOS-Queue an einen anderen Task ab.

Gestartet wird alles in 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());          // blockiert, bis das WLAN eine IP hat

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

Flashe und beobachte das Log mit idf.py flash monitor. Du solltest connected und danach subscribed sehen.

Telemetrie veröffentlichen

esp_mqtt_client_publish(client, topic, data, len, qos, retain) gibt eine Message-ID zurück (0 bei QoS 0, -1 bei Fehler). Mit len = 0 ruft die Bibliothek strlen() auf einem String-Payload auf. Bei QoS 1 und 2 schreibt der Aufruf aus dem aufrufenden Task und kann auf das Netzwerk warten; esp_mqtt_client_enqueue() nimmt dieselben Argumente plus ein store-Flag und überlässt das Senden dem MQTT-Task:

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));
    }
}
// in app_main, nach esp_mqtt_client_start():
// xTaskCreate(telemetry_task, "telemetry", 3072, NULL, 5, NULL);

Nachrichten, die eingereiht werden, während der Client offline ist, bleiben in der Outbox (begrenzt durch CONFIG_MQTT_OUTBOX_EXPIRED_TIMEOUT_MS und den freien Heap) und werden nach dem Reconnect gesendet. Wähle das QoS bewusst: Unter MQTT-QoS-Stufen steht, was jede Stufe kostet.

Im Browser prüfen

Öffne den Online-MQTT-Client, verbinde dich mit broker.connectmq.com (WebSocket 8084, Pfad /mqtt) und abonniere testmqtt/espidf/dev01/#. Du solltest sehen:

  1. testmqtt/espidf/dev01/status = online, als retained markiert.
  2. Alle fünf Sekunden eine Telemetrie-Nachricht.

Veröffentliche jetzt im Browser reboot auf testmqtt/espidf/dev01/cmd/system: Das ESP32-Log gibt Topic und Payload aus. Zieh zum Schluss das Stromkabel. Nach etwa 1,5 × Keep Alive (hier 45 Sekunden) veröffentlicht der Broker das Last Will und der Status springt auf offline. Dieser eine Test beweist, dass Verbindung, Publish, Subscribe und Will funktionieren. So funktioniert das Last Will erklärt das Timing.

Deinen ESP32 im Online-Client beobachten

TLS auf Port 8883 hinzufügen

Der Wechsel zu TLS erfordert zwei Änderungen: eine mqtts://-URI und eine Möglichkeit, das Serverzertifikat zu prüfen. Am einfachsten ist das ESP x509 Certificate Bundle, das die gängigen öffentlichen Root-CAs enthält und standardmäßig aktiviert ist (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",
};

Nutzt dein Broker eine private CA oder willst du eine bestimmte Root pinnen, bette stattdessen die PEM-Datei ein:

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,
};

TLS braucht die richtige Uhrzeit, um die Gültigkeit des Zertifikats zu prüfen, und während des Handshakes grob 30–40 KB zusätzlichen Heap; auf einem ausgelasteten ESP32 ist das die erste Stelle, an der du bei fehlschlagenden Verbindungen suchen solltest. Unser ESP32-TLS-Leitfaden behandelt Client-Zertifikate (.credentials.authentication.certificate und .key) für Mutual TLS.

Häufige esp-mqtt-Fehler

Log-ZeileUrsacheLösung
esp-tls: couldn't get hostname for :broker.connectmq.com:DNS fehlgeschlagen, meist ist das WLAN noch nicht verbundenMQTT erst starten, wenn eine IP vorhanden ist
mbedtls_ssl_handshake returned -0x2700Zertifikatsprüfung fehlgeschlagenBundle oder richtige CA nutzen; Systemzeit prüfen
mbedtls_ssl_setup returned -0x7F00Zu wenig Speicher für TLSHeap freigeben, andere Puffer verkleinern
Error transport connectPort blockiert oder falsche Schema/Port-Kombinationmqtts:// mit 8883, mqtt:// mit 1883 prüfen
MQTT_ERROR_TYPE_CONNECTION_REFUSED, Code 5Nicht autorisiertBenutzername, Passwort und ACLs prüfen

Die numerischen CONNACK-Codes und die MQTT-5-Reason-Codes findest du im Reason-Code-Nachschlagewerk. Für Geräte, die sich verbinden und alle paar Minuten wieder abfallen, lies WLAN- und MQTT-Abbrüche beim ESP32.

MQTT 5 aktivieren

Schalte CONFIG_MQTT_PROTOCOL_5 in menuconfig ein (Component config → ESP-MQTT Configurations) und setze .session.protocol_ver = MQTT_PROTOCOL_V_5. Die grundlegenden Publish- und Subscribe-Aufrufe bleiben gleich; MQTT-5-Extras wie User Properties, Message Expiry und Response Topics setzt du vor dem Publish mit Funktionen wie esp_mqtt5_client_set_publish_property(). Abgelehnte Verbindungen melden dann präzise Reason Codes statt der sechs Return Codes von MQTT 3.1.1, was das Debuggen der Authentifizierung deutlich erleichtert.

esp-mqtt liefert dir mit sehr wenig Code einen produktionsreifen Client: ein Config-Struct, ein Event-Handler und der Transport, den die URI festlegt. Bring ihn zuerst über unverschlüsseltes TCP zum Laufen, beweise Publish, Subscribe und Will im Online-Client, wechsle dann auf mqtts:// und behalte dieselben Tests als Regressionsprüfung vor jedem Firmware-Release.

Häufig gestellte Fragen

Brauche ich für MQTT in ESP-IDF eine zusätzliche Bibliothek?

Für die meisten Projekte nicht. Der esp-mqtt-Client ist Teil von ESP-IDF (Header mqtt_client.h). Einige neuere ESP-IDF-Versionen liefern ihn über die Component Registry als espressif/mqtt aus – prüfe also die Doku deiner Version, wenn der Header nicht gefunden wird.

Warum blockiert esp_mqtt_client_publish?

Bei QoS 1 und 2 schreibt die Funktion aus dem aufrufenden Task auf den Socket und kann auf das Netzwerk warten. Mit esp_mqtt_client_enqueue landet die Nachricht in der Outbox und der MQTT-Task verschickt sie – das ist in zeitkritischem Code sicherer.

Unterstützt esp-mqtt MQTT 5?

Ja. Aktiviere MQTT 5 in menuconfig (Component config → ESP-MQTT Configurations) und setze session.protocol_ver auf MQTT_PROTOCOL_V_5. Dann bekommst du Reason Codes, User Properties und die übrigen MQTT-5-Extras über eigene APIs.