Tutorials

ESP32-MQTT-Tutorial mit Arduino (PubSubClient)

ESP32-MQTT-Tutorial mit Arduino und PubSubClient: mit WLAN und Broker verbinden, Sensor-JSON publishen, Befehle abonnieren, sicher neu verbinden und TLS aktivieren.

Aktualisiert · 8 Min. Lesezeit

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:

TopicRichtungZweck
testmqtt/esp32/<id>/telemetryESP32 zum BrokerSensor-JSON alle 10 Sekunden
testmqtt/esp32/<id>/statusESP32 zum Brokeronline / offline (retained, Last Will)
testmqtt/esp32/<id>/cmdBroker zum ESP32Befehle wie led:on

Namenskonventionen findest du unter MQTT-Topics und Wildcards.

Der vollständige Sketch

esp32_mqtt.ino
#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):

bash
# 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:

cpp
#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()BedeutungTypische Lösung
-4Verbindungs-TimeoutKeep-Alive prüfen und sicherstellen, dass loop() oft läuft
-2Netzwerkverbindung fehlgeschlagenFalscher Host oder Port, Firewall oder fehlgeschlagener TLS-Handshake
2Client-ID abgelehntEine kürzere oder eindeutige Client-ID verwenden
4Falscher Benutzername oder falsches PasswortZugangsdaten prüfen, inklusive Groß-/Kleinschreibung
5Nicht autorisiertDie 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:

TLS-Änderungen
#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.