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
- Öffne in der Arduino IDE die Voreinstellungen und füge diese Boardverwalter-URL hinzu:
https://arduino.esp8266.com/stable/package_esp8266com_index.json. - Boardverwalter → esp8266 by ESP8266 Community installieren.
- Bibliotheksverwalter → PubSubClient von Nick O'Leary installieren.
- Als Board NodeMCU 1.0 (ESP-12E Module) oder LOLIN(WEMOS) D1 R2 & mini wählen.
Mit arduino-cli sehen dieselben Schritte so aus:
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 PubSubClientDer 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
#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 kehrtloop()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)
offlineauf.../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.combehalten ohnehin keine Sessions, deshalb abonniert der Sketch inconnectMqtt()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, undpublish()liefert dannfalseohne 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:
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:
#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 langsamDer 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
| Methode | Wie | Abwägung |
|---|---|---|
setFingerprint() | SHA-1 des Server-Zertifikats pinnen | Am 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üfung | Verschlü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:
#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:
| state | Konstante | Typische Ursache |
|---|---|---|
| -4 | MQTT_CONNECTION_TIMEOUT | Keine Ping-Antwort innerhalb des Keep-Alive; schwaches WLAN |
| -3 | MQTT_CONNECTION_LOST | Netzwerk weg oder Broker hat den Socket geschlossen |
| -2 | MQTT_CONNECT_FAILED | TCP/TLS gescheitert: falscher Host oder Port, DNS, Firewall, veralteter Fingerprint |
| -1 | MQTT_DISCONNECTED | Saubere Trennung oder nie verbunden |
| 1 | MQTT_CONNECT_BAD_PROTOCOL | Broker lehnt MQTT 3.1.1 ab |
| 2 | MQTT_CONNECT_BAD_CLIENT_ID | Client-ID abgelehnt |
| 3 | MQTT_CONNECT_UNAVAILABLE | Broker nicht verfügbar |
| 4 | MQTT_CONNECT_BAD_CREDENTIALS | Falscher Benutzername oder falsches Passwort |
| 5 | MQTT_CONNECT_UNAUTHORIZED | Authentifiziert, 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.


