ESP32 und Arduino

ESP-IDF MQTT Beispiel: Einen esp-mqtt-Client von Grund auf bauen

Bauen Sie ein ESP-IDF-Projekt aus einem leeren Ordner mit der Komponente esp-mqtt: WLAN-Station einrichten, MQTT-Event-Handler, Publish und Subscribe, MQTT-5-Properties und TLS mit dem Zertifikats-Bundle.

Terminal mit idf.py monitor neben einem ESP32 in einem ESP-IDF-MQTT-Beispielprojekt
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

Dieses ESP-IDF-MQTT-Beispiel baut aus einem leeren Ordner einen funktionierenden esp-mqtt-Client: eine WLAN-Station, einen ereignisgesteuerten MQTT-Handler, Publish und Subscribe, MQTT-5-Verbindungs-Properties und schließlich TLS. Am Ende haben Sie eine Firmware, die mit broker.connectmq.com spricht, und ein klares Bild davon, wie die Komponente esp-mqtt aufgebaut ist.

Warum ESP-IDF statt Arduino

Arduino-Bibliotheken wie PubSubClient sind schnell eingerichtet, unterstützen aber nur MQTT 3.1.1 und arbeiten überwiegend blockierend. Die ESP-IDF-Komponente esp-mqtt wird von Espressif gepflegt, läuft in einem eigenen FreeRTOS-Task, unterstützt MQTT 3.1, 3.1.1 und 5.0 und deckt TCP, TLS, WebSocket und Secure WebSocket über eine einzige URI ab. Wiederverbindung, Keepalive und Outbox-Verwaltung sind eingebaut, und Ihr Code reagiert auf Events, statt zu pollen. Deshalb ist ESP-IDF auch die übliche Basis, wenn aus einem Prototyp ein Seriengerät wird, etwa ein Sensorknoten oder Edge-Gateway für Maschinendaten in einer Industrie-4.0-Umgebung.

Der Preis ist mehr Einrichtungsaufwand: CMake, menuconfig und eine explizite WLAN-Initialisierung. Dieser Leitfaden hält diesen Aufwand so gering wie möglich. Er richtet sich an ESP-IDF 5.x, wo die Konfigurationsstruktur verschachtelte Felder wie .broker.address.uri verwendet. Code für ESP-IDF 4.x nutzte flache Felder und lässt sich nicht unverändert kompilieren. Die maßgebliche API-Referenz ist die ESP-IDF-MQTT-Dokumentation.

Projektstruktur

Legen Sie diese Struktur an:

connectmq-idf/
  CMakeLists.txt
  sdkconfig.defaults
  main/
    CMakeLists.txt
    main.c

Die CMakeLists.txt auf oberster Ebene:

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 ".")

Die Komponente main hängt automatisch von allen Framework-Komponenten ab, Sie müssen mqtt, esp_wifi oder nvs_flash also nicht explizit auflisten. In ESP-IDF 5.x wird esp-mqtt mit dem Framework ausgeliefert. Falls Ihre ESP-IDF-Version die Komponente in die Component Registry verschoben hat, fügen Sie sie mit idf.py add-dependency "espressif/mqtt" hinzu.

sdkconfig.defaults aktiviert die MQTT-5-Unterstützung, die standardmäßig ausgeschaltet ist:

CONFIG_MQTT_PROTOCOL_5=y

Das vollständige ESP-IDF-MQTT-Beispiel

Hier ist die komplette main/main.c. Sie verbindet sich mit dem WLAN, startet den MQTT-Client gegen den öffentlichen MQTT-Broker broker.connectmq.com, abonniert ein Befehls-Topic und veröffentlicht alle fünf Sekunden einen Zähler.

#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,   /* Sekunden */
    };
    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));
    }
}

Bauen, flashen und den Monitor öffnen:

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

Beobachten Sie dann den Datenverkehr von Ihrem Rechner aus:

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

Das ESP32-Log zeigt, wie der Befehl hello über MQTT_EVENT_DATA eintrifft.

Wie das Event-Modell von esp-mqtt funktioniert

Der Client läuft in einem eigenen Task, den esp_mqtt_client_start() erzeugt. Alles, was er Ihnen mitteilen will, kommt als Event an dem Handler an, den Sie registriert haben:

