ESP32 und Arduino

MQTT-5-Funktionen von esp-mqtt auf dem ESP32 mit ESP-IDF nutzen

Ein praxisnahes ESP-IDF-Projekt, das MQTT 5.0 in esp-mqtt aktiviert und User Properties, Message Expiry, Topic Aliases, Session Expiry und Response Topics mit einem echten MQTT-5-Broker einsetzt.

ESP32-Board, das MQTT-5-Pakete mit User Properties und Expiry-Headern sendet, als Illustration für esp-mqtt MQTT 5 in ESP-IDF
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Mit der MQTT-5-Unterstützung von esp-mqtt in ESP-IDF kann ein ESP32 Nachrichten Metadaten anhängen, Ablaufzeiten setzen, Topics mit Aliases verkürzen und Request/Response-Abläufe aufbauen, ohne eigene Konventionen erfinden zu müssen. In diesem Tutorial aktivieren Sie MQTT 5.0 in einem ESP-IDF-Projekt, verbinden sich mit broker.connectmq.com und nutzen jede Funktion in funktionierendem C-Code, den Sie noch heute flashen können.

Warum MQTT 5 auf einem Mikrocontroller

MQTT 3.1.1 ist auf einem ESP32 bestens nutzbar, und die meisten Arduino-Bibliotheken hören dort auf. MQTT 5.0 ergänzt Funktionen, die echte Probleme auf Geräten lösen:

Funktion Gelöstes Problem auf dem Gerät
User Properties Firmware-Version, Einheiten oder Schema-Version senden, ohne die JSON-Payload aufzublähen
Message Expiry Interval Verhindern, dass veraltete Befehle oder Messwerte Stunden später zugestellt werden
Topic Alias Einen langen Topic-String bei wiederholten Publishes durch eine 2-Byte-Zahl ersetzen
Session Expiry Interval Abonnements über kurze WLAN-Aussetzer hinweg behalten, ohne sie ewig zu speichern
Response Topic und Correlation Data Standardisiertes Request/Response, damit ein Backend ein Gerät aufrufen und die Antwort zuordnen kann
Reason Codes Genau erfahren, warum ein Connect, Publish oder Subscribe abgelehnt wurde

esp-mqtt, der mit ESP-IDF gebündelte MQTT-Client, implementiert all das hinter einer Option zur Kompilierzeit. Sie benötigen außerdem einen Broker, der MQTT 5.0 spricht. ConnectMQ unterstützt 3.1.1 und 5.0 auf jedem Listener, sodass derselbe Endpunkt für alte und neue Firmware funktioniert. Den Protokollhintergrund liefert MQTT-5-Funktionen erklärt mit einer ausführlichen Beschreibung jeder Property.

Voraussetzungen

  • Installiertes ESP-IDF 5.x, bei dem idf.py in Ihrem Terminal funktioniert.
  • Ein beliebiges Board der ESP32-Familie (ESP32, S3, C3, C6).
  • Mosquitto-2.x-Clients auf Ihrem Rechner; mosquitto_sub kann MQTT-5-Properties ausgeben.
  • Vertrautheit mit einem einfachen ESP-IDF-MQTT-Projekt. Falls Sie noch keines gebaut haben, beginnen Sie mit dem ESP-IDF-MQTT-Beispiel und kommen Sie dann zurück.

Projekt einrichten und MQTT 5.0 aktivieren

Legen Sie einen Projektordner mit folgendem Aufbau an:

mqtt5_connectmq/
  CMakeLists.txt
  sdkconfig.defaults
  main/
    CMakeLists.txt
    main.c

Die oberste CMakeLists.txt, die den WLAN-Helfer aus den offiziellen Beispielen einbindet:

cmake_minimum_required(VERSION 3.16)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(mqtt5_connectmq)

main/CMakeLists.txt:

idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS "."
                       PRIV_REQUIRES mqtt nvs_flash esp_netif esp_event protocol_examples_common)

sdkconfig.defaults schaltet die MQTT-5-Codepfade in esp-mqtt ein. Ohne diese Option werden die Funktionen esp_mqtt5_* nicht einkompiliert:

CONFIG_MQTT_PROTOCOL_5=y

