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:
idf.py create-project mqtt_demo
cd mqtt_demo
idf.py set-target esp32Mach 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.
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)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:
| URI | Transport | Port |
|---|---|---|
mqtt://broker.connectmq.com:1883 | TCP | 1883 |
mqtts://broker.connectmq.com:8883 | TLS | 8883 |
ws://broker.connectmq.com:8083/mqtt | WebSocket | 8083 |
wss://broker.connectmq.com:8084/mqtt | Secure WebSocket | 8084 |
Eine realistische Konfiguration setzt außerdem Client-ID, Zugangsdaten, Keep Alive und ein Last Will:
#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:
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 nachstart(). 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. topicunddatasind keine C-Strings. Nutze immer die_len-Felder. Große Nachrichten kommen in mehrerenMQTT_EVENT_DATA-Stücken an; mitevent->current_data_offsetundevent->total_data_lensetzt 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:
#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:
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:
testmqtt/espidf/dev01/status=online, als retained markiert.- 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):
#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:
idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
PRIV_REQUIRES mqtt esp_wifi esp_netif nvs_flash protocol_examples_common
EMBED_TXTFILES "ca.pem")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-Zeile | Ursache | Lösung |
|---|---|---|
esp-tls: couldn't get hostname for :broker.connectmq.com: | DNS fehlgeschlagen, meist ist das WLAN noch nicht verbunden | MQTT erst starten, wenn eine IP vorhanden ist |
mbedtls_ssl_handshake returned -0x2700 | Zertifikatsprüfung fehlgeschlagen | Bundle oder richtige CA nutzen; Systemzeit prüfen |
mbedtls_ssl_setup returned -0x7F00 | Zu wenig Speicher für TLS | Heap freigeben, andere Puffer verkleinern |
Error transport connect | Port blockiert oder falsche Schema/Port-Kombination | mqtts:// mit 8883, mqtt:// mit 1883 prüfen |
MQTT_ERROR_TYPE_CONNECTION_REFUSED, Code 5 | Nicht autorisiert | Benutzername, 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.