Event Wann es auslöst Typische Aktion
MQTT_EVENT_CONNECTED CONNACK empfangen Abonnieren, Birth-Nachricht veröffentlichen
MQTT_EVENT_DISCONNECTED Verbindung verloren Zustand aktualisieren; der Client verbindet sich automatisch neu
MQTT_EVENT_SUBSCRIBED SUBACK empfangen Abonnements bestätigen
MQTT_EVENT_PUBLISHED PUBACK/PUBCOMP bei QoS 1/2 Zustellung verfolgen
MQTT_EVENT_DATA Nachricht empfangen Payload kopieren und weiterreichen
MQTT_EVENT_ERROR Transport- oder Protokollfehler Fehlertyp und Codes protokollieren

Drei Details bringen Entwickler häufig ins Stolpern:

  1. Topic und Daten sind nicht nullterminiert. Verwenden Sie immer topic_len und data_len, wie es das obige Format %.*s tut.
  2. Große Nachrichten kommen in Stücken an. Ist ein Payload größer als der Puffer des Clients (.buffer.size, standardmäßig 1024 Byte), erhalten Sie mehrere MQTT_EVENT_DATA-Events. Prüfen Sie event->current_data_offset und event->total_data_len und setzen Sie die Teile wieder zusammen.
  3. In MQTT_EVENT_CONNECTED abonnieren. Mit Clean Session sind Abonnements nach einem Reconnect weg. Erledigen Sie sie im Connected-Handler, werden sie jedes Mal wiederhergestellt.

Der Handler läuft im MQTT-Task, halten Sie ihn also kurz. Schwere Arbeit gehört in einen separaten Task, der über eine Queue versorgt wird, wie im Leitfaden zu ESP32 FreeRTOS MQTT beschrieben.

Publish oder Enqueue

esp_mqtt_client_publish() blockiert: Es sendet das Paket aus dem aufrufenden Task und hält es bei QoS 1 und 2 in der Outbox, bis es bestätigt ist. Ein Aufruf aus dem Event-Handler heraus ist in Ordnung. Wenn Sie aus einem zeitkritischen Task veröffentlichen, legt esp_mqtt_client_enqueue(client, topic, data, len, qos, retain, true) die Nachricht in die Outbox und kehrt sofort zurück; der MQTT-Task sendet sie später. Beide geben eine Message ID oder bei einem Fehler einen negativen Wert zurück, prüfen Sie diesen also.

MQTT-5-Funktionen nutzen

Mit .session.protocol_ver = MQTT_PROTOCOL_V_5 und aktiviertem CONFIG_MQTT_PROTOCOL_5 wechselt der Client zu MQTT 5. Das Beispiel setzt bereits über esp_mqtt5_client_set_connect_property() ein Session Expiry Interval. Auf dem öffentlichen MQTT-Broker broker.connectmq.com darf die Session Expiry bis zu 30 Minuten betragen; die hier verwendeten 10 Minuten liegen bequem darunter.

Publish-Properties werden vor dem Publish-Aufruf gesetzt:

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

Message Expiry verhindert, dass veraltete Messwerte an einen Subscriber ausgeliefert werden, der viel später zurückkommt. Eine ausführlichere Tour durch Response Topics, User Properties und Reason Codes in dieser Komponente bietet esp-mqtt und MQTT 5, die Protokollseite erklärt MQTT-5-Funktionen.

QoS, Retain und Keepalive wählen

Das Beispiel nutzt absichtlich drei verschiedene Publish-Einstellungen. Routine-Telemetrie geht mit QoS 0 ohne Retain hinaus: Geht ein Messwert verloren, kommt der nächste fünf Sekunden später, und nichts wartet auf eine Bestätigung. Die Statusnachricht verwendet QoS 1 mit Retain, sodass der Broker sie bestätigt und jedes später abonnierende Dashboard sieht, ob das Gerät online ist. Der Last Will spiegelt dieses Status-Topic mit offline, das der Broker veröffentlicht, wenn das Gerät ohne sauberes Trennen verschwindet.

Das Keepalive von 30 Sekunden ist ein vernünftiger Mittelweg. esp-mqtt sendet PINGREQ-Pakete selbstständig, anders als bei Arduino-Bibliotheken müssen Sie also keine Loop-Funktion aufrufen, um die Session am Leben zu halten. Ein kürzeres Keepalive erkennt tote Geräte schneller, kostet aber mehr Funkzeit; ein längeres schont Batterieknoten. Wenn Ihnen die Zustellgarantien neu sind, erklärt MQTT-QoS-Stufen erklärt, was jede Stufe auf der Leitung kostet.

Auf TLS über Port 8883 umstellen

Für alles, was über eine Demo hinausgeht, verschlüsseln Sie die Verbindung. ESP-IDF liefert ein Zertifikats-Bundle mit gängigen Root-CAs aus, einschließlich des Let's-Encrypt-Roots, das ConnectMQ verwendet, sodass Sie keine PEM-Datei einfügen müssen. Ändern Sie das URI-Schema und binden Sie das Bundle an:

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

