Tutorials

ESP8266-MQTT-Tutorial mit PubSubClient

NodeMCU oder Wemos D1 mini per PubSubClient an einen MQTT-Broker anbinden: kompletter Sketch, nicht blockierender Reconnect, Last Will und TLS mit Fingerprint.

Aktualisiert · 9 Min. Lesezeit

ESP8266-MQTT-Tutorial mit PubSubClient

Der ESP8266 ist immer noch einer der günstigsten Wege, einen Sensor ins WLAN zu bringen, und PubSubClient ist die MQTT-Bibliothek, die die meisten ESP8266-Sketches nutzen. Dieses Tutorial liefert dir einen kompletten Sketch für NodeMCU oder Wemos D1 mini, der sich mit einem MQTT-Broker verbindet, Telemetrie publiziert, Befehle empfängt, sich ohne Blockieren neu verbindet und sich per Last Will meldet – und stellt die Verbindung anschließend auf TLS mit Zertifikats-Fingerprint um. Außerdem erfährst du, wann eine Root-CA die bessere Wahl ist.

Getestetes Setup: Arduino IDE 2.x oder arduino-cli, ESP8266-Core 3.1.x, PubSubClient 2.8.

Toolchain installieren

  1. Öffne in der Arduino IDE die Voreinstellungen und füge diese Boardverwalter-URL hinzu: https://arduino.esp8266.com/stable/package_esp8266com_index.json.
  2. Boardverwalter → esp8266 by ESP8266 Community installieren.
  3. Bibliotheksverwalter → PubSubClient von Nick O'Leary installieren.
  4. Als Board NodeMCU 1.0 (ESP-12E Module) oder LOLIN(WEMOS) D1 R2 & mini wählen.

Mit arduino-cli sehen dieselben Schritte so aus:

Setup mit arduino-cli
arduino-cli config add board_manager.additional_urls \
  https://arduino.esp8266.com/stable/package_esp8266com_index.json
arduino-cli core update-index
arduino-cli core install esp8266:esp8266
arduino-cli lib install PubSubClient

Der ESP8266 unterstützt nur 2,4-GHz-WLAN. Sendet dein Router eine gemeinsame SSID für beide Bänder, klappt das meist; ein reines 5-GHz-Netz funktioniert nie.

Der komplette Sketch

esp8266_mqtt.ino
#include <ESP8266WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID = "your-ssid";
const char* WIFI_PASS = "your-wifi-password";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = "username";
const char* MQTT_PASS = "password";

WiFiClient net;
PubSubClient mqtt(net);

String clientId, baseTopic;
unsigned long lastAttempt = 0, retryDelay = 1000, 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("[%s] %s\n", topic, msg.c_str());
  if (msg == "on")  digitalWrite(LED_BUILTIN, LOW);    // die eingebaute LED ist low-aktiv
  if (msg == "off") digitalWrite(LED_BUILTIN, HIGH);
}

bool connectMqtt() {
  String status = baseTopic + "/status";
  // connect(id, user, pass, willTopic, willQos, willRetain, willMessage)
  if (!mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS,
                    status.c_str(), 1, true, "offline")) {
    Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
    return false;
  }
  mqtt.publish(status.c_str(), "online", true);              // retained
  mqtt.subscribe((baseTopic + "/led").c_str(), 1);
  Serial.println("MQTT connected as " + clientId);
  return true;
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);

  WiFi.persistent(false);              // Flash nicht bei jedem Boot neu beschreiben
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  clientId  = "esp8266-" + String(ESP.getChipId(), HEX);
  baseTopic = "testmqtt/esp8266/" + clientId;

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);             // Standard sind 256 Bytes inkl. Topic und Header
  mqtt.setKeepAlive(30);
  mqtt.setSocketTimeout(5);            // ein fehlgeschlagener Connect blockiert höchstens ~5 s
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    delay(10);                         // der Core verbindet das WLAN selbst neu
    return;
  }

  if (!mqtt.connected()) {
    unsigned long now = millis();
    if (now - lastAttempt >= retryDelay) {
      lastAttempt = now;
      retryDelay = connectMqtt() ? 1000 : min(retryDelay * 2, 60000UL);
    }
    return;
  }

  mqtt.loop();                         // muss oft laufen: empfängt Nachrichten, sendet Pings

  if (millis() - lastPublish >= 10000) {
    lastPublish = millis();
    char payload[96];
    snprintf(payload, sizeof(payload),
             "{\"rssi\":%d,\"heap\":%u,\"uptime\":%lu}",
             WiFi.RSSI(), ESP.getFreeHeap(), millis() / 1000);
    mqtt.publish((baseTopic + "/telemetry").c_str(), payload);
  }
}

