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:
- Topic und Daten sind nicht nullterminiert. Verwenden Sie immer
topic_lenunddata_len, wie es das obige Format%.*stut. - 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 mehrereMQTT_EVENT_DATA-Events. Prüfen Sieevent->current_data_offsetundevent->total_data_lenund setzen Sie die Teile wieder zusammen. - In
MQTT_EVENT_CONNECTEDabonnieren. 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
- Kompilierfehler zu
.urioder.host: Sie vermischen die Konfigurationssyntax von ESP-IDF 4.x und 5.x. Verwenden Sie die hier gezeigten verschachtelten Felder. - MQTT-5-Funktionen undefiniert:
CONFIG_MQTT_PROTOCOL_5ist nicht gesetzt. Löschen Siesdkconfig, nachdem Siesdkconfig.defaultshinzugefügt haben, oder setzen Sie die Option inidf.py menuconfig. MQTT_ERROR_TYPE_TCP_TRANSPORTdirekt nach dem Start: Das WLAN ist noch nicht verbunden, DNS ist gescheitert, oder Schema und Port passen nicht zusammen (mqtts://braucht 8883).- Verbindung mit Zugangsdaten abgelehnt: Prüfen Sie die exakte Client ID aus dem Portal und dass das Passwortfeld
.credentials.authentication.passwordheißt. - Abgeschnittene Payloads: Erhöhen Sie
.buffer.sizeoder setzen Sie gestückelteMQTT_EVENT_DATA-Events zusammen. - 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.



