ESP32 und Arduino

PubSubClient-Arduino-Bibliothek: Einrichtung, Callbacks, Puffer und Keep Alive

Ein praxisnahes PubSubClient-Tutorial für ESP32 und ESP8266: Bibliothek installieren, mit einem MQTT-Broker verbinden, Callbacks verarbeiten, Puffer dimensionieren, Keep Alive einstellen und mit echten Zugangsdaten auf TLS umsteigen.

Mikrocontroller-Board mit der PubSubClient-Bibliothek, das MQTT-Nachrichten mit einem Cloud-Broker austauscht
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

PubSubClient ist die meistgenutzte MQTT-Bibliothek für Arduino-kompatible Boards und klein genug, um auf fast allem mit einem Netzwerkstack zu laufen. In diesem Tutorial installieren Sie sie, verbinden einen ESP32 mit einem echten Broker, publizieren und abonnieren und lernen die drei Einstellungen kennen, die im Feld die meisten Probleme verursachen: Puffergröße, Keep Alive und Socket-Timeout. Am Ende haben Sie einen Sketch mit robustem Reconnect, den Sie in jedem Projekt wiederverwenden können.

Was PubSubClient ist (und was nicht)

PubSubClient, geschrieben von Nick O'Leary, ist ein schlanker MQTT-3.1.1-Client. Er besitzt die Netzwerkverbindung nicht selbst. Stattdessen kapselt er jedes Objekt, das die Arduino-Schnittstelle Client implementiert: WiFiClient auf ESP32 und ESP8266, EthernetClient auf einem kabelgebundenen Shield oder WiFiClientSecure, wenn Sie TLS nutzen wollen. Dieses Design ist der Grund, warum dieselbe Bibliothek auf so vielen Boards läuft.

Sie ist außerdem bewusst minimal gehalten. Wer die Grenzen vorher kennt, spart Stunden bei der Fehlersuche:

Funktion Verhalten von PubSubClient
Protokollversion MQTT 3.1.1 (MQTT 3.1 optional beim Kompilieren), kein MQTT 5
QoS beim Publish Nur QoS 0
QoS beim Subscribe QoS 0 oder QoS 1
Standard-Paketpuffer 256 Byte (Topic + Payload + Header)
Standard-Keep-Alive 15 Sekunden
Standard-Socket-Timeout 15 Sekunden
Last Will Unterstützt bei connect()
Retained Publish Unterstützt
Threading-Modell Single-Threaded, Sie müssen loop() häufig aufrufen

Wenn Sie MQTT-5-Properties oder QoS-2-Publishing vom Gerät aus brauchen, sehen Sie sich einen anderen Client an. Für die große Mehrheit von Sensoren und Aktoren sind QoS-0-Telemetrie plus QoS-1-Befehlsabonnements genau das Richtige. Unser Leitfaden zu den MQTT-QoS-Stufen erklärt, warum diese Kombination gut funktioniert.

PubSubClient installieren und das Board vorbereiten

  1. Öffnen Sie in der Arduino IDE Werkzeuge → Bibliotheken verwalten, suchen Sie nach "PubSubClient" und installieren Sie die Bibliothek von Nick O'Leary (Version 2.8 oder neuer).
  2. Installieren Sie das benötigte Board-Paket: "esp32 by Espressif Systems" für ESP32-Boards oder "esp8266 by ESP8266 Community" für Boards im NodeMCU-Stil.
  3. Wählen Sie Board und Port aus und laden Sie einmal den leeren Blink-Sketch hoch, um zu prüfen, ob die Toolchain funktioniert.
  4. Wählen Sie eine eindeutige Geräte-ID, zum Beispiel pubsub-7f3a9c. Sie verwenden sie sowohl als MQTT Client ID als auch als Topic-Präfix.

Für den ersten Test nutzen Sie den öffentlichen Bereich von broker.connectmq.com, den von ConnectMQ betriebenen kostenlosen öffentlichen MQTT-Broker, der für Tests und Prototypen gedacht ist. Er akzeptiert anonyme Verbindungen auf Port 1883, solange die Client ID nicht leer ist. Alles, was dort veröffentlicht wird, ist für jeden lesbar; senden Sie daher niemals echte Zugangsdaten oder personenbezogene Daten dorthin.

Das vollständige PubSubClient-Beispiel

