To deploy an ESP32 hotspot web server in the field without relying on a local router, you must configure the microcontroller in SoftAP (Software Access Point) mode. This forces the ESP32 to broadcast its own WiFi network, assign IP addresses via its internal DHCP server, and host a web interface directly on 192.168.4.1. This approach is mandatory for remote telemetry, off-grid solar monitoring, and agricultural sensor nodes where infrastructure WiFi is absent or unreliable.
The Offline Decision Matrix: Why SoftAP?
Before wiring relays and writing firmware, confirm that a local hotspot is actually the correct architecture for your deployment. Use this decision path to lock in your network topology.
| Requirement | Architecture | Verdict |
|---|---|---|
| Needs cloud logging / remote access over internet | Station (STA) Mode | Reject (Requires external router) |
| Needs ultra-low power, no browser UI, just data bursts | ESP-NOW | Reject (Requires dedicated receiver) |
| Needs local browser UI, single client, no internet | SoftAP (Hotspot) | SELECT THIS |
Hardware BOM & Pin Mapping
This build targets a specific, widely available board variant to ensure the pin definitions in the code below map correctly to your silicon. Do not substitute an ESP32-S2 or ESP32-C3 without adjusting the GPIO assignments, as their internal routing differs.
Target Board & Components
- Microcontroller: ESP32-WROOM-32E DevKitC V4 (38-pin layout, 4MB Flash). Avoid the older 30-pin V1 clones; the V4 integrates the CP2102N USB-UART bridge which handles auto-bootloader entry reliably.
- Actuator: 5V 1-Channel Relay Module with optocoupler isolation (e.g., Songle SRD-05VDC-SL-C).
- Power: 5V 2A USB-C power supply (SoftAP mode draws ~160mA average with 240mA TX spikes; a standard 500mA USB port will brownout the board during WiFi transmission).
Pin Mapping Table
| Component | Module Pin | ESP32-WROOM-32E GPIO | Notes |
|---|---|---|---|
| Relay IN | Signal | GPIO 16 | Safe boot pin. Active LOW on most optocoupler modules. |
| Relay VCC | VCC | VIN (5V) | Do not power 5V relay coils from the 3V3 pin. |
| Relay GND | GND | GND | Common ground required. |
| Status LED | Anode | GPIO 2 | Onboard blue LED. Active HIGH. |
Firmware: Compilable SoftAP Web Server Code
The following C++ code is written for the Arduino IDE using the official Espressif Arduino Core (v3.0.x or newer). It includes explicit pin definitions, a captive-portal-friendly HTML payload, and hardware-level error handling if the RF subsystem fails to initialize.
esp32 by Espressif Systems. Select ESP32 Dev Module as your target board. Set Flash Size to 4MB and Partition Scheme to Default 4MB with spiffs.
#include <WiFi.h>
#include <WebServer.h>
// --- PIN DEFINITIONS ---
#define RELAY_PIN 16
#define STATUS_LED 2
// --- NETWORK CREDENTIALS ---
// WPA2 requires a minimum of 8 characters. Shorter strings will fail silently.
const char* ssid = "FluxField_AP";
const char* password = "flux123456";
WebServer server(80);
const char* html_page = "<!DOCTYPE html><html><head>"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
"<style>body{font-family:sans-serif;text-align:center;padding:20px;}"
".btn{padding:15px 30px;font-size:18px;margin:10px;border:none;border-radius:8px;color:white;cursor:pointer;}"
".on{background:#4CAF50;} .off{background:#f44336;}</style></head>"
"<body><h2>Flux Field Controller</h2>"
"<button class=\"btn on\" onclick=\"location.href='/relay?state=1'\">RELAY ON</button>"
"<button class=\"btn off\" onclick=\"location.href='/relay?state=0'\">RELAY OFF</button>"
"</body></html>";
void handleRoot() {
server.send(200, "text/html", html_page);
}
void handleRelay() {
if (server.hasArg("state")) {
String state = server.arg("state");
if (state == "1") {
digitalWrite(RELAY_PIN, LOW); // Active LOW for most optocoupler relays
digitalWrite(STATUS_LED, HIGH);
} else {
digitalWrite(RELAY_PIN, HIGH);
digitalWrite(STATUS_LED, LOW);
}
}
server.sendHeader("Location", "/");
server.send(303);
}
void setup() {
pinMode(RELAY_PIN, OUTPUT);
pinMode(STATUS_LED, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // Default OFF
digitalWrite(STATUS_LED, LOW);
Serial.begin(115200);
delay(1000);
Serial.println("\n[BOOT] Initializing ESP32 Hotspot Web Server...");
// Disable Station mode to save power and prevent rogue scanning
WiFi.mode(WIFI_AP);
// Start SoftAP with error handling
if (!WiFi.softAP(ssid, password)) {
Serial.println("[ERROR] SoftAP creation failed! Check password length (min 8 chars).");
// Blink LED rapidly to indicate hardware/RF failure
while(1) {
digitalWrite(STATUS_LED, !digitalRead(STATUS_LED));
delay(100);
}
}
Serial.print("[OK] AP IP Address: ");
Serial.println(WiFi.softAPIP()); // Default is 192.168.4.1
server.on("/", handleRoot);
server.on("/relay", handleRelay);
server.begin();
Serial.println("[OK] HTTP Server started.");
}
void loop() {
server.handleClient();
// Yield to WiFi stack to prevent watchdog resets under heavy load
delay(2);
}
Debugging: When the Hotspot Fails
The ESP32 WiFi stack is notoriously sensitive to configuration errors and power delivery issues. If your phone cannot see the network, or the serial monitor throws errors, follow this diagnostic sequence.
The First 3 Things to Check
- Password Length Constraint: WPA2-PSK mandates a minimum of 8 ASCII characters. If your
passwordstring is 7 characters or fewer,WiFi.softAP()will returnfalseand the radio will remain silent. - Phone Captive Portal Hijacking: Modern iOS and Android devices detect that the ESP32 has no internet backhaul. They will intercept your HTTP requests and redirect you to a "No Internet" captive portal screen. Fix: Turn off Mobile Data on your phone, or tap "Keep connected to this network" in the WiFi settings.
- Power Supply Brownouts: SoftAP mode requires sustained RF transmission for beacon frames. If powered by a weak laptop USB port, the 3.3V LDO on the DevKit will droop during TX spikes, resetting the chip. Use a dedicated 5V 2A wall adapter.
Exact Error Strings & Ranked Causes
If the serial monitor outputs errors during boot, match the exact string to the solution below.
| Exact Serial Error String | Ranked Causes | Fix |
|---|---|---|
E (xxx) wifi: esp_wifi_start 1458: wifi not start |
1. RF calibration failed. 2. Antenna keep-out zone violated (metal enclosure). 3. Corrupted NVS partition. |
Move board away from metal. In Arduino IDE, select Erase All Flash Before Sketch Upload to clear corrupted NVS WiFi calibration data. |
Soft AP IP: 0.0.0.0 |
1. Password < 8 chars. 2. SSID contains unsupported UTF-8 characters. 3. WiFi.mode() omitted. |
Verify password length. Ensure WiFi.mode(WIFI_AP) is called before softAP(). |
Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout) |
1. Blocking code in loop().2. Missing delay() or yield() in web server loop. |
Add delay(2); at the end of loop() to feed the WiFi task. Remove heavy delay() calls inside HTTP handlers. |
For deeper architectural insights into how the ESP32 manages concurrent Station and SoftAP interfaces, consult the official Espressif WiFi Driver API Guide, specifically the section on RF calibration and coexistence.
Extending or Simplifying the Build
Once the baseline hotspot is broadcasting and the relay is clicking, you will inevitably need to adapt the firmware for production. Here is how to scale the build in either direction.
How to Simplify (Lower Power & Cost)
- Drop the Web Server: If you only need to toggle a pin and don't care about a visual UI, strip out
WebServer.hentirely. Use the ESP32 BLE Library to expose a simple Bluetooth Low Energy GATT characteristic. This cuts active current draw from ~160mA (WiFi) to ~8mA (BLE advertising), allowing months of runtime on a 18650 Li-ion cell. - Use Deep Sleep: If the relay only needs to pulse once an hour, configure the ESP32 to wake via the internal RTC timer, toggle the GPIO, and immediately return to deep sleep (10μA).
How to Extend (Add Sensors & Persistence)
- Inject Sensor Data: To display live telemetry (e.g., BME280 temperature/humidity) on the web page, do not use string concatenation in the HTTP handler. Instead, use a global
Stringbuffer updated in theloop(), or migrate toAsyncWebServer(from theESPAsyncWebServerlibrary) to prevent the main thread from blocking during I2C reads. - Save State Across Reboots: The current code defaults the relay to OFF on boot. To remember the last state, write the boolean state to the ESP32's Preferences library (NVS flash) every time the
/relayendpoint is hit, and read it back insetup(). - Custom IP Subnets: By default, the ESP32 assigns itself
192.168.4.1. If this conflicts with your phone's mobile data subnet, useWiFi.softAPConfig(local_ip, gateway, subnet)before callingWiFi.softAP()to force a custom range like10.10.10.1.