Warum der Sketch so aussieht

  • Nicht blockierender Reconnect. Das klassische Beispiel nutzt while (!client.connected()) { ...; delay(5000); } und friert damit alles andere ein – Taster, Sensoren, sogar den WLAN-Stack –, solange der Broker nicht erreichbar ist. Hier kehrt loop() sofort zurück und versucht es mit exponentiellem Backoff erneut (1 s, 2 s, 4 s … bis 60 s), damit eine Flotte einen neu startenden Broker nicht überrennt.
  • Stabile Client-ID aus ESP.getChipId(). Zwei Boards mit derselben ID werfen sich endlos gegenseitig raus; siehe Best Practices für Client-IDs.
  • Last Will + retained Status. Verliert das Board den Strom, publiziert der Broker nach Ablauf des Keep-Alive (hier 1,5 × 30 s) offline auf .../status. Wer später abonniert, sieht sofort den aktuellen Zustand.
  • Nach jedem Connect abonnieren. PubSubClient nutzt eine Clean Session, Subscriptions überleben einen Reconnect also nicht. Geteilte Test-Endpunkte wie broker.connectmq.com behalten ohnehin keine Sessions, deshalb abonniert der Sketch in connectMqtt() immer neu.
  • setBufferSize(512) – der Standardpuffer von PubSubClient umfasst 256 Bytes für das gesamte Paket, inklusive Topic. Ein langes Topic plus JSON-Payload sprengt das schnell, und publish() liefert dann false ohne weiteren Hinweis. Der Paketgrößen-Rechner zeigt, wie groß dein Paket wirklich ist.
  • setSocketTimeout(5) – connect() selbst blockiert. Der Standard-Timeout von 15 Sekunden reicht, um Watchdog-Resets oder eine eingefrorene Oberfläche auszulösen.

Den Sketch testen

Flashe das Board und öffne den seriellen Monitor mit 115200 Baud. Du solltest MQTT connected as esp8266-xxxxxx sehen. Erscheint stattdessen state=-2 oder ein anderer Code, spring zur Fehlertabelle weiter unten.

Öffne dann den Online-MQTT-Client, verbinde dich mit broker.connectmq.com und abonniere testmqtt/esp8266/#. Zuerst erscheint der retained online-Status, danach alle 10 Sekunden ein Telemetrie-JSON. Publiziere on an testmqtt/esp8266/<client-id>/led, und die LED geht an.

Deinen ESP8266 im Browser beobachten

Um den Last Will zu testen, zieh den Stecker und warte etwa 45 Sekunden: Das Status-Topic springt auf offline. Steck das Board wieder ein, und es kehrt zu online zurück – dein Reconnect-Pfad funktioniert.

TLS mit Fingerprint ergänzen

Port 1883 überträgt das MQTT-Passwort im Klartext. Der ESP8266-Core enthält BearSSL, für TLS auf Port 8883 brauchst du also nur eine andere Client-Klasse. Die schlankeste Art, den Server zu prüfen, ist Certificate Pinning per SHA-1-Fingerprint.

Hol dir den Fingerprint des aktuellen Broker-Zertifikats von deinem Rechner:

SHA-1-Fingerprint auslesen
openssl s_client -connect broker.connectmq.com:8883 \
  -servername broker.connectmq.com </dev/null 2>/dev/null \
  | openssl x509 -noout -fingerprint -sha1 \
  | sed 's/.*=//; s/:/ /g'

Dann änderst du drei Dinge im Sketch:

Änderungen für TLS
#include <WiFiClientSecure.h>

const uint16_t MQTT_PORT = 8883;
const char* FINGERPRINT = "AA BB CC DD EE FF 00 11 22 33 44 55 66 77 88 99 AA BB CC DD";

BearSSL::WiFiClientSecure net;         // ersetzt WiFiClient
PubSubClient mqtt(net);

// in setup(), vor mqtt.setServer():
net.setFingerprint(FINGERPRINT);
mqtt.setSocketTimeout(10);             // der TLS-Handshake ist auf einem 80-MHz-Chip langsam

Der Handshake dauert merklich länger als ein normaler TCP-Connect und belegt einen guten Teil des knappen ESP8266-RAMs. Behalte ESP.getFreeHeap() in der Telemetrie im Blick; wird es eng, kann BearSSL mit net.setBufferSizes(1024, 1024) kleinere Puffer nutzen – aber nur, wenn der Broker die TLS-Erweiterung Max Fragment Length unterstützt. Prüfe das vorher mit BearSSL::WiFiClientSecure::probeMaxFragmentLength(MQTT_HOST, 8883, 1024).

Fingerprint vs. Root-CA

MethodeWieAbwägung
setFingerprint()SHA-1 des Server-Zertifikats pinnenAm kleinsten und schnellsten, bricht aber bei jeder Zertifikatserneuerung
setTrustAnchors()Root-CA pinnen (BearSSL::X509List)Übersteht Erneuerungen; braucht die korrekte Uhrzeit per configTime()
setInsecure()Keine PrüfungVerschlüsselt, akzeptiert aber jeden Server – nur zum Debuggen