Tragen Sie anschließend Ihre WLAN-Zugangsdaten mit idf.py menuconfig unter Example Connection Configuration ein. Falls Ihre ESP-IDF-Version die Komponente mqtt nicht mehr im Quellbaum mitliefert, fügen Sie sie mit idf.py add-dependency espressif/mqtt hinzu.

Der esp-mqtt-MQTT-5-Beispielcode

Ersetzen Sie den zufälligen Teil von PREFIX und die Client ID durch eigene Werte.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_log.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "esp_random.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "protocol_examples_common.h"
#include "mqtt_client.h"

#define PREFIX     "connectmq/demo/idf5m7"
#define CLIENT_ID  "idf5-demo-m7q2"

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

/* Properties, die einmal im CONNECT gesendet werden */
static esp_mqtt5_connection_property_config_t connect_property = {
    .session_expiry_interval = 600,      /* Session 10 min nach Trennung behalten */
    .maximum_packet_size = 4096,
    .receive_maximum = 16,
    .topic_alias_maximum = 4,            /* Aliases, die wir vom Broker akzeptieren */
    .request_resp_info = true,
    .request_problem_info = true,
    .will_delay_interval = 15,           /* 15 s warten, bevor der Will veröffentlicht wird */
    .payload_format_indicator = true,
    .message_expiry_interval = 3600,
    .content_type = "text/plain",
};

static esp_mqtt5_user_property_item_t connect_user_props[] = {
    {"model", "esp32-devkit"},
    {"fw", "1.4.2"},
};

static esp_mqtt5_user_property_item_t telemetry_user_props[] = {
    {"schema", "telemetry-v2"},
    {"unit", "celsius"},
};

static void print_user_properties(mqtt5_user_property_handle_t props)
{
    if (!props) return;
    uint8_t count = esp_mqtt5_client_get_user_property_count(props);
    if (!count) return;
    esp_mqtt5_user_property_item_t *items = malloc(count * sizeof(*items));
    if (items && esp_mqtt5_client_get_user_property(props, items, &count) == ESP_OK) {
        for (int i = 0; i < count; i++) {
            ESP_LOGI(TAG, "  user property %s = %s", items[i].key, items[i].value);
            free((char *)items[i].key);
            free((char *)items[i].value);
        }
    }
    free(items);
}

static void publish_telemetry(float temperature, int seq)
{
    char payload[96];
    int len = snprintf(payload, sizeof(payload),
                       "{\"seq\":%d,\"temperature\":%.2f}", seq, temperature);

    esp_mqtt5_publish_property_config_t pub_prop = {
        .payload_format_indicator = 1,    /* UTF-8-Payload */
        .message_expiry_interval = 60,    /* nach einer Minute nutzlos */
        .topic_alias = 1,                 /* dieses Topic auf Alias 1 abbilden */
        .content_type = "application/json",
    };
    esp_mqtt5_client_set_user_property(&pub_prop.user_property, telemetry_user_props,
        sizeof(telemetry_user_props) / sizeof(telemetry_user_props[0]));
    esp_mqtt5_client_set_publish_property(client, &pub_prop);

    int msg_id = esp_mqtt_client_publish(client, PREFIX "/telemetry", payload, len, 1, 0);
    ESP_LOGI(TAG, "published seq %d, msg_id %d", seq, msg_id);

    esp_mqtt5_client_delete_user_property(pub_prop.user_property);
}

static void reply_to_request(esp_mqtt_event_handle_t event)
{
    char response_topic[128];
    int n = event->property->response_topic_len;
    if (n <= 0 || n >= (int)sizeof(response_topic)) return;
    memcpy(response_topic, event->property->response_topic, n);
    response_topic[n] = '\0';

    esp_mqtt5_publish_property_config_t pub_prop = {
        .correlation_data = event->property->correlation_data,
        .correlation_data_len = event->property->correlation_data_len,
        .message_expiry_interval = 30,
    };
    esp_mqtt5_client_set_publish_property(client, &pub_prop);

    char body[96];
    int len = snprintf(body, sizeof(body), "{\"ok\":true,\"freeHeap\":%lu}",
                       (unsigned long)esp_get_free_heap_size());
    esp_mqtt_client_publish(client, response_topic, body, len, 1, 0);
    ESP_LOGI(TAG, "replied on %s", response_topic);
}