Der folgende Sketch verbindet sich mit dem WLAN, verbindet sich mit Last Will mit dem Broker, veröffentlicht alle fünf Sekunden eine JSON-Nachricht, abonniert ein Befehls-Topic und verbindet sich neu, ohne den Rest Ihres Codes zu blockieren.

#include <WiFi.h>          // use <ESP8266WiFi.h> on ESP8266
#include <PubSubClient.h>

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* DEVICE_ID = "pubsub-7f3a9c";   // make this unique

char topicTelemetry[64];
char topicCommand[64];
char topicStatus[64];

WiFiClient netClient;
PubSubClient mqtt(netClient);

unsigned long lastPublish = 0;
unsigned long lastReconnectAttempt = 0;

void onMessage(char* topic, byte* payload, unsigned int length) {
  // payload is NOT null-terminated: copy it with the length
  char msg[64];
  unsigned int n = length < sizeof(msg) - 1 ? length : sizeof(msg) - 1;
  memcpy(msg, payload, n);
  msg[n] = '\0';

  Serial.printf("Message on %s: %s\n", topic, msg);

  if (strcmp(msg, "ping") == 0) {
    mqtt.publish(topicStatus, "pong");
  }
}

void connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nIP address: %s\n", WiFi.localIP().toString().c_str());
}

bool connectMqtt() {
  // connect(clientId, user, pass, willTopic, willQos, willRetain, willMessage)
  bool ok = mqtt.connect(DEVICE_ID, nullptr, nullptr,
                         topicStatus, 1, true, "offline");
  if (ok) {
    mqtt.publish(topicStatus, "online", true);
    mqtt.subscribe(topicCommand, 1);
    Serial.println("MQTT connected");
  } else {
    Serial.printf("MQTT connect failed, rc=%d\n", mqtt.state());
  }
  return ok;
}

void setup() {
  Serial.begin(115200);

  snprintf(topicTelemetry, sizeof(topicTelemetry), "connectmq/demo/%s/telemetry", DEVICE_ID);
  snprintf(topicCommand,   sizeof(topicCommand),   "connectmq/demo/%s/cmd", DEVICE_ID);
  snprintf(topicStatus,    sizeof(topicStatus),    "connectmq/demo/%s/status", DEVICE_ID);

  connectWifi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);   // room for larger JSON payloads
  mqtt.setKeepAlive(30);     // seconds
  mqtt.setSocketTimeout(10); // seconds
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWifi();
  }

  if (!mqtt.connected()) {
    unsigned long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      connectMqtt();
    }
    return;
  }

  mqtt.loop();

  if (millis() - lastPublish > 5000) {
    lastPublish = millis();
    char payload[96];
    snprintf(payload, sizeof(payload),
             "{\"uptime\":%lu,\"rssi\":%d,\"heap\":%u}",
             millis() / 1000, WiFi.RSSI(), ESP.getFreeHeap());
    if (!mqtt.publish(topicTelemetry, payload)) {
      Serial.println("Publish failed");
    }
  }
}

Beobachten Sie die Ausgabe auf Ihrem Rechner mit den Mosquitto-Kommandozeilenwerkzeugen:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-7f3a9c -t "connectmq/demo/pubsub-7f3a9c/#" -v

Senden Sie dann einen Befehl an das Board:

mosquitto_pub -h broker.connectmq.com -p 1883 -i sender-7f3a9c -t "connectmq/demo/pubsub-7f3a9c/cmd" -m "ping"

Innerhalb einer Sekunde sollte pong auf dem Status-Topic erscheinen.

So funktionieren Callbacks in PubSubClient

Die Callback-Signatur ist fest vorgegeben: void callback(char* topic, byte* payload, unsigned int length). Drei Details sind wichtig.

Erstens ist die Payload ein Zeiger in den internen Puffer der Bibliothek und nicht nullterminiert. Wer sie mit Serial.println((char*)payload) als C-String behandelt, gibt nach der eigentlichen Nachricht Datenmüll aus. Kopieren Sie immer anhand von length, wie im Beispiel.

Zweitens läuft der Callback innerhalb von mqtt.loop(). Wenn Sie aus dem Callback heraus publizieren, verwendet die Bibliothek denselben Puffer erneut, sodass die ursprünglichen Zeiger topic und payload danach nicht mehr gültig sind. Kopieren Sie alles, was Sie brauchen, bevor Sie publish() aufrufen.

