Der ESP32 ist der Mikrocontroller schlechthin für MQTT-Projekte: eingebautes WLAN, genug RAM für TLS und erstklassige Arduino-Unterstützung. In diesem Tutorial baust du mit der Bibliothek PubSubClient einen vollständigen Sketch, der Sensorwerte als JSON publisht, auf einem Command-Topic lauscht, sich selbstständig neu verbindet und – im letzten Schritt – auf TLS umstellt.
Was du brauchst
- Ein ESP32-Dev-Board und ein USB-Kabel.
- Arduino IDE 2.x (oder PlatformIO) mit dem Board-Paket esp32 by Espressif Systems, installiert über den Boardverwalter.
- Die Bibliothek PubSubClient von Nick O'Leary, installiert über den Bibliotheksverwalter.
- Einen Broker. Zum Testen nutzen wir den öffentlichen HiveMQ-Broker auf Port
1883– okay für eine Demo, aber jeder kann die Topics mitlesen.
Topic-Struktur
Eine saubere Topic-Struktur macht Geräte leicht verwaltbar. Jedes Board bekommt ein eigenes Präfix auf Basis einer eindeutigen Geräte-ID:
| Topic | Richtung | Zweck |
|---|---|---|
testmqtt/esp32/<id>/telemetry | ESP32 zum Broker | Sensor-JSON alle 10 Sekunden |
testmqtt/esp32/<id>/status | ESP32 zum Broker | online / offline (retained, Last Will) |
testmqtt/esp32/<id>/cmd | Broker zum ESP32 | Befehle wie led:on |
Namenskonventionen findest du unter MQTT-Topics und Wildcards.
Der vollständige Sketch
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "your-ssid";
const char* WIFI_PASSWORD = "your-wifi-password";
const char* MQTT_HOST = "broker.hivemq.com";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = nullptr; // set for brokers that require auth
const char* MQTT_PASS = nullptr;
const int LED_PIN = 2; // on-board LED on many dev boards
WiFiClient net;
PubSubClient mqtt(net);
char deviceId[24];
char topicTelemetry[64];
char topicStatus[64];
char topicCmd[64];
unsigned long lastReconnectAttempt = 0;
unsigned long lastPublish = 0;
void onMessage(char* topic, byte* payload, unsigned int length) {
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
Serial.printf("Message on %s: %s\n", topic, msg.c_str());
if (msg == "led:on") digitalWrite(LED_PIN, HIGH);
if (msg == "led:off") digitalWrite(LED_PIN, LOW);
}
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nWiFi connected, IP %s\n", WiFi.localIP().toString().c_str());
}
bool connectMqtt() {
// Last Will: broker publishes "offline" (retained) if we drop unexpectedly
bool ok = mqtt.connect(deviceId, MQTT_USER, MQTT_PASS,
topicStatus, 1, true, "offline");
if (!ok) {
Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
return false;
}
Serial.println("MQTT connected");
mqtt.publish(topicStatus, "online", true);
mqtt.subscribe(topicCmd, 1);
return true;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
// Unique client ID from the chip's MAC address
uint64_t mac = ESP.getEfuseMac();
snprintf(deviceId, sizeof(deviceId), "esp32-%04X%08X",
(uint16_t)(mac >> 32), (uint32_t)mac);
snprintf(topicTelemetry, sizeof(topicTelemetry), "testmqtt/esp32/%s/telemetry", deviceId);
snprintf(topicStatus, sizeof(topicStatus), "testmqtt/esp32/%s/status", deviceId);
snprintf(topicCmd, sizeof(topicCmd), "testmqtt/esp32/%s/cmd", deviceId);
Serial.printf("Device ID: %s\n", deviceId);
connectWiFi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setBufferSize(512); // default is 256 bytes for the whole packet
mqtt.setKeepAlive(30);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}
if (!mqtt.connected()) {
// Non-blocking reconnect: try every 5 seconds without stalling loop()
unsigned long now = millis();
if (now - lastReconnectAttempt > 5000) {
lastReconnectAttempt = now;
if (connectMqtt()) lastReconnectAttempt = 0;
}
return;
}
mqtt.loop(); // must run often: handles keep-alive and incoming messages
unsigned long now = millis();
if (now - lastPublish > 10000) {
lastPublish = now;
float temperature = temperatureRead(); // ESP32 internal sensor (rough)
char payload[128];
snprintf(payload, sizeof(payload),
"{\"device\":\"%s\",\"temp\":%.1f,\"rssi\":%d,\"uptime\":%lu}",
deviceId, temperature, WiFi.RSSI(), now / 1000);
bool sent = mqtt.publish(topicTelemetry, payload);
Serial.printf("Publish %s: %s\n", sent ? "ok" : "FAILED", payload);
}
}Öffne den Seriellen Monitor mit 115200 Baud, um die Geräte-ID zu sehen, und verfolge dann die Telemetrie im Browser, indem du testmqtt/esp32/# abonnierst. Publishe led:on an das cmd-Topic, und die LED geht an.
Deine ESP32-Telemetrie live ansehen
So funktioniert der Sketch
Eindeutige Client-IDs
MQTT-Broker erlauben nur eine Verbindung pro Client-ID. Wenn zwei Boards dieselbe fest eingetragene ID wie ESP32Client verwenden, werfen sie sich in einer Endlosschleife gegenseitig raus. Leitest du die ID aus der eFuse-MAC-Adresse ab, bleibt sie eindeutig und über Neustarts hinweg stabil. Der Client-ID-Generator erklärt die Regeln, falls du ein anderes Schema willst.
Eine nicht blockierende Reconnect-Schleife
Viele Beispiele nutzen eine while (!client.connected())-Schleife mit delay(5000). Das blockiert alles andere auf dem Board. Das millis()-Muster oben versucht es alle fünf Sekunden erneut, während der Rest von loop() weiterläuft. mqtt.state() verrät dir, warum ein Connect fehlgeschlagen ist: -2 ist ein Netzwerkfehler, 4 falsche Zugangsdaten und 5 nicht autorisiert. Die vollständige Liste findest du in unserem Leitfaden zu MQTT-Verbindungsfehlern.
Puffergröße und MQTT_MAX_PACKET_SIZE
Der Puffer von PubSubClient muss das gesamte Paket aufnehmen: Fixed Header, Topic und Payload. Standard sind 256 Bytes, und eine JSON-Payload mit langem Topic überschreitet das schnell – publish() gibt dann stillschweigend false zurück. Ältere Tutorials raten, MQTT_MAX_PACKET_SIZE im Header der Bibliothek zu ändern; seit Version 2.8 rufst du einfach vor dem Verbinden setBufferSize() auf. Der MQTT-Paketgrößen-Rechner zeigt dir, wie groß deine Pakete wirklich sind.
Grenzen von PubSubClient
- Nur MQTT 3.1.1 – keine MQTT-5-Properties.
- Publisht nur mit QoS 0; Subscriptions können QoS 0 oder 1 nutzen.
- Retained Publishes und Last Will werden unterstützt, wie oben gezeigt.
Online-Status mit Last Will und Retained Messages
Der Aufruf von connect() registriert einen Last Will: Verliert das Board ohne sauberes Disconnect Strom oder WLAN, publisht der Broker nach Ablauf des Keep-Alive-Timeouts offline auf das Status-Topic. Direkt nach dem Verbinden publisht der Sketch online auf dasselbe Topic. Beides ist retained, sodass auch ein Dashboard, das später subscribt, sofort den aktuellen Zustand sieht. Dieses Muster wird unter Last Will and Testament und Retained Messages erklärt.
Testen auf der Kommandozeile
Zum Testen des Command-Topics brauchst du kein zweites Gerät. Mit installierten Mosquitto-Clients schickst du einen Befehl und beobachtest die Telemetrie (ersetze die ID durch die im Seriellen Monitor ausgegebene):
# watch everything your board publishes
mosquitto_sub -h broker.hivemq.com -t 'testmqtt/esp32/#' -v
# switch the LED on
mosquitto_pub -h broker.hivemq.com -t 'testmqtt/esp32/esp32-ABCD12345678/cmd' -m 'led:on'Weitere Kommandozeilen-Rezepte findest du unter Beispiele für mosquitto_pub und mosquitto_sub.
JSON mit ArduinoJson erzeugen
snprintf reicht für eine Handvoll Felder, aber für verschachtelte Daten oder das Parsen von JSON-Befehlen ist die Bibliothek ArduinoJson (Version 7) sicherer:
#include <ArduinoJson.h>
void publishTelemetry() {
JsonDocument doc;
doc["device"] = deviceId;
doc["temp"] = temperatureRead();
doc["rssi"] = WiFi.RSSI();
char payload[256];
size_t n = serializeJson(doc, payload, sizeof(payload));
mqtt.publish(topicTelemetry, (const uint8_t*)payload, n, false);
}Halte Payloads klein und flach: Kurze Keys und Zahlen statt Strings sparen RAM, Funkzeit und Broker-Bandbreite.
Fehlerbehebung anhand der State-Codes
mqtt.state() | Bedeutung | Typische Lösung |
|---|---|---|
-4 | Verbindungs-Timeout | Keep-Alive prüfen und sicherstellen, dass loop() oft läuft |
-2 | Netzwerkverbindung fehlgeschlagen | Falscher Host oder Port, Firewall oder fehlgeschlagener TLS-Handshake |
2 | Client-ID abgelehnt | Eine kürzere oder eindeutige Client-ID verwenden |
4 | Falscher Benutzername oder falsches Passwort | Zugangsdaten prüfen, inklusive Groß-/Kleinschreibung |
5 | Nicht autorisiert | Die Zugriffsregeln des Brokers verweigern diesen Client |
TLS mit WiFiClientSecure hinzufügen
Über den unverschlüsselten Port 1883 gehen deine Zugangsdaten und Daten im Klartext raus. Der Wechsel auf TLS an Port 8883 braucht drei Änderungen: WiFiClientSecure.h einbinden, den Netzwerk-Client austauschen und ihm das CA-Zertifikat deines Brokers übergeben:
#include <WiFiClientSecure.h>
// PEM of the root CA that signed your broker's certificate
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the root CA certificate here...
-----END CERTIFICATE-----
)EOF";
WiFiClientSecure net;
PubSubClient mqtt(net);
const uint16_t MQTT_PORT = 8883;
// in setup(), before mqtt.setServer():
net.setCACert(ROOT_CA);
// net.setInsecure(); // skips verification - local testing only!Viele öffentliche Broker nutzen Zertifikate von Let's Encrypt, deren Root ISRG Root X1 ist; das richtige Zertifikat findest du in der Doku deines Brokers. TLS braucht bei manchen Setups außerdem die korrekte Uhrzeit für die Gültigkeitsprüfung der Zertifikate und belegt spürbar mehr RAM – ein ESP32 kommt damit gut klar, ein ESP8266 kämpft. Das Gesamtbild liefern unsere MQTT-Security-Best-Practices.
Willst du diesen Sketch mit Host, Port und Zugangsdaten deines Brokers schon ausgefüllt? Der MQTT-Code-Generator erzeugt ihn für deine eigenen Broker-Einstellungen.
Häufig gestellte Fragen
Warum publisht PubSubClient keine Nachrichten über 256 Bytes?
PubSubClient nutzt standardmäßig einen 256-Byte-Puffer, der das gesamte MQTT-Paket inklusive Topic aufnehmen muss. publish() gibt false zurück, wenn das Paket nicht hineinpasst. Rufe vor dem Verbinden client.setBufferSize(1024) oder mehr auf.
Warum verliert mein ESP32 ständig die Verbindung zum MQTT-Broker?
Die häufigsten Ursachen sind eine Client-ID, die auch ein anderes Gerät nutzt, lange delay()-Aufrufe, die client.loop() ausbremsen, sodass Keep-Alive-Pings verpasst werden, und schwaches WLAN. Nutze pro Gerät eine eindeutige Client-ID und halte loop() nicht blockierend.
Unterstützt PubSubClient QoS 1 und MQTT 5?
PubSubClient spricht nur MQTT 3.1.1. Es kann mit QoS 0 oder 1 subscriben, publisht aber nur mit QoS 0. Wenn du QoS 1 beim Publishen oder MQTT-5-Features brauchst, sieh dir Bibliotheken wie AsyncMqttClient oder die ESP-IDF-Komponente esp-mqtt an.