Mit ESP32-WiFiManager-MQTT-Parametern lässt sich ein Gerät, das Sie an jemand anderen weitergeben, per Smartphone konfigurieren: Es öffnet ein Captive Portal, der Nutzer wählt ein WLAN und füllt die Broker-Einstellungen aus, und das Gerät merkt sich alles über Neustarts hinweg. Dieses Tutorial baut diesen Ablauf mit der WiFiManager-Bibliothek und Preferences auf und verwendet standardmäßig broker.connectmq.com, sodass alles sofort funktioniert.
Das Problem mit fest codierten Einstellungen
Die meisten MQTT-Beispiele beginnen mit Zeilen wie const char* WIFI_SSID = "..." und const char* MQTT_PASS = "...". Auf dem Schreibtisch ist das in Ordnung, aber jedes neue Netzwerk und jede Änderung der Zugangsdaten bedeutet Neukompilieren und erneutes Flashen. Außerdem enthält das Firmware-Binary dann Geheimnisse, und Sie können nicht dasselbe Binary an zwei Kunden geben.
Ein Provisionierungsportal löst das. Beim ersten Start oder wenn der Nutzer einen Taster gedrückt hält, startet der ESP32 einen eigenen Access Point. Verbindet man sich mit dem Smartphone, öffnet sich eine Webseite, auf der der Nutzer Folgendes einträgt:
- Das WLAN und sein Passwort.
- Host und Port des MQTT-Brokers.
- MQTT-Client-ID, Benutzername und Passwort.
- Ein Topic-Präfix für dieses Gerät.
Das Gerät speichert die Werte im nichtflüchtigen Speicher, tritt dem WLAN bei und verbindet sich mit dem Broker. Dasselbe Binary funktioniert für alle.
Bibliotheken und Hardware
- WiFiManager von tzapu (2.x), installierbar über den Arduino-Bibliotheksverwalter oder in PlatformIO als
tzapu/WiFiManager. - PubSubClient für MQTT.
- Preferences, das mit dem Arduino-ESP32-Core ausgeliefert wird und Schlüssel-Wert-Paare im NVS-Flash speichert.
- Ein beliebiges ESP32-Board. Das Beispiel nutzt den BOOT-Taster (GPIO 0) der meisten DevKits, um das Portal erneut zu öffnen.
So funktioniert der ESP32-WiFiManager-MQTT-Ablauf
| Situation | Was passiert |
|---|---|
| Erster Start, kein gespeichertes WLAN | Portal startet als Access Point Sensor-Setup-xxxx |
| Gespeichertes WLAN erreichbar | Gerät verbindet sich still, lädt die MQTT-Einstellungen aus Preferences |
| Gespeichertes WLAN nicht erreichbar | Portal öffnet sich erneut, nachdem der Verbindungsversuch gescheitert ist |
| BOOT-Taster beim Start 3 s gehalten | Portal öffnet sich auf Wunsch, damit der Nutzer Einstellungen ändern kann |
| Portal 3 Minuten ungenutzt | Gerät startet neu und versucht es erneut, sodass es nie dauerhaft hängt |
WiFiManager speichert die WLAN-Zugangsdaten selbst. Die MQTT-Felder sind eigene Parameter, die Sie dem Portal hinzufügen und selbst dauerhaft speichern.
Der vollständige Sketch
#include <WiFi.h>
#include <WiFiManager.h>
#include <Preferences.h>
#include <PubSubClient.h>
const int CONFIG_BUTTON = 0; // BOOT-Taster der meisten ESP32-DevKits
Preferences prefs;
WiFiClient net;
PubSubClient mqtt(net);
char mqttHost[64] = "broker.connectmq.com";
char mqttPort[6] = "1883";
char mqttClient[48] = "";
char mqttUser[48] = "";
char mqttPass[64] = "";
char topicPrefix[64] = "connectmq/demo/d93f2a";
bool shouldSave = false;
unsigned long lastPublish = 0;
void loadSettings() {
prefs.begin("mqtt", true);
prefs.getString("host", mqttHost, sizeof(mqttHost));
prefs.getString("port", mqttPort, sizeof(mqttPort));
prefs.getString("client", mqttClient, sizeof(mqttClient));
prefs.getString("user", mqttUser, sizeof(mqttUser));
prefs.getString("pass", mqttPass, sizeof(mqttPass));
prefs.getString("prefix", topicPrefix, sizeof(topicPrefix));
prefs.end();
if (strlen(mqttClient) == 0) { // niemals mit leerer Client ID verbinden
snprintf(mqttClient, sizeof(mqttClient), "esp32-%08x", (uint32_t)ESP.getEfuseMac());
}
}
void saveSettings() {
prefs.begin("mqtt", false);
prefs.putString("host", mqttHost);
prefs.putString("port", mqttPort);
prefs.putString("client", mqttClient);
prefs.putString("user", mqttUser);
prefs.putString("pass", mqttPass);
prefs.putString("prefix", topicPrefix);
prefs.end();
}
void runWifiManager(bool forcePortal) {
WiFiManager wm;
WiFiManagerParameter pHost("host", "MQTT host", mqttHost, sizeof(mqttHost) - 1);
WiFiManagerParameter pPort("port", "MQTT port (1883 or 8883)", mqttPort, sizeof(mqttPort) - 1);
WiFiManagerParameter pClient("client", "MQTT client ID", mqttClient, sizeof(mqttClient) - 1);
WiFiManagerParameter pUser("user", "MQTT username (optional)", mqttUser, sizeof(mqttUser) - 1);
WiFiManagerParameter pPass("pass", "MQTT password (optional)", mqttPass, sizeof(mqttPass) - 1,
"type='password'");
WiFiManagerParameter pPrefix("prefix", "Topic prefix", topicPrefix, sizeof(topicPrefix) - 1);
wm.addParameter(&pHost);
wm.addParameter(&pPort);
wm.addParameter(&pClient);
wm.addParameter(&pUser);
wm.addParameter(&pPass);
wm.addParameter(&pPrefix);
wm.setSaveParamsCallback([]() { shouldSave = true; });
wm.setConfigPortalTimeout(180);
char apName[32];
snprintf(apName, sizeof(apName), "Sensor-Setup-%04x", (uint16_t)ESP.getEfuseMac());
bool connected = forcePortal ? wm.startConfigPortal(apName)
: wm.autoConnect(apName);
if (!connected) {
ESP.restart(); // Portal-Timeout: komplett von vorn versuchen
}
if (shouldSave) {
strlcpy(mqttHost, pHost.getValue(), sizeof(mqttHost));
strlcpy(mqttPort, pPort.getValue(), sizeof(mqttPort));
strlcpy(mqttClient, pClient.getValue(), sizeof(mqttClient));
strlcpy(mqttUser, pUser.getValue(), sizeof(mqttUser));
strlcpy(mqttPass, pPass.getValue(), sizeof(mqttPass));
strlcpy(topicPrefix, pPrefix.getValue(), sizeof(topicPrefix));
saveSettings();
loadSettings(); // wendet den Client-ID-Fallback erneut an
}
}
bool buttonHeld(unsigned long ms) {
pinMode(CONFIG_BUTTON, INPUT_PULLUP);
unsigned long start = millis();
while (digitalRead(CONFIG_BUTTON) == LOW) {
if (millis() - start > ms) return true;
delay(10);
}
return false;
}
void connectMqtt() {
while (!mqtt.connected()) {
bool ok = strlen(mqttUser) > 0
? mqtt.connect(mqttClient, mqttUser, mqttPass)
: mqtt.connect(mqttClient);
if (ok) {
String cmd = String(topicPrefix) + "/cmd";
mqtt.subscribe(cmd.c_str());
} else {
Serial.printf("MQTT failed state=%d, retrying\n", mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
loadSettings();
delay(200);
runWifiManager(buttonHeld(3000));
mqtt.setServer(mqttHost, atoi(mqttPort));
Serial.printf("Broker %s:%s as %s\n", mqttHost, mqttPort, mqttClient);
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (millis() - lastPublish > 10000) {
lastPublish = millis();
String topic = String(topicPrefix) + "/telemetry";
String payload = "{\"uptime\":" + String(millis() / 1000) + "}";
mqtt.publish(topic.c_str(), payload.c_str());
}
}
Ein Rundgang durch das Portal
- Flashen Sie den Sketch und öffnen Sie den seriellen Monitor.
- Verbinden Sie Ihr Smartphone mit dem WLAN namens
Sensor-Setup-xxxx. Die meisten Smartphones öffnen das Captive Portal automatisch; andernfalls rufen Sie192.168.4.1im Browser auf. - Tippen Sie auf Configure WiFi, wählen Sie Ihr Netzwerk und geben Sie dessen Passwort ein.
- Behalten Sie die Standardwerte, um den öffentlichen MQTT-Broker broker.connectmq.com zu nutzen, oder geben Sie eigene Einstellungen ein, und speichern Sie.
- Das Gerät tritt Ihrem Netzwerk bei, verbindet sich mit dem Broker und beginnt, auf
<prefix>/telemetryzu veröffentlichen.
Prüfen Sie von einem Rechner aus:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-d93f2a -t 'connectmq/demo/d93f2a/#' -v
Jedes Gerät, das das Standardpräfix behält, veröffentlicht auf dasselbe Topic. Wenn Sie mehrere einrichten, geben Sie also jedem im Portal ein eigenes Präfix.
Details, die für Zuverlässigkeit sorgen
Erst laden, dann das Portal aufbauen. Die eigenen Parameter werden mit den aktuellen Werten als Vorgabe erzeugt, sodass das Portal das Gespeicherte zeigt und kein leeres Formular. Wer ein Feld bearbeitet, verliert die anderen nicht.
Niemals mit leerer Client ID verbinden. Leert der Nutzer das Feld, greift loadSettings() auf eine aus der MAC-Adresse des Chips abgeleitete ID zurück. Eine leere oder doppelte Client ID ist einer der häufigsten Gründe, warum Geräte sich nicht verbinden oder sich gegenseitig hinauswerfen; die vollständige Liste finden Sie unter warum sich ein ESP32 nicht mit einem MQTT-Broker verbinden kann.
Den Portal-Timeout nutzen. Ohne setConfigPortalTimeout() bleibt ein Gerät, dessen Router vorübergehend ausgefallen ist, für immer im Portalmodus. Mit Timeout startet es neu und versucht erneut das gespeicherte Netzwerk.
Zahlen im Portal als Strings speichern, einmal umwandeln. WiFiManager-Parameter sind Textfelder. Speichern Sie den Port als String und wandeln Sie ihn bei der Konfiguration des Clients mit atoi() um.
Das Passwortfeld verbergen. Das zusätzliche Argument type='password' maskiert das MQTT-Passwort im Portal. Der Wert wird über den offenen Access Point trotzdem per unverschlüsseltem HTTP übertragen, provisionieren Sie also nur an einem vertrauenswürdigen Ort.
Eingaben der Nutzer validieren
Ein Captive Portal akzeptiert alles, was der Nutzer eingibt, also muss die Firmware mit Fehlern umgehen können. Einige Prüfungen nach dem Speichern verhindern, dass sich ein Gerät mit falschen Einstellungen selbst lahmlegt:
- Port: Nur Werte zwischen 1 und 65535 akzeptieren. Liefert
atoi()0, weil das Feld Text enthielt, auf 1883 zurückfallen. - Host: Leerzeichen entfernen und eine leere Zeichenfolge ablehnen. Nutzer fügen oft eine URL wie
mqtt://broker.connectmq.comein; entfernen Sie das Schema und alles nach dem Hostnamen. - Topic-Präfix: Einen abschließenden Schrägstrich entfernen und Wildcards (
+und#) ablehnen, die in Publish-Topics nicht erlaubt sind. - Zugangsdaten: Ist ein Benutzername gesetzt, aber das Passwort leer, auf der seriellen Konsole warnen, weil der Broker die Verbindung ablehnen wird.
Scheitert die MQTT-Verbindung wiederholt mit einem positiven mqtt.state()-Code (falsche Zugangsdaten oder nicht autorisiert), liegt meist ein Einstellungsproblem und kein Netzwerkproblem vor. Ein gutes Muster ist, diese Fehlschläge zu zählen und das Portal nach mehreren in Folge automatisch wieder zu öffnen, damit der Nutzer die Werte ohne Taster oder serielles Kabel korrigieren kann.
TLS für Produktions-Broker ergänzen
Port 1883 ist unverschlüsselt und für Tests auf dem öffentlichen MQTT-Broker in Ordnung. Für einen privaten Broker wechseln Sie zu WiFiClientSecure, wenn der Nutzer Port 8883 wählt:
#include <WiFiClientSecure.h>
WiFiClientSecure secureNet;
void configureTransport() {
if (atoi(mqttPort) == 8883) {
secureNet.setCACert(ISRG_ROOT_X1); // Let's-Encrypt-Root, als PEM-String gespeichert
mqtt.setClient(secureNet);
} else {
mqtt.setClient(net);
}
mqtt.setServer(mqttHost, atoi(mqttPort));
}
Rufen Sie in setup() configureTransport() statt der einfachen setServer()-Zeile auf. Mit ConnectMQ tragen Nutzer dann die für ihren Broker generierte Client ID sowie Benutzername und Passwort im Portal ein (YOUR_CLIENT_ID, YOUR_USERNAME, YOUR_PASSWORD). Die Client ID ist an diese Zugangsdaten gebunden und muss daher exakt so eingegeben werden, wie sie ausgegeben wurde. Der Leitfaden zu ESP32 MQTT SSL/TLS erklärt, wie Sie das Root-Zertifikat einbetten.
Fehlerbehebung
| Problem | Wahrscheinliche Ursache | Lösung |
|---|---|---|
| Portal erscheint nie | Gespeichertes WLAN verbindet sich weiterhin | BOOT-Taster während des Resets halten oder einmal wm.resetSettings() aufrufen |
| Parameter werden nicht gespeichert | Save-Callback nicht ausgelöst | setSaveParamsCallback() verwenden und auf der Portalseite Save drücken |
| Werte abgeschnitten | Feldlänge kleiner als die Eingabe | Puffergröße und Parameterlänge erhöhen |
| Verbindet sich mit WLAN, aber nicht mit MQTT | Falscher Port für den Transport oder falsche Zugangsdaten | mqtt.state() und die Kombination aus Port und TLS prüfen |
| Smartphone öffnet das Portal nicht | Captive-Erkennung blockiert | Manuell 192.168.4.1 im Browser aufrufen |
Wie es weitergeht
Sobald die Einstellungen konfigurierbar sind, folgen als natürliche nächste Schritte Fernupdates und Statusmeldungen: ESP32-OTA-Updates per MQTT auslösen zeigt, wie Sie neue Firmware ohne USB ausliefern, und das ESP32-IoT-Tutorial bettet die Provisionierung in ein vollständiges Geräteprojekt ein.
Geräte für ConnectMQ provisionieren
Die Standardeinstellungen zeigen auf den kostenlosen öffentlichen MQTT-Broker, sodass ein frisches Gerät in dem Moment funktioniert, in dem es dem WLAN beitritt; dieser Broker ist für Tests und Prototypen gedacht, nicht für Produktionsdaten. Für echte Installationen erstellen Sie einen privaten Broker bei ConnectMQ und tragen dessen Zugangsdaten im Portal ein. ConnectMQ ist ein verwalteter MQTT-Broker, sodass die Personen, die Geräte konfigurieren, nie einen Server betreiben müssen. Das passt etwa zu Maschinenbauern, die Nachrüstsensoren für Energiemonitoring oder Zustandsüberwachung an Kundenwerke ausliefern: Der Servicetechniker trägt vor Ort WLAN, Broker-Zugangsdaten und ein Topic-Präfix nach der ISA-95-Struktur des Werks ein, und dasselbe Binary funktioniert in jeder Halle. Starten Sie im kostenlosen Serverless-Tarif unter app.connectmq.com, ohne Kreditkarte.
Häufig gestellte Fragen
Wo speichert WiFiManager das WLAN-Passwort?
Der WLAN-Treiber des ESP32 speichert die Netzwerkzugangsdaten im NVS-Flash. Die MQTT-Felder werden in diesem Sketch separat mit Preferences im Namespace mqtt gespeichert.
Wie öffne ich das WiFiManager-Portal später erneut?
Halten Sie in diesem Sketch den BOOT-Taster beim Start drei Sekunden lang gedrückt, was startConfigPortal() aufruft. Sie könnten es auch per MQTT-Befehl auslösen, solange das Gerät noch erreichbar ist.
Kann ich WiFiManager mit AsyncMqttClient verwenden?
Ja. WiFiManager kümmert sich nur um die Provisionierung. Sobald es zurückkehrt, konfigurieren Sie mit den gespeicherten Werten den MQTT-Client Ihrer Wahl.
Ist das Captive Portal sicher?
Das Portal läuft als offener Access Point über unverschlüsseltes HTTP, sodass jeder in der Nähe die Werte während der Einrichtung mitlesen könnte. Provisionieren Sie an einem vertrauenswürdigen Ort und erwägen Sie, ein Passwort für den Setup-Access-Point zu setzen.



