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

  1. On boot, WiFiManager tries the Wi-Fi credentials stored by the ESP32's Wi-Fi driver in flash.
  2. If they work, autoConnect() returns true within a few seconds and your sketch carries on.
  3. 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.
  4. A phone that joins this access point usually opens the configuration page automatically (the captive portal). If not, browse to 192.168.4.1.
  5. The user selects a network, enters the password and any custom fields, and taps Save.
  6. 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

ItemDetails
BoardAny ESP32 DevKit
Reset buttonThe on-board BOOT button on GPIO 0 (active LOW), or a push button from a GPIO to GND
Status LEDOn-board LED, GPIO 2 on many DevKit V1 boards (check your board)
LibraryWiFiManager by tzapu, version 2.x, from the Library Manager
Built-in libraryPreferences (part of the ESP32 Arduino core)

Complete Sketch

WiFiManager with custom MQTT fields, Preferences storage and a long-press reset
#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

  • WiFiManagerParameter objects 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.
  • setSaveConfigCallback tells 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 and prefs.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

  1. Power on a new device. The status LED stays on and a Wi-Fi network named RoomNode-Setup appears on the phone.
  2. Join it with the password setup1234. The configuration page opens.
  3. Tap Configure WiFi, choose the home network, enter its password and the MQTT fields, then tap Save.
  4. The access point disappears, the LED turns off, and the Serial Monitor prints the IP address and broker settings.
  5. 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.