Stellen Sie sicher, dass CONFIG_MBEDTLS_CERTIFICATE_BUNDLE aktiviert ist (standardmäßig der Fall). Verwenden Sie die im Portal für Ihren Broker generierte Client ID sowie Benutzername und Passwort; die Client ID ist an diese Zugangsdaten gebunden, eine ausgedachte ID wird also abgelehnt. Derselbe URI-Ansatz funktioniert für WebSocket-Transporte: ws://broker.connectmq.com:8083/mqtt und wss://broker.connectmq.com:8084/mqtt, sofern CONFIG_MQTT_TRANSPORT_WEBSOCKET aktiviert ist.

Checkliste zur Fehlerbehebung

  1. Kompilierfehler zu .uri oder .host: Sie vermischen die Konfigurationssyntax von ESP-IDF 4.x und 5.x. Verwenden Sie die hier gezeigten verschachtelten Felder.
  2. MQTT-5-Funktionen undefiniert: CONFIG_MQTT_PROTOCOL_5 ist nicht gesetzt. Löschen Sie sdkconfig, nachdem Sie sdkconfig.defaults hinzugefügt haben, oder setzen Sie die Option in idf.py menuconfig.
  3. MQTT_ERROR_TYPE_TCP_TRANSPORT direkt nach dem Start: Das WLAN ist noch nicht verbunden, DNS ist gescheitert, oder Schema und Port passen nicht zusammen (mqtts:// braucht 8883).
  4. Verbindung mit Zugangsdaten abgelehnt: Prüfen Sie die exakte Client ID aus dem Portal und dass das Passwortfeld .credentials.authentication.password heißt.
  5. Abgeschnittene Payloads: Erhöhen Sie .buffer.size oder setzen Sie gestückelte MQTT_EVENT_DATA-Events zusammen.
  6. Stack Overflow im MQTT-Task: Erhöhen Sie .task.stack_size, insbesondere mit TLS und ausführlichem Logging.

Wenn Sie allgemein Verbindungsfehler untersuchen, behandelt der Leitfaden dazu, was zu tun ist, wenn sich ein ESP32 nicht mit einem MQTT Broker verbinden kann, Prüfungen auf Netzwerkebene, die auch für ESP-IDF gelten.

Bringen Sie Ihre esp-mqtt-Firmware mit ConnectMQ in die Produktion

Sie haben dieses ESP-IDF-MQTT-Beispiel gerade gegen den öffentlichen MQTT-Broker ausgeführt, der für Tests und Prototypen gedacht ist, nicht für produktive Daten. Wenn Sie bereit für private Zugangsdaten sind, bietet Ihnen ConnectMQ einen verwalteten MQTT-Broker mit MQTT 5, TLS auf 8883 und einem Let's-Encrypt-Zertifikat, dem das ESP-IDF-Bundle bereits vertraut. Erstellen Sie einen im kostenlosen Serverless-Tarif unter app.connectmq.com, ohne Kreditkarte, und ändern Sie in Ihrer Konfiguration nur URI und Zugangsdaten. Das Gesamtbild für Geräteprojekte zeigt das ESP32-IoT-Tutorial.

Häufig gestellte Fragen

Verbindet sich esp-mqtt automatisch neu?

Ja. Nach einem Verbindungsabbruch versucht es der Client selbstständig erneut, gesteuert über .network.reconnect_timeout_ms. Abonnieren Sie in MQTT_EVENT_CONNECTED neu, damit die Abonnements mit der Verbindung zurückkommen.

Welche ESP-IDF-Version braucht dieses Beispiel?

Es richtet sich an ESP-IDF 5.x, wo die Client-Konfiguration verschachtelte Felder wie .broker.address.uri verwendet. ESP-IDF 4.x nutzte eine flache Struktur und erfordert kleine Anpassungen.

Kann ich MQTT 5 mit dem kostenlosen ConnectMQ-Tarif nutzen?

Ja. Jeder Broker auf der Plattform unterstützt MQTT 3.1.1 und 5.0. Der kostenlose Serverless-Tarif unterstützt QoS bis 1, verwenden Sie dort in Ihren Publishes also QoS 0 oder 1.

Warum wird mein empfangenes Topic mit Datenmüll am Ende ausgegeben?

Die Topic- und Datenzeiger in MQTT_EVENT_DATA sind nicht nullterminiert. Geben Sie sie mit %.*s und den Längenfeldern aus oder kopieren Sie sie in einen terminierten Puffer.

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.