static void mqtt5_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, session present: %d", event->session_present);
        esp_mqtt_client_publish(client, PREFIX "/status", "online", 0, 1, 1);

        esp_mqtt5_subscribe_property_config_t sub_prop = {
            .subscribe_id = 7,
            .no_local_flag = true,        /* eigene Publishes nicht zurückspiegeln */
        };
        esp_mqtt5_client_set_subscribe_property(client, &sub_prop);
        esp_mqtt_client_subscribe(client, PREFIX "/request", 1);
        break;
    }
    case MQTT_EVENT_DATA:
        ESP_LOGI(TAG, "data on %.*s: %.*s", event->topic_len, event->topic,
                 event->data_len, event->data);
        print_user_properties(event->property->user_property);
        if (event->property->response_topic_len > 0) {
            reply_to_request(event);
        }
        break;
    case MQTT_EVENT_DISCONNECTED:
        ESP_LOGW(TAG, "disconnected");
        print_user_properties(event->property->user_property);
        break;
    case MQTT_EVENT_ERROR:
        if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
            ESP_LOGE(TAG, "connection refused, reason code 0x%02x",
                     event->error_handle->connect_return_code);
        }
        break;
    default:
        break;
    }
}

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());

    esp_mqtt_client_config_t cfg = {
        .broker.address.uri = "mqtt://broker.connectmq.com:1883",
        .session.protocol_ver = MQTT_PROTOCOL_V_5,
        .credentials.client_id = CLIENT_ID,
        .session.last_will = {
            .topic = PREFIX "/status",
            .msg = "offline",
            .msg_len = 7,
            .qos = 1,
            .retain = 1,
        },
    };

    client = esp_mqtt_client_init(&cfg);

    esp_mqtt5_client_set_user_property(&connect_property.user_property, connect_user_props,
        sizeof(connect_user_props) / sizeof(connect_user_props[0]));
    esp_mqtt5_client_set_connect_property(client, &connect_property);
    esp_mqtt5_client_delete_user_property(connect_property.user_property);
    connect_property.user_property = NULL;

    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt5_event_handler, NULL);
    esp_mqtt_client_start(client);

    for (int seq = 0;; seq++) {
        vTaskDelay(pdMS_TO_TICKS(5000));
        float temperature = 20.0f + (esp_random() % 500) / 100.0f;
        publish_telemetry(temperature, seq);
    }
}

Bauen, flashen und überwachen:

idf.py set-target esp32
idf.py build flash monitor

Die Properties beim Eintreffen beobachten

Mosquitto 2.x gibt MQTT-5-Properties aus, wenn Sie mit Protokollversion 5 und ausführlicher Ausgabe abonnieren:

mosquitto_sub -h broker.connectmq.com -p 1883 -V mqttv5 -i viewer-idf5m7 \
  -t "connectmq/demo/idf5m7/#" -F "%t %P %p"

Das Format-Token %P gibt User Properties aus, sodass jede Telemetriezeile neben dem JSON schema:telemetry-v2 unit:celsius zeigt. Testen Sie nun Request/Response. Warten Sie in einem Terminal auf die Antwort und senden Sie in einem anderen eine Anfrage mit Response Topic und Correlation Data:

mosquitto_sub -h broker.connectmq.com -p 1883 -V mqttv5 -i reply-idf5m7 \
  -t "connectmq/demo/idf5m7/reply/laptop" -v

mosquitto_pub -h broker.connectmq.com -p 1883 -V mqttv5 -i req-idf5m7 \
  -t "connectmq/demo/idf5m7/request" -m '{"cmd":"health"}' \
  -D publish response-topic "connectmq/demo/idf5m7/reply/laptop" \
  -D publish correlation-data "req-001"

Der ESP32 protokolliert die Anfrage, kopiert die Correlation Data und veröffentlicht die Antwort auf dem Topic, das der Anfragende gewählt hat. Nach demselben Muster ruft ein Backend-Dienst Tausende Geräte auf, ohne Antwort-Topics fest einzuprogrammieren. Der öffentliche MQTT-Broker ist geteilt und für alle lesbar, nutzen Sie ihn also nur für Experimente; die Seite zum kostenlosen öffentlichen MQTT-Broker erklärt seine Grenzen, etwa die auf 30 Minuten begrenzte Session Expiry.