Drittens: Halten Sie Callbacks kurz. Ein delay(2000) im Callback hindert loop() daran, die Verbindung zu bedienen, und kann einen Keep-Alive-Timeout auslösen. Setzen Sie ein Flag oder speichern Sie den Wert und reagieren Sie dann in Ihrer Hauptschleife darauf.

Puffergröße: die häufigste PubSubClient-Falle

Seit Version 2.8 können Sie setBufferSize() zur Laufzeit aufrufen, statt MQTT_MAX_PACKET_SIZE im Header der Bibliothek zu ändern. Der Puffer muss das gesamte MQTT-Paket aufnehmen: festen Header, Topic-Länge, Topic und Payload. Mit dem Standard von 256 Byte scheitert ein JSON-Dokument mit 220 Zeichen auf einem Topic mit 40 Zeichen einfach, und publish() gibt ohne weitere Erklärung false zurück. Zu große eingehende Nachrichten werden stillschweigend verworfen, bevor Ihr Callback überhaupt aufgerufen wird.

Faustregeln:

  • Dimensionieren Sie den Puffer für Ihr größtes Topic plus Payload, zuzüglich etwa 10 Byte Header.
  • Rufen Sie setBufferSize() vor connect() auf und prüfen Sie den Rückgabewert: Er ist false, wenn die Speicherzuweisung fehlgeschlagen ist.
  • Halten Sie den Puffer auf dem ESP8266 moderat, weil sich TLS und WLAN den RAM teilen.

Ausführlich, inklusive Streaming großer Payloads mit beginPublish(), behandeln wir das Thema in PubSubClient-Puffergröße: Fehler bei großen Nachrichten beheben.

Keep Alive und Socket-Timeout

Keep Alive teilt dem Broker mit, wie lange der Client schweigen darf. PubSubClient sendet ein PINGREQ, wenn im Intervall nichts anderes gesendet wurde, aber nur, wenn loop() aufgerufen wird. Blockiert Ihr Code länger als etwa das Anderthalbfache des Keep-Alive-Intervalls, beendet der Broker die Session und Ihr Last Will wird ausgelöst.

  • setKeepAlive(30) ist ein sinnvoller Standard für netzbetriebene Geräte in ordentlichem WLAN.
  • Gehen Sie bei Mobilfunkverbindungen, bei denen jedes Paket Energie oder Datenvolumen kostet, höher (60 bis 120 Sekunden).
  • setSocketTimeout(10) begrenzt, wie lange connect() und Lesevorgänge blockieren dürfen. Niedrigere Werte verhindern, dass ein wackeliges Netzwerk Ihr Gerät jeweils 15 Sekunden lang einfriert.

Den Verbindungszustand auslesen

Wenn connect() fehlschlägt, sagt Ihnen mqtt.state(), warum. Negative Werte sind Probleme auf Netzwerkebene, positive Werte sind Ablehnungen durch den Broker.

state() Bedeutung
-4 Verbindungs-Timeout, kein CONNACK rechtzeitig
-3 Verbindung verloren
-2 Netzwerkverbindung fehlgeschlagen (DNS, Port, TLS, Routing)
-1 Sauber getrennt
0 Verbunden
1 Falsche Protokollversion
2 Client ID abgelehnt
3 Server nicht verfügbar
4 Falscher Benutzername oder falsches Passwort
5 Nicht autorisiert

Der mit Abstand häufigste Wert ist -2. Unser eigener Leitfaden zu PubSubClient rc=-2 und anderen Statuscodes geht jeden Wert mit Lösungen durch.

Umstieg auf TLS und echte Zugangsdaten

Der öffentliche MQTT-Broker ist ideal zum Lernen, aber Produktivgeräte brauchen Verschlüsselung und eigene Zugangsdaten. Mit einem verwalteten MQTT-Broker von ConnectMQ erstellen Sie im Portal einen Broker und erhalten einen generierten Benutzernamen, ein Passwort und eine Client ID. Die Änderung am Sketch ist klein:

#include <WiFiClientSecure.h>

WiFiClientSecure netClient;   // replaces WiFiClient
PubSubClient mqtt(netClient);

// ISRG Root X1, the Let's Encrypt root used by broker.connectmq.com
const char* ROOT_CA = R"EOF(
-----BEGIN CERTIFICATE-----
...paste ISRG Root X1 PEM here...
-----END CERTIFICATE-----
)EOF";

