ESP32/Web server/53. Switching a pin from the page
Your chip
Your language
Web server · 53 of 81

Switching a pin from the page

A checkbox on a phone that turns on an LED across the room is the moment this stops feeling like a tutorial. It also introduces the bug every one of these projects has - the page believes whatever it last heard, and the pin is the only thing that actually knows.

/esp32/web-server-led-control · arduino · S3

Click the checkbox, then make the reply go missing and click it again.

checkbox → /led?on=… → digitalWrite
in step
The network
The pin
LOW
The checkbox
clear
Agree?
yes
The checkbox is a picture of the pin. It is accurate while every request lands. Set the network to lose one and watch the two separate — then use the button, which is the one line the sketch runs on page load so that a reload starts from the truth rather than from a default.

The page is never the source of truth

The checkbox is a drawing of the pin. It is accurate for as long as nothing goes wrong, and the two things that go wrong are common: a reply that never arrives, and a second phone.

The fix is a contract, and it costs one line:

  • The board answers with the state of the pin, read back after the write.
  • The page draws what came back, not what was clicked.
  • The page asks once on load, so a reload or a reconnect starts from the truth rather than from a default.

Do that and a dropped reply leaves the checkbox visibly wrong for one second instead of invisibly wrong forever.

GET is fine here, and it is a habit worth breaking later

A browser is allowed to fetch a GET twice, and a link preview or a prefetch will do exactly that. Toggling an LED that way is harmless. Anything that opens a door or spends money should be a POST, and the reason is not tidiness — it is that nothing in the network is allowed to repeat a POST on its own.

Where this stops scaling

One click is one connection, and each connection is a handshake. That is fine for a switch and it is not fine for a slider you drag. When the board needs to push state to several viewers at once, one persistent connection replaces all of it: WebSocket dashboards.

On your S3
ChipXtensa LX7 · 2 × 240 MHz
Board settingESP32S3 Dev Module
Default I2CSDA 8 · SCL 9
Watch out forThe port vanishes after upload

The code

One route serves the page, one takes the command, and both answer with the state the pin is in afterwards. The page never assumes it got what it asked for - it prints what came back.

led_control.ino
#include <WiFi.h>
#include <WebServer.h>

const int LED = 23;                // any output pin
WebServer server(80);

const char PAGE[] PROGMEM = R"html(
<!doctype html><meta charset=utf-8>
<h1>LED</h1>
<input type=checkbox id=c onchange=set()>
<span id=s>unknown</span>
<script>
const send = url => fetch(url).then(r => r.json()).then(j => {
  c.checked = j.on;                // the pin decides, not the click
  s.textContent = j.on ? 'on' : 'off';
});
const set = () => send('/led?on=' + c.checked);
send('/led');                      // ask on load, so a reload tells the truth
</script>
)html";

void reply() {
  bool on = digitalRead(LED);
  server.sendHeader("Cache-Control", "no-store");
  server.send(200, "application/json", on ? "{\"on\":true}" : "{\"on\":false}");
}

void setup() {
  Serial.begin(115200);
  pinMode(LED, OUTPUT);
  WiFi.begin("your-network", "your-password");
  while (WiFi.status() != WL_CONNECTED) delay(250);
  Serial.println(WiFi.localIP());

  server.on("/", HTTP_GET, [] { server.send_P(200, "text/html", PAGE); });

  server.on("/led", HTTP_GET, [] {
    if (server.hasArg("on")) digitalWrite(LED, server.arg("on") == "true");
    reply();                       // with or without the argument, report the pin
  });

  server.begin();
}

void loop() {
  server.handleClient();
}

Answer with the pin, not with the request. `digitalRead` after the write costs nothing and it is what makes a lost reply visible instead of silently wrong.

When it does not work

The checkbox flips and the LED does not

The request never arrived or never matched. Visit /led?on=true directly in the browser - if the LED comes on, the route is fine and the page's fetch is the problem. If it does not, the route or the pin number is.

The LED is on and the checkbox says off after a reload

The page is starting from its own default instead of asking. Fetch the state once when the page loads. A checkbox is a picture of the pin, and a picture drawn from nothing is a guess.

The LED comes on but never goes off

The argument is being read as a string and anything non-empty is being treated as true. Compare against the literal text true, and let every other value mean off.

Two phones fight over it

They will, and there is nothing to fix in the sketch. Both are correct about what they asked for and neither is told when the other changes it. Either poll the state on a timer as well as on click, or move to a WebSocket, where the board can tell both of them at once.

Where this goes next

Three pages of this all break the same way at the second phone. The next one is why, and the library that fixes it.

Async web server

Edit this page — content/esp32/web-server-led-control.mdx

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse ESP32 on the forum