Wie sich die einzelnen Funktionen verhalten

User Properties

User Properties sind Schlüssel-Wert-Paare aus Strings im Paket-Header. Sie eignen sich ideal für Metadaten, nach denen Konsumenten filtern können, ohne die Payload zu parsen. Halten Sie sie kurz: Sie werden mit jeder Nachricht gesendet und zählen zur Paketgröße.

Message Expiry Interval

Eine Telemetrie-Nachricht mit 60 Sekunden Ablaufzeit, die für einen Offline-Subscriber in der Warteschlange liegt, verwirft der Broker, sobald sie älter als eine Minute ist. Bei der Zustellung wird die verbleibende Lebensdauer mitgegeben. Verwenden Sie kurze Ablaufzeiten für Befehle wie „Ventil öffnen“ oder „Förderband stoppen“, die nie verspätet ausgeführt werden dürfen.

Topic Alias

Der Client sendet das Topic einmal mit Alias 1 und kann sich danach per Nummer darauf beziehen. esp-mqtt führt dafür eine eigene Alias-Tabelle. Das CONNACK des Brokers gibt an, wie viele Aliases er akzeptiert; überschreiten Sie diese Zahl nie. Die Einsparung zählt am meisten bei langen Topics im ISA-95-Stil auf langsamen oder volumenbasiert abgerechneten Verbindungen. Solche Topics entstehen typischerweise in einem Unified Namespace (UNS), in dem die ISA-95-Ebenen Enterprise, Site, Area, Line und Cell die Topic-Struktur bilden, etwa acme/werk-stuttgart/montage/linie-1/zelle-3/temperatur; hier spart ein Alias bei jedem Publish Dutzende Bytes. Mehr zum Konzept erklärt Sparkplug B und der Unified Namespace.

Session Expiry und Will Delay

Mit session_expiry_interval = 600 behält der Broker Ihre Abonnements und wartende QoS-1-Nachrichten nach einer Trennung zehn Minuten lang, und session_present im CONNECTED-Event verrät Ihnen, ob er das getan hat. Der Will Delay von 15 Sekunden bewirkt, dass ein kurzer WLAN-Aussetzer mit anschließendem Reconnect nicht auf jedem Dashboard kurz einen offline-Status aufblitzen lässt. Die Anleitung zu ESP32 MQTT Last Will vergleicht das mit dem Verhalten von MQTT 3.1.1.

Response Topic und Correlation Data

Der Anfragende bestimmt, wohin die Antwort geht, und hängt einen undurchsichtigen Korrelationswert an; das Gerät spiegelt beides einfach zurück. So bleibt die Firmware generisch: Sie muss nie wissen, welcher Dienst oder Benutzer gefragt hat. Geben Sie jedem Anfragenden ein eigenes Antwort-Topic und halten Sie die Correlation Data kurz, etwa einen Zähler oder eine UUID, denn das Gerät kopiert sie in die Antwort.

Reason Codes

MQTT 5 ersetzt die wenigen Return Codes von 3.1.1 durch eine detaillierte Liste, die auch Publish, Subscribe und Disconnect abdeckt. Protokollieren Sie sie. Ein Gerät, das „0x87 not authorized“ oder „0x97 quota exceeded“ in seinen Logs meldet, erspart Ihnen Stunden im Vergleich zu einer stummen Reconnect-Schleife und lässt Sie Konfigurationsfehler von Netzwerkproblemen unterscheiden.

Checkliste zur Fehlerbehebung

  1. Linker-Fehler bei esp_mqtt5_*-Funktionen. CONFIG_MQTT_PROTOCOL_5 ist nicht gesetzt. Löschen Sie sdkconfig, behalten Sie sdkconfig.defaults und bauen Sie neu.
  2. Verbindung mit Reason Code 0x85 abgelehnt. Ungültiger Client Identifier. Verwenden Sie eine nicht leere ID und auf einem Produktions-Broker genau die Client ID, die mit Ihren Zugangsdaten ausgegeben wurde.
  3. Reason Code 0x97 nach vielen Publishes. Kontingent überschritten. Drosseln Sie das Tempo: Der öffentliche MQTT-Broker erlaubt 10 Nachrichten pro Sekunde und Client.
  4. Topic-Alias-Fehler und Verbindungsabbrüche. Sie haben das Topic Alias Maximum des Brokers überschritten. Verwenden Sie weniger Aliases oder setzen Sie topic_alias auf 0.
  5. Zeichensalat bei der Ausgabe von Topics. event->topic und event->data sind nicht nullterminiert; geben Sie sie immer mit %.*s und den Längenfeldern aus.

