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
Default Recommendation: If you are building a standalone field device that a technician will configure or monitor via a smartphone browser on-site, terminate your decision here and use SoftAP Mode.

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.

Board Manager Setup: In Arduino IDE, go to Board Manager and install 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

  1. Password Length Constraint: WPA2-PSK mandates a minimum of 8 ASCII characters. If your password string is 7 characters or fewer, WiFi.softAP() will return false and the radio will remain silent.
  2. 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.
  3. 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.h entirely. 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 String buffer updated in the loop(), or migrate to AsyncWebServer (from the ESPAsyncWebServer library) 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 /relay endpoint is hit, and read it back in setup().
  • Custom IP Subnets: By default, the ESP32 assigns itself 192.168.4.1. If this conflicts with your phone's mobile data subnet, use WiFi.softAPConfig(local_ip, gateway, subnet) before calling WiFi.softAP() to force a custom range like 10.10.10.1.
Final Bench Tip: Always test your ESP32 hotspot web server with the USB cable disconnected, running off a 5V power bank. The USB data lines on some PC hubs introduce ground loops that detune the ESP32's PCB trace antenna, artificially reducing your SoftAP range from 30 meters to under 5 meters. Powering it standalone gives you a true baseline of the RF performance.