Build Project

IoT Projects

ESP32 Browser LED On and Off Control

Control a resistor-limited LED from a local ESP32 web form, with POST validation, startup OFF and a best-effort command timeout.

IntermediateParent Safe
ESP32 Browser LED On and Off Control logical illustration, not a photograph or verified physical pinout
Jump to a section

What You Will Build

A phone-controlled LED with explicit On and Off buttons. The ESP32 creates its own network, accepts a validated form submission and shows the last commanded state. This is the first actuator example after the existing read-only AP server, not a replacement for it.

Before You Start

Classic ESP32-WROOM DevKit, Arduino IDE 2.x and Arduino-ESP32 3.3.2 as the API review baseline, not the latest-release claim. Select ESP32 Dev Module only for the stated generic WROOM board. Other ESP32 families need their own GPIO and LEDC review. Complete the Serial-only IDE upload first. Documentation/code-structure and local browser checks do not establish compilation or physical operation; these sketches have not been compiled or hardware-tested. Use one low-current red indicator LED such as the documented WP7113ID, with 470 Ω in series. Do not substitute high-power LEDs, relay coils or LED strips.

Parts

Wiring

GPIO25 drives a series path through 470 Ω to the LED anode; the cathode returns to GND. Resistor placement on either side of the LED is electrically equivalent if it remains in series. Check polarity using the actual datasheet or power-off diode test. No built-in GPIO2 LED is assumed.

ESP32 Browser LED On and Off Control logical signal and power map; not physical pin order

Open wiring diagram full size

  1. 1

    Disconnect USB and identify LED polarity.

  2. 2

    Wire GPIO25 through the 470 Ω resistor to the anode, and cathode to GND.

  3. 3

    Check that the resistor is in series rather than bypassed by a breadboard row.

  4. 4

    Reconnect USB, upload the sketch and read Serial at 115200.

Wiring Reference

SignalESP32 PinDirectionNotes
470 Ω resistor → LED anodeGPIO25OutputSeries path; LED cathode returns to GND
LED cathodeGNDGroundVerify actual LED polarity
DevKit powerUSB data cablePowerNo onboard LED or 5 V signal assumed

Libraries and Setup

  • Built-in WiFi and WebServer from Arduino-ESP32 3.3.2; no additional Arduino library.

Configure the Example

Change CHANGE_ME_1234 to your own 8–63 character AP password. Upload and open Serial at 115200. Join the printed ESP32 network, stay connected if your phone reports no internet, and open the printed http:// address directly. The classic board uses a 2.4 GHz AP on channel 6, allows one connected station and does not join your router. No internet, external JavaScript, cloud account or third-party Arduino library is required. Press Turn on or Turn off. Only those POST submissions change the output; opening or refreshing the page does not. The page does not poll automatically, so refresh after an expiry to see the latest software state.

Control and Recovery Behavior

ConditionResult
Startup / AP failureGPIO25 is set LOW before AP startup; a failed AP leaves the LED OFF.
Valid POST /ledExactly one value argument, 0 or 1, changes the LED and returns a 303 redirect to the page.
Invalid requestHTTP 400 for missing, duplicate/extra or unsupported values; an unknown or wrong-method route returns 404.
Idle ON commandThe loop switches OFF after 30 seconds since the last accepted command; GET does not extend this window.

Code

Install the libraries above and match the configuration and wiring before uploading.

web-led.ino
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

WebServer server(80);
const char* AP_PASSWORD = "CHANGE_ME_1234"; // Replace with your own 8-63 character password.
bool httpReady = false;

void redirectHome() {
  server.sendHeader("Location", "/");
  server.send(303, "text/plain", "Open the control page\n");
}
String pageStart(const char* title) {
  String page = "<!doctype html><html lang='en'><head>";
  page += "<meta name='viewport' content='width=device-width,initial-scale=1'>";
  page += "<style>body{font:1.1rem system-ui;max-width:40rem;margin:2rem auto;padding:1rem}button,input{font:inherit;margin:.5rem 0}input[type=range]{width:100%}</style>";
  page += "<title>"; page += title; page += "</title></head><body><h1>";
  page += title; page += "</h1>";
  return page;
}
void sendPage(const String& page) {
  server.sendHeader("Cache-Control", "no-store");
  server.send(200, "text/html; charset=utf-8", page + "</body></html>");
}
void startHTTP(const char* name) {
  server.onNotFound([]() { server.send(404, "text/plain", "No such route\n"); });
  if (!WiFi.mode(WIFI_AP) || !WiFi.softAP(name, AP_PASSWORD, 6, false, 1)) {
    Serial.println("AP failed; output stays off. Check board and password length.");
    return;
  }
  server.begin(); httpReady = true;
  Serial.print("Join "); Serial.println(name);
  Serial.print("Open http://"); Serial.println(WiFi.softAPIP());
}

