The Verdict: Which ESP8266 Web Config Approach Wins?
Hardcoding WiFi SSIDs and MQTT broker addresses into your ESP8266 sketch is a maintenance nightmare the moment you move a prototype from your bench to a new network. You need an esp8266 web config portal. But with multiple libraries available, which one should you actually use?
| Your Project Need | Library / Approach | Verdict |
|---|---|---|
| WiFi credentials only (no custom app params) | tzapu/WiFiManager |
Overkill but reliable. |
| WiFi + Custom Params (MQTT host, polling rates, device IDs) | tzapu/WiFiManager + ArduinoJson |
WINNER. Best balance of UX and flexibility. |
| Complex multi-page dashboards with live data charts | ESPAsyncWebServer + LittleFS |
Pick this only if building a full UI. |
| Enterprise OTA with strict mDNS and secure provisioning | IotWebConf |
Too heavy for standard hobbyist/Prosumer IoT. |
Hardware Spec Sheet and Pin Mapping
This build targets the NodeMCU v3 LoLin (ESP-12E module). While the Wemos D1 Mini is great for tight spaces, the NodeMCU v3 features an onboard CP2102 or CH340 USB-to-UART bridge and a built-in 3.3V LDO regulator, making it the superior choice for bench prototyping where you need to monitor serial debug output while triggering the config portal.
| Component | Exact Variant / Spec | Estimated Cost |
|---|---|---|
| Microcontroller | NodeMCU v3 LoLin (ESP-12E, 4MB Flash) | $4.50 - $6.00 |
| Power Supply | 5V 2A USB Micro Adapter (Crucial for TX spikes) | $5.00 |
| Config Trigger | 6x6mm Tactile Pushbutton | $0.10 |
| Status Indicator | 5mm LED + 330Ω Resistor | $0.05 |
Pin Mapping Table
| NodeMCU Label | GPIO Number | Function in this Build |
|---|---|---|
| D2 | GPIO4 | Config Portal Trigger Button (Active LOW) |
| D4 | GPIO2 | Onboard LED (WiFi Status Indicator) |
| D1 | GPIO5 | External Load / Relay Control |
| 3V3 | N/A | Button Pull-up Power (if not using internal) |
| GND | N/A | Common Ground |
Step-by-Step: Setting Up the Config Portal
- Install Core Libraries: In the Arduino IDE Library Manager, install
WiFiManagerby tzapu (v2.0.17+) andArduinoJsonby Benoit Blanchon (v6.21.0+). - Configure Flash Partition: Go to Tools > Flash Size and select 4MB (FS:2MB OTA:~1019KB). This allocates 2MB for LittleFS, which is mandatory for saving custom JSON parameters persistently.
- Wire the Trigger: Connect one leg of the tactile button to D2 (GPIO4) and the other to GND. We will use the ESP8266's internal pull-up resistor in code to save a physical resistor.
- Define Custom Parameters: Create character arrays for your custom fields (e.g., MQTT Server IP) and register them with the WiFiManager instance before calling
startConfigPortal().
The Complete Compilable Code
This sketch implements a physical button trigger to launch the captive portal, saves custom MQTT parameters to LittleFS using ArduinoJson, and includes robust error handling for filesystem operations.
// Target Board: NodeMCU 1.0 (ESP-12E Module)
// Flash Size: 4MB (FS:2MB OTA:~1019KB)
#include <ESP8266WiFi.h>
#include <WiFiManager.h>
#include <ArduinoJson.h>
#include <LittleFS.h>
// --- PIN DEFINITIONS ---
#define PIN_CONFIG_BUTTON D2 // GPIO4 (Active LOW)
#define PIN_STATUS_LED D4 // GPIO2 (Built-in LED, Active LOW)
#define PIN_LOAD_RELAY D1 // GPIO5
// --- CUSTOM PARAMETER DEFAULTS ---
char mqtt_server[40] = "192.168.1.100";
char mqtt_port[6] = "1883";
char device_id[20] = "esp-sensor-01";
// Flag for saving data
bool shouldSaveConfig = false;
void saveConfigCallback () {
Serial.println("\n[CALLBACK] saveConfigCallback triggered.");
shouldSaveConfig = true;
}
void setupFileSystem() {
if (!LittleFS.begin()) {
Serial.println("[ERROR] Failed to mount LittleFS. Formatting...");
LittleFS.format();
LittleFS.begin();
}
}
bool loadConfig() {
File configFile = LittleFS.open("/config.json", "r");
if (!configFile) {
Serial.println("[WARN] No config file found. Using defaults.");
return false;
}
size_t size = configFile.size();
std::unique_ptr<char[]> buf(new char[size]);
configFile.readBytes(buf.get(), size);
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, buf.get());
if (error) {
Serial.print("[ERROR] deserializeJson() failed: ");
Serial.println(error.c_str());
return false;
}
strlcpy(mqtt_server, doc["mqtt_server"] | "192.168.1.100", sizeof(mqtt_server));
strlcpy(mqtt_port, doc["mqtt_port"] | "1883", sizeof(mqtt_port));
strlcpy(device_id, doc["device_id"] | "esp-sensor-01", sizeof(device_id));
Serial.printf("[INFO] Loaded Config -> MQTT: %s:%s, ID: %s\n", mqtt_server, mqtt_port, device_id);
return true;
}
void saveConfig() {
DynamicJsonDocument doc(1024);
doc["mqtt_server"] = mqtt_server;
doc["mqtt_port"] = mqtt_port;
doc["device_id"] = device_id;
File configFile = LittleFS.open("/config.json", "w");
if (!configFile) {
Serial.println("[ERROR] Failed to open config file for writing.");
return;
}
serializeJson(doc, configFile);
configFile.close();
Serial.println("[INFO] Configuration saved to LittleFS.");
}
void setup() {
Serial.begin(115200);
delay(100);
pinMode(PIN_CONFIG_BUTTON, INPUT_PULLUP);
pinMode(PIN_STATUS_LED, OUTPUT);
pinMode(PIN_LOAD_RELAY, OUTPUT);
digitalWrite(PIN_STATUS_LED, LOW); // Turn ON LED during setup
setupFileSystem();
loadConfig();
WiFiManager wm;
wm.setSaveConfigCallback(saveConfigCallback);
// Add Custom Parameters to Portal
WiFiManagerParameter custom_mqtt_server("server", "MQTT Server IP", mqtt_server, 40);
WiFiManagerParameter custom_mqtt_port("port", "MQTT Port", mqtt_port, 6);
WiFiManagerParameter custom_device_id("dev_id", "Device ID", device_id, 20);
wm.addParameter(&custom_mqtt_server);
wm.addParameter(&custom_mqtt_port);
wm.addParameter(&custom_device_id);
// AutoConnect attempts to connect to saved WiFi. If it fails, it starts the AP.
if (!wm.autoConnect("ESP8266-Config-Portal", "configpass123")) {
Serial.println("[ERROR] Failed to connect and hit timeout. Restarting.");
ESP.restart();
}
// If we get here, we are connected to WiFi
if (shouldSaveConfig) {
strlcpy(mqtt_server, custom_mqtt_server.getValue(), sizeof(mqtt_server));
strlcpy(mqtt_port, custom_mqtt_port.getValue(), sizeof(mqtt_port));
strlcpy(device_id, custom_device_id.getValue(), sizeof(device_id));
saveConfig();
}
digitalWrite(PIN_STATUS_LED, HIGH); // Turn OFF LED (Connected)
Serial.printf("[INFO] Connected to WiFi. IP: %s\n", WiFi.localIP().toString().c_str());
}
void loop() {
// Physical button press to force Config Portal
if (digitalRead(PIN_CONFIG_BUTTON) == LOW) {
delay(50); // Debounce
if (digitalRead(PIN_CONFIG_BUTTON) == LOW) {
Serial.println("[ACTION] Button held. Forcing Config Portal...");
digitalWrite(PIN_STATUS_LED, LOW);
WiFiManager wm;
wm.startConfigPortal("ESP8266-Force-Config", "configpass123");
// After portal closes, restart to apply new settings cleanly
ESP.restart();
}
}
// Main application logic goes here
// e.g., Read sensors, publish to MQTT using mqtt_server and mqtt_port
delay(10);
}
Troubleshooting: Exact Errors and the First 3 Checks
When your ESP8266 web config portal fails to launch or crashes immediately after saving, run through these diagnostics.
The First 3 Things to Check When It Fails
- Power Supply Ripple: The ESP8266 draws 300mA+ peak currents during WiFi transmission bursts. If you are powering the NodeMCU from a standard PC USB port (limited to 500mA) while also driving sensors, the voltage will sag below 3.0V, causing a brownout reset. Fix: Use a dedicated 5V 2A wall adapter. See the Espressif ESP8266 Hardware Design Guidelines for exact decoupling capacitor requirements.
- Flash Partition Mismatch: If LittleFS fails to mount, you likely forgot to set the partition scheme. Fix: Verify Tools > Flash Size is set to 4MB (FS:2MB OTA:~1019KB).
- Captive Portal DNS Interception: Modern iOS and Android devices will detect that the ESP8266 AP has no internet access and may block the portal redirect or show a "No Internet" warning that hides the UI. Fix: Tap the network, select "Use Without Internet", and manually navigate to
192.168.4.1if the popup doesn't trigger.
Ranked Causes for Exact Error Strings
| Exact Serial Error String | Root Cause | Solution |
|---|---|---|
ets Jan 8 2013,rst cause:4, boot mode:(3,6) |
Hardware Watchdog Timer (WDT) reset. Your code blocked the main thread for >3.2 seconds, likely during a heavy LittleFS write or a blocking HTTP request inside a callback. | Add yield(); or delay(1); inside long loops. Ensure saveConfigCallback only sets a boolean flag and does not perform file I/O directly. |
Fatal exception 28(LoadProhibitedCause) |
Null pointer or unaligned memory access. Usually triggered when deserializeJson fails silently and you attempt to read from an empty JSON document. |
Always check the DeserializationError return code before accessing JSON keys, as demonstrated in the ArduinoJson Deserialization Documentation. |
wm:[ERROR] WiFiManager config file open failed |
Internal WiFiManager debug flag showing it cannot write to SPIFFS/LittleFS because the filesystem partition is missing or unformatted. | Run LittleFS.format() once via code, or ensure the correct Flash Size partition is selected in the Arduino IDE before uploading. |
Extending and Simplifying Your Build
Once the baseline esp8266 web config portal is stable, you will inevitably need to adapt it to your specific hardware constraints.
How to Extend (Adding Advanced Features)
- Custom HTML Snippets: Use
wm.setCustomHeadElement("<style>body{background:#222;color:#fff;}</style>")to inject dark mode CSS directly into the captive portal header. - Read-Only Parameters: If you want to display the device's current MAC address or firmware version on the config page without letting the user edit it, use the
WiFiManagerParameterconstructor that accepts acustom HTMLstring, wrapping the value in a<p>tag instead of an<input>field. - Drop-Down Menus: WiFiManager doesn't natively support
<select>dropdowns easily. To implement them, pass raw HTML strings into the custom parameter definition:"<select name='region'><option value='US'>US</option><option value='EU'>EU</option></select>".
How to Simplify (Stripping it Down)
If you are building a simple smart plug and only need WiFi credentials (no MQTT or custom device IDs), delete the ArduinoJson and LittleFS dependencies entirely. Remove the custom parameter declarations, remove the saveConfigCallback, and rely solely on wm.autoConnect(). WiFiManager will automatically store the SSID and Password in the ESP8266's non-volatile RTC/EEPROM memory, saving you roughly 15% of flash space and eliminating filesystem corruption risks.
Flash the provided sketch to your NodeMCU v3, wire the D2 button to ground, and power it up. The LED will illuminate while searching for a network, drop you into the 192.168.4.1 portal on your phone, and persist your exact MQTT configuration across power cycles. No hardcoded credentials, no serial monitor required.






