ESP32 Wi-Fi Provisioning with WiFiManager and Custom MQTT Settings
Every sketch so far has had the Wi-Fi name and password typed into the code. That is fine on your desk, but it fails the moment you give the device to someone else, change your router, or take a project to an exhibition. Provisioning is the process of giving a device its network credentials after it has been built, without recompiling.
The WiFiManager library by tzapu makes this easy. If the ESP32 cannot join a known network, it starts its own access point with a captive portal. You connect with a phone, pick your Wi-Fi network, type the password, and fill in extra fields such as the MQTT broker address. The ESP32 saves everything and reboots as a normal client.
The Provisioning Flow
- On boot, WiFiManager tries the Wi-Fi credentials stored by the ESP32's Wi-Fi driver in flash.
- If they work,
autoConnect()returns true within a few seconds and your sketch carries on. - If not, the ESP32 switches to access point mode with the name you choose, such as
RoomNode-Setup, and runs a small web server and DNS server on 192.168.4.1. - A phone that joins this access point usually opens the configuration page automatically (the captive portal). If not, browse to 192.168.4.1.
- The user selects a network, enters the password and any custom fields, and taps Save.
- WiFiManager tests the connection. On success, the credentials are kept by the Wi-Fi driver and the custom values are handed to your code to store.
WiFiManager only remembers the Wi-Fi network itself. Custom values such as the MQTT host are your responsibility, and this tutorial stores them with the ESP32 Preferences library, which writes key-value pairs to the NVS partition in flash.
Hardware and Library
| Item | Details |
|---|---|
| Board | Any ESP32 DevKit |
| Reset button | The on-board BOOT button on GPIO 0 (active LOW), or a push button from a GPIO to GND |
| Status LED | On-board LED, GPIO 2 on many DevKit V1 boards (check your board) |
| Library | WiFiManager by tzapu, version 2.x, from the Library Manager |
| Built-in library | Preferences (part of the ESP32 Arduino core) |
Complete Sketch
#include <WiFi.h>
#include <WiFiManager.h> // tzapu WiFiManager 2.x
#include <Preferences.h>
const int BUTTON_PIN = 0; // BOOT button, LOW when pressed
const int LED_PIN = 2;
Preferences prefs;
String mqttHost, mqttPort, mqttUser;
bool shouldSave = false;
void saveConfigCallback() { // called only when the user saves the portal
shouldSave = true;
}
void loadSettings() {
prefs.begin("cfg", true); // namespace "cfg", read-only
mqttHost = prefs.getString("host", "192.168.1.50");
mqttPort = prefs.getString("port", "1883");
mqttUser = prefs.getString("user", "");
prefs.end();
}
void storeSettings() {
prefs.begin("cfg", false); // read-write
prefs.putString("host", mqttHost);
prefs.putString("port", mqttPort);
prefs.putString("user", mqttUser);
prefs.end();
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
loadSettings();
WiFi.mode(WIFI_STA); // WiFiManager handles AP mode itself
WiFiManager wm;
// Extra fields on the portal page: id, label, default value, max length
WiFiManagerParameter pHost("host", "MQTT broker IP or name", mqttHost.c_str(), 40);
WiFiManagerParameter pPort("port", "MQTT port", mqttPort.c_str(), 6);
WiFiManagerParameter pUser("user", "MQTT username", mqttUser.c_str(), 32);
wm.addParameter(&pHost);
wm.addParameter(&pPort);
wm.addParameter(&pUser);
wm.setSaveConfigCallback(saveConfigCallback);
wm.setConfigPortalTimeout(180); // give up after 3 minutes, then restart
wm.setConnectTimeout(20); // seconds to try saved Wi-Fi
digitalWrite(LED_PIN, HIGH); // LED on while not connected
// Second argument is the portal's own password (min 8 characters)
if (!wm.autoConnect("RoomNode-Setup", "setup1234")) {
Serial.println("No Wi-Fi configured, restarting");
ESP.restart();
}
digitalWrite(LED_PIN, LOW);
if (shouldSave) { // copy portal values into flash
mqttHost = pHost.getValue();
mqttPort = pPort.getValue();
mqttUser = pUser.getValue();
storeSettings();
Serial.println("Settings saved");
}
Serial.println("Connected to " + WiFi.SSID() + ", IP " + WiFi.localIP().toString());
Serial.println("MQTT broker: " + mqttHost + ":" + mqttPort + " user " + mqttUser);
// ... set up PubSubClient here with mqttHost and mqttPort.toInt() ...
}
void loop() {
// Hold BOOT for 5 seconds to wipe Wi-Fi and settings
static uint32_t pressedAt = 0;
if (digitalRead(BUTTON_PIN) == LOW) {
if (pressedAt == 0) pressedAt = millis();
if (millis() - pressedAt > 5000) {
Serial.println("Factory reset");
WiFiManager wm;
wm.resetSettings(); // erase stored Wi-Fi credentials
prefs.begin("cfg", false);
prefs.clear(); // erase MQTT settings
prefs.end();
delay(500);
ESP.restart();
}
} else {
pressedAt = 0;
}
// ... normal application code ...
}
Walking Through the Code
WiFiManagerParameterobjects add text boxes to the portal. The default value comes from flash, so a second visit to the portal shows the current settings rather than blanks.setSaveConfigCallbacktells you that the user actually submitted the form. Without this check you would rewrite flash on every boot for no reason, which wears it out.autoConnect(name, password)blocks until connected or until the portal timeout. A portal password stops neighbours from reconfiguring your device; leave it out only on a bench.setConfigPortalTimeout(180)matters after a power cut: if the router boots slower than the ESP32, the node would otherwise sit in portal mode forever. With the timeout it restarts and tries the saved network again.- Factory reset uses a long press so an accidental tap does not wipe the device.
resetSettings()erases the Wi-Fi credentials andprefs.clear()removes everything in the namespace.
Using the saved settings
After setup() finishes, the strings hold the broker details. Pass them to PubSubClient with mqtt.setServer(mqttHost.c_str(), mqttPort.toInt()). Because setServer keeps a pointer to the host string rather than copying it, mqttHost must stay in memory for the life of the program, which is why it is a global variable here and not a local one.
What the user sees
- Power on a new device. The status LED stays on and a Wi-Fi network named RoomNode-Setup appears on the phone.
- Join it with the password setup1234. The configuration page opens.
- Tap Configure WiFi, choose the home network, enter its password and the MQTT fields, then tap Save.
- The access point disappears, the LED turns off, and the Serial Monitor prints the IP address and broker settings.
- On later boots the device connects straight to the saved network without showing the portal.
Troubleshooting
- Portal page does not open automatically: some phones keep mobile data active. Turn off mobile data or browse to 192.168.4.1 manually.
- Network not listed: the ESP32 supports only 2.4 GHz Wi-Fi. A 5 GHz-only network will never appear.
- Saved but never connects: a typo in the password, or the network needs an enterprise (username plus password) login or a web login page, which this method does not handle.
- Custom values lost after reboot: they were read from the parameters but not written to Preferences, or the namespace name differs between load and save.
- Boot loops on GPIO 0: holding BOOT during reset puts the ESP32 into download mode; press the button only after the sketch is running.
Summary
WiFiManager turns credential setup into a phone-based captive portal. Custom parameters collect application settings, Preferences keeps them in NVS flash, a portal timeout avoids getting stuck after power cuts, and a long-press reset lets users start over. Your devices can now be shipped to any network without recompiling.