constexpr uint8_t LED_PIN = 25;
constexpr uint32_t COMMAND_LIFETIME_MS = 30000;
bool ledOn = false;
uint32_t lastCommand = 0;

void showPage() {
  String page = pageStart("ESP32 web LED");
  page += String("<p>Commanded output: ") + (ledOn ? "ON" : "OFF") + "</p>";
  page += "<form method='post' action='/led'><button name='value' value='1'>Turn on</button> ";
  page += "<button name='value' value='0'>Turn off</button></form>";
  page += "<p>ON expires 30 seconds after the last accepted command. Refresh to read the current software state.</p>";
  sendPage(page);
}
void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW);
  server.on("/", HTTP_GET, showPage);
  server.on("/led", HTTP_POST, []() {
    if (server.args() != 1 || !server.hasArg("value") ||
        (server.arg("value") != "0" && server.arg("value") != "1")) {
      server.send(400, "text/plain", "Exactly one value, 0 or 1, is required\n"); return;
    }
    ledOn = server.arg("value") == "1";
    digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
    lastCommand = millis();
    Serial.println(ledOn ? "LED commanded ON" : "LED commanded OFF");
    redirectHome();
  });
  startHTTP("ESP32-WebLED");
}
void loop() {
  if (httpReady) server.handleClient();
  if (ledOn && millis() - lastCommand >= COMMAND_LIFETIME_MS) {
    ledOn = false; digitalWrite(LED_PIN, LOW);
    Serial.println("Command expired: LED OFF");
  }
  delay(1);
}

How the Code Works

GET / renders the page without changing the LED. The two submit buttons send one value field to POST /led. The handler validates the argument count and exact string before touching GPIO25. Post/Redirect/Get avoids an ordinary page refresh repeating a prior form command. digitalWrite and the displayed ledOn flag describe the software command; neither measures actual light output. Unsigned elapsed-time subtraction handles normal millis wrap for this short timeout.

Expected Output

With the supported circuit, Turn on should illuminate the LED and redirect to a page saying ON. Turn off should extinguish it. Leaving an ON command idle should print Command expired and switch it off near 30 seconds; refresh to see OFF. These are expected bench observations, not measurements recorded during authoring.

Troubleshooting

  • Cannot open the page Confirm the phone is still on the ESP32 AP, use the Serial-printed http:// address and check mobile-data/VPN routing. The AP deliberately provides no internet.
  • HTTP 400 from a command Use the provided form. Exact argument counts and allowed values are checked; blank, negative, decimal or nonnumeric values are not converted silently.
  • Page shows an old value Refresh the page. It reports the last accepted software command, not measured light output or shaft position. GET/refresh does not renew the command lifetime.
  • Software says ON but LED is dark Unplug USB, verify polarity, GPIO25, common ground and the resistor/LED series path. A command flag does not diagnose a reversed or broken LED.
  • LED never switches off exactly at the chosen instant This synchronous server can temporarily block loop servicing. The timeout is approximate cleanup, not a deterministic shutdown deadline.

Checks to Perform on Your Build

  • Compare both form buttons with the LED and Serial output.
  • Refresh without pressing a control and confirm it does not renew an ON command.
  • Send a malformed command only on your bench: it should return 400 without changing output.
  • Reset and verify startup OFF before repeating the test.

Practical Design Notes

At 3.3 V a 470 Ω resistor limits even a shorted LED path to about 7 mA; the actual LED current is lower and depends on forward voltage. This conservative resistor choice is not a universal GPIO current rating. Use the detailed Blink/component material for LED basics rather than removing current limiting. This is plain HTTP on a trusted local demonstration AP. The Wi-Fi password is network admission, not per-user HTTP authentication. Anyone admitted can issue commands; POST alone does not prevent CSRF, and the sketch has no TLS, CSRF token, rate limiting or user roles. Do not expose the port through a router or adapt this example to locks, heaters, mains equipment or safety-critical mechanisms. The synchronous WebServer can wait on slow clients; the loop timeout is best-effort cleanup, not a hard real-time or certified safety stop.

Review, Testing, and References

Author: Abdul Mubeen and the ESP32 Engine editorial team. Last updated: 2026-10-24. Reviewed: wiring logic, Arduino code structure, beginner safety, and learning sequence.

Educational level: Intermediate. Estimated completion time: Varies by build. This project is for learning and prototyping; production or unattended hardware needs additional engineering review.