Umstieg auf TLS mit eigenen Zugangsdaten

Für die Produktion ändern Sie die URI auf mqtts://broker.connectmq.com:8883, ergänzen .broker.verification.crt_bundle_attach = esp_crt_bundle_attach (mit Include von esp_crt_bundle.h) und setzen .credentials.username = "YOUR_USERNAME", .credentials.authentication.password = "YOUR_PASSWORD" sowie .credentials.client_id = "YOUR_CLIENT_ID". Das Zertifikats-Bundle enthält bereits die Root von Let's Encrypt, eine PEM-Datei ist also nicht nötig. Die vollständige Optionsliste finden Sie in der ESP-IDF-MQTT-Dokumentation.

Betreiben Sie MQTT-5-Geräte mit ConnectMQ

Properties helfen nur, wenn der Broker sie durchgängig berücksichtigt. ConnectMQ ist ein verwalteter MQTT-Broker mit Session Expiry, Message Expiry, Topic Aliases, Response Topics und klaren Reason Codes aus MQTT 5.0 sowie Shared Subscriptions im kostenpflichtigen Serverless-Plan zum Skalieren von Backend-Konsumenten. Erstellen Sie einen kostenlosen Serverless-Broker unter app.connectmq.com ohne Kreditkarte und richten Sie dieses Projekt darauf aus. Weitere Geräterezepte bündelt das ESP32-IoT-Tutorial in der vollständigen Reihe.

Häufig gestellte Fragen

Unterstützt esp-mqtt MQTT 5 auf jedem ESP32-Chip?

Ja. Die MQTT-5-Unterstützung ist Teil der Komponente esp-mqtt und nicht des Chips. Sie funktioniert daher auf ESP32, S2, S3, C3, C6 und anderen von Ihrer ESP-IDF-Version unterstützten Targets, sobald CONFIG_MQTT_PROTOCOL_5 aktiviert ist.

Kann ich MQTT-5- und MQTT-3.1.1-Clients am selben Broker mischen?

Ja. Ein Broker, der beide Versionen unterstützt, stellt Nachrichten zwischen ihnen zu. MQTT-3.1.1-Subscriber sehen die MQTT-5-Properties einfach nicht.

Muss ich Properties vor jedem Publish setzen?

Publish-Properties gelten für den nächsten Publish-Aufruf, setzen Sie sie also jedes Mal, wenn Sie sie benötigen. Connect-Properties werden einmal vor dem Start des Clients gesetzt.

Gibt es eine Arduino-Bibliothek mit MQTT-5-Unterstützung?

Die meisten verbreiteten Arduino-MQTT-Bibliotheken implementieren nur MQTT 3.1.1. Wenn Sie MQTT 5 auf einem ESP32 benötigen, ist ESP-IDF mit esp-mqtt die vollständigste Option, und Sie können es auch aus Projekten mit Arduino als Komponente aufrufen.

Verwandte Artikel

ESP32 NTP MQTT Zeitstempel: genaue Uhrzeit in jeder Nachricht

Synchronisieren Sie die ESP32-Uhr per NTP, formatieren Sie Zeitstempel in ISO 8601 und Epoch-Millisekunden und hängen Sie sie an MQTT-Payloads, damit Messwerte ihren echten Erfassungszeitpunkt behalten, auch wenn sie verspätet veröffentlicht werden.

8 Min. Lesezeit

ESP32 IoT Tutorial: Vom ersten Sketch zum Cloud-Sensor

Vom frisch ausgepackten ESP32 zum sicheren, mit der Cloud verbundenen Temperatursensor: Einrichtung der Arduino IDE, WLAN, ein DHT22, MQTT Publish und Subscribe, LED-Fernsteuerung und TLS auf Port 8883.

12 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.