Zertifikate öffentlicher CAs werden regelmäßig erneuert, oft alle paar Monate. Eine Flotte mit gepinntem Fingerprint verbindet sich am Tag der Erneuerung nicht mehr (state=-2), und du musst jedes Gerät neu flashen. Für alles Langlebige pinnst du stattdessen die Root-CA:

Root-CA statt Fingerprint
#include <time.h>
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...Root-Zertifikat der CA, die das Zertifikat deines Brokers signiert...
-----END CERTIFICATE-----
)EOF";
BearSSL::X509List trustAnchor(ROOT_CA);

// in setup(), nachdem das WLAN verbunden ist:
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
while (time(nullptr) < 1700000000) delay(200);   // auf NTP warten
net.setTrustAnchors(&trustAnchor);

Ohne gültige Uhrzeit hält BearSSL jedes Zertifikat für abgelaufen oder noch nicht gültig. Mehr zu TLS, einschließlich Client-Zertifikaten, findest du im ESP32-TLS-Leitfaden – die BearSSL-API ist anders, die Konzepte sind dieselben.

Fehlercodes

mqtt.state() liefert diese Werte:

stateKonstanteTypische Ursache
-4MQTT_CONNECTION_TIMEOUTKeine Ping-Antwort innerhalb des Keep-Alive; schwaches WLAN
-3MQTT_CONNECTION_LOSTNetzwerk weg oder Broker hat den Socket geschlossen
-2MQTT_CONNECT_FAILEDTCP/TLS gescheitert: falscher Host oder Port, DNS, Firewall, veralteter Fingerprint
-1MQTT_DISCONNECTEDSaubere Trennung oder nie verbunden
1MQTT_CONNECT_BAD_PROTOCOLBroker lehnt MQTT 3.1.1 ab
2MQTT_CONNECT_BAD_CLIENT_IDClient-ID abgelehnt
3MQTT_CONNECT_UNAVAILABLEBroker nicht verfügbar
4MQTT_CONNECT_BAD_CREDENTIALSFalscher Benutzername oder falsches Passwort
5MQTT_CONNECT_UNAUTHORIZEDAuthentifiziert, aber keine Verbindungserlaubnis

Positive Werte sind CONNACK-Return-Codes des Brokers – schlag sie im Reason-Code-Tool nach. Negative Werte stammen aus der Bibliothek selbst und bedeuten fast immer ein Netzwerkproblem. Die Schritt-für-Schritt-Methode aus ESP32-Broker-Verbindung testen funktioniert auf einem ESP8266 genauso.

Wie es weitergeht

  • Wechsel auf einen neueren Chip? Das ESP32-Arduino-Tutorial nutzt dieselbe PubSubClient-API.
  • QoS-1-Publishes oder ein anderer API-Stil gewünscht? Vergleiche mit der arduino-mqtt-Bibliothek.
  • Größere Payloads? ArduinoJson mit MQTT zeigt, wie du Puffer richtig dimensionierst.

Ein zuverlässiger ESP8266-MQTT-Knoten hängt an ein paar Gewohnheiten: loop() nie blockieren, mit Backoff neu verbinden, nach jedem Connect neu abonnieren, den Puffer für deine echten Pakete dimensionieren und bei Geräten, die auch nach der nächsten Zertifikatserneuerung laufen müssen, eine Root-CA statt eines Fingerprints pinnen.

Häufig gestellte Fragen

Warum funktioniert die TLS-Verbindung meines ESP8266 nach ein paar Wochen nicht mehr?

Wenn du das Zertifikat mit setFingerprint() gepinnt hast, ändert sich der Fingerprint bei jeder Zertifikatserneuerung des Brokers, und der Handshake scheitert dann mit state -2. Pinne stattdessen die Root-CA mit setTrustAnchors(); das übersteht Erneuerungen, solange dieselbe CA das neue Zertifikat signiert.

Kann PubSubClient auf dem ESP8266 mit QoS 1 publizieren?

Nein. PubSubClient publiziert nur mit QoS 0. Abonnieren geht mit QoS 0 oder 1, eingehende Nachrichten können also bestätigt werden, ausgehende sind aber Fire-and-Forget. Brauchst du QoS-1-Publishes, nimm eine Bibliothek wie arduino-mqtt oder ergänze eine Bestätigung auf Anwendungsebene.

Was bedeutet PubSubClient-state -2?

MQTT_CONNECT_FAILED: Die TCP- oder TLS-Verbindung zum Broker ließ sich nicht öffnen. Das Problem liegt unterhalb von MQTT – WLAN, DNS, falscher Port, eine Firewall oder ein fehlgeschlagener TLS-Handshake, etwa ein veralteter Fingerprint oder ein normaler WiFiClient auf Port 8883.