void setup() {
  // ... Wi-Fi as before ...
  netClient.setCACert(ROOT_CA);
  mqtt.setServer("broker.connectmq.com", 8883);
}

bool connectMqtt() {
  return mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD",
                      topicStatus, 1, true, "offline");
}

ConnectMQ bindet jede Client ID an die dafür ausgegebenen Zugangsdaten, verwenden Sie also genau die Client ID aus dem Portal. Topic-Präfixe auf einem privaten Broker gestalten Sie selbst; das öffentliche Präfix connectmq/demo/... wird nicht mehr benötigt. Sitzt das Board als Retrofit-Sensor an einer Maschine in der Fertigung, bietet sich die ISA-95-Struktur eines Unified Namespace (UNS) an, etwa acme/werk-muenchen/montage/linie-1/zelle-2/vibration. Weitere Ideen zur Absicherung finden Sie in den Best Practices für MQTT-Sicherheit.

Checkliste zur Fehlersuche

  1. Verbindet sich und bricht dann alle paar Sekunden ab: Zwei Geräte teilen sich eine Client ID. Der Broker trennt die ältere Session, sobald sich das zweite Gerät verbindet.
  2. publish() gibt false zurück: Payload plus Topic übersteigen den Puffer, oder der Client ist nicht verbunden.
  3. Callback wird nie ausgelöst: Sie haben mqtt.loop() vergessen, das Abonnement wurde vor dem Bestehen der Verbindung angelegt, oder die Nachricht ist größer als der Puffer.
  4. Zufällige Verbindungsabbrüche unter Last: lange blockierender Code in loop(); teilen Sie ihn auf oder verlagern Sie ihn in einen Timer.
  5. Funktioniert auf 1883, scheitert auf 8883: falsches Root-Zertifikat, oder die Uhr ist auf Boards, die Gültigkeitsdaten prüfen, nicht gestellt (häufig beim ESP8266).

Wenn Sie zwischen Bibliotheken wählen, behandeln der Leitfaden zur ArduinoMqttClient-Bibliothek und der Async-MQTT-Client für den ESP32 die wichtigsten Alternativen, und das ESP32-IoT-Tutorial fügt alle Teile zu einem vollständigen Sensorprojekt zusammen.

Verbinden Sie Ihre PubSubClient-Geräte mit ConnectMQ

Sobald Ihr Sketch mit dem öffentlichen Broker läuft, ist der Umzug auf einen privaten Cloud-MQTT-Broker eine Änderung von zwei Zeilen. ConnectMQ bietet Ihnen MQTT über TCP, TLS auf 8883 und WebSockets, einen MQTT-Client im Browser zum Testen, einen Live-Topic-Explorer und Schwellenwert-Alarmregeln. Erstellen Sie einen kostenlosen Serverless-Broker unter app.connectmq.com, ohne Kreditkarte, und Sie haben in wenigen Minuten Ihre Zugangsdaten.

Häufig gestellte Fragen

Unterstützt PubSubClient MQTT 5?

Nein. PubSubClient spricht MQTT 3.1.1, das jeder gängige Broker, auch ConnectMQ, weiterhin neben MQTT 5 unterstützt. Wenn Sie MQTT-5-Properties auf dem Gerät brauchen, wählen Sie eine andere Client-Bibliothek.

Kann PubSubClient mit QoS 1 publizieren?

Nein. publish() sendet immer mit QoS 0. Sie können mit QoS 1 abonnieren, sodass Befehle, die mit QoS 1 an das Gerät gesendet werden, vom Broker zuverlässig zugestellt werden.

Warum zeigt meine PubSubClient-Payload seltsame Zeichen?

Der Payload-Puffer ist nicht nullterminiert. Kopieren Sie length Bytes in Ihren eigenen Puffer und fügen Sie ein abschließendes \0 hinzu, bevor Sie ihn als String behandeln.

Wie groß darf eine Nachricht in PubSubClient maximal sein?

Die Puffergröße ist ein 16-Bit-Wert, die praktische Obergrenze liegt also knapp unter 64 KB, aber die eigentliche Grenze ist der RAM. Halten Sie Nachrichten auf ressourcenarmen Boards klein und denken Sie daran, dass auch Ihr Broker-Tarif eine maximale Nachrichtengröße hat.

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

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.