PROJECT 7 · WI‑FI / WEB

Relay Web Server

Switch a relay from a web page: a tiny 3.3 V signal controlling a much larger circuit.

Builds on: Project 5's web-controlled output, now switching a relay instead of an LED, over a request-handling library instead of hand-rolled parsing.

⏱ ~30 min 📊 Intermediate 🔖 Prerequisite: Project 5
⬇ Download the sketch Save Project_7_ESP32_Relay_Web_Server.ino, then open it in the Arduino IDE (setup: Foundation 3). Needs the ESP Async WebServer + AsyncTCP libraries and your Wi-Fi credentials.
⚠️ Safety first. Mains voltage (120 V / 230 V) is dangerous. In this project use only low‑voltage loads (the kit's LED). Never wire mains while powered; if you're not experienced with mains, don't use it: ask someone who is.

A relay module rated "10 A 250 V AC" describes the contacts, not a finished, certified product. Wiring it to mains for real, beyond this project's LED demo, also needs mains‑rated wire and connectors, an enclosure that keeps bare terminals out of reach, and ideally a circuit protected by a GFCI/RCD. Treat the module as one part of a mains‑safe build, not the whole thing.
🎲 Try it without hardware 📄 diagram.json Open the Wokwi simulator, then open or download diagram.json above and paste its wiring in to load the circuit.

🎯 Objective

By the end of this project, you will:

  • Understand what a relay does, and why it lets a 3.3 V pin switch a mains-voltage load.
  • Read a relay module's terminals: COM, NO and NC, and pick the right pair.
  • Recognise active-low control, where a LOW signal turns the relay on.
  • Use the ESP Async WebServer library and see why it replaces the hand-written request loop of Projects 5 and 6.
  • Serve a page stored in flash with PROGMEM, filled in at send time by a template processor.
  • Read query parameters such as ?relay=1&state=1 instead of parsing the URL by hand.

1What a relay is

A relay is an electrically‑operated switch. A small coil current (from an ESP32 GPIO) flips a mechanical contact, connecting or disconnecting a separate higher‑power circuit: the two sides electrically isolated.

2-channel relay module labeled
Switched terminals + control pins.
Relay module screw terminals
The screw terminals (COM/NO/NC).

Switched side: COM / NO / NC

  • COM: the wire you're switching.
  • NO (Normally Open): off until triggered → use for off by default.
  • NC (Normally Closed): on until triggered → use for on by default.

Control side: VCC / GND / IN

Relay control pins VCC GND IN1 IN2
VCC/GND power it; IN1/IN2 are the ESP32 triggers.
JD-VCC jumper on the relay module
JD‑VCC jumper: leave on for the simplest wiring.

Inductive loads and flyback protection

Most relay modules, including this kit's, already carry a small flyback diode across the relay's own coil. That protects the control side (the ESP32) from the voltage spike a coil produces the instant it switches off.

The switched side needs its own protection only if the load itself is inductive: a motor, a solenoid, a fan, anything built around a coil. Switching an inductive load off makes it try to keep current flowing, which can arc across the relay's contacts and wear them out early, and can throw electrical noise back down the wiring. The fix is a snubber across the load: a diode wired to only conduct the reverse spike for DC loads, or an RC snubber for AC loads. This project's LED isn't inductive, so none of this applies here, but it's the first thing to add if you later switch a motor or a solenoid through this relay.

2Install the required libraries

Why we switch to a web-server library now. Projects 5 and 6 hand-rolled the HTTP parsing: reading bytes one at a time, watching for the blank line, and searching the raw request text yourself. That was worth doing once so you could see what a request and response actually look like. From here on, that plumbing does not change project to project, so this project switches to the ESP Async WebServer library: it parses the request, matches a URL to a handler you register, and answers many clients without blocking loop(), the same non-blocking idea millis() introduced in Project 4.

This project uses the asynchronous server libraries: install both:

  • ESP Async WebServer
  • AsyncTCP (ESP32)

Add each via Sketch → Include Library → Add .ZIP Library…, then restart the IDE.

3Wiring the Circuit

Relay wired to the ESP32 with an LED as the safe load
Relay VCC→VIN, GND→GND, IN1→GPIO 26; LED switched through COM→NO.
  • Relay VCCVIN (5 V) · GNDGND
  • Relay IN1GPIO 26 (IN2 → GPIO 27 for channel 2)
  • Switched side: LED (with 220 Ω) through COM → NO

Going further: wiring the second channel

The sketch already drives both relay channels, and the web page already shows two toggle switches (NUM_RELAYS = 2); the wiring above only demoes channel 1 to keep the first build simple. To see both work, wire a second LED (with its own 220 Ω resistor) through channel 2's COM → NO, exactly like channel 1's, and confirm "Relay #2" clicks and lights it from the web page too.

4Upload & use

Set your Wi‑Fi credentials, upload, open Serial Monitor at 115200, press EN/RESET:

==============================================
 Project 7: ESP32 Relay Web Server
==============================================
2 relay(s), mode: Normally Open (NO)
Relay #1 -> GPIO 26
Relay #2 -> GPIO 27
Connecting to Wi-Fi: MyNetwork
....
Wi-Fi connected!
Open this address in your browser:  http://192.168.1.42
----------------------------------------------
[WEB] Relay #1 -> ON

Open the address; flip a toggle → the relay clicks and the clean line above prints (the original sketch printed a cryptic NO 11).

NO‑mode note: in Normally‑Open mode the relay is off when the pin is HIGH, so the sketch writes the inverse of the requested state. That's expected.

5Code Walkthrough: Understanding the Sketch

This sketch looks different from Projects 5 and 6, and the difference is worth understanding: the async library takes over the request handling you previously wrote by hand.

Step 1: Configuration at the top

#define RELAY_NO    true            // relays wired Normally Open
#define NUM_RELAYS  2               // how many relays on the module
int relayGPIOs[NUM_RELAYS] = {26, 27};

AsyncWebServer server(80);

Everything you might change lives in one block. relayGPIOs is an array, so adding a third relay means adding a pin and raising NUM_RELAYS: no other code changes. RELAY_NO records how the relay is wired, which decides the logic further down.

Step 2: the web page, stored in flash

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
  ...
  %BUTTONPLACEHOLDER%
  ...
)rawliteral";
  • R"rawliteral( ... )rawliteral" is a raw string literal: everything between the markers is taken literally, so HTML can contain quotes and newlines without escaping. Compare Project 5, where every line was a separate client.println().
  • PROGMEM keeps the page in flash instead of copying it into RAM at startup. The ESP32 has plenty of flash and much less RAM.

%BUTTONPLACEHOLDER% is a marker the library replaces when the page is sent.

Step 3: setup() puts the relays in a safe state, then registers routes

for (int i = 1; i <= NUM_RELAYS; i++) {
  pinMode(relayGPIOs[i-1], OUTPUT);
  if (RELAY_NO) digitalWrite(relayGPIOs[i-1], HIGH);   // NO: HIGH = off
}

Starting with every relay off matters more here than with LEDs: a relay that switches on at power-up would energise whatever it controls.

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send_P(200, "text/html", index_html, processor);
});

server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){
  if (request->hasParam(PARAM_INPUT_1) && request->hasParam(PARAM_INPUT_2)) {
    int relayNum = request->getParam(PARAM_INPUT_1)->value().toInt();
    int wantOn   = request->getParam(PARAM_INPUT_2)->value().toInt();
    if (RELAY_NO) digitalWrite(relayGPIOs[relayNum-1], !wantOn);  // NO: LOW = on
  }
  request->send(200, "text/plain", "OK");
});

server.begin();

server.on(path, method, handler) says "when someone asks for this URL, run this function". The [](AsyncWebServerRequest *request){ ... } is a lambda, a function written inline. You register routes once and the library calls them for you.

Look closely at !wantOn. On a normally open module the relay closes when the control pin goes LOW, so the sketch inverts the value: asking for "on" (1) writes LOW (0).

Step 4: processor() builds the switches

String processor(const String& var){
  if (var == "BUTTONPLACEHOLDER") {
    String buttons = "";
    for (int i = 1; i <= NUM_RELAYS; i++) {
      buttons += /* one toggle switch per relay */;
    }
    return buttons;
  }
  return String();
}

When the library meets %BUTTONPLACEHOLDER% it calls processor("BUTTONPLACEHOLDER") and drops in whatever comes back, so the page grows automatically if you add relays. relayState() reads each pin back and returns "checked" or "", drawing each switch in the position matching the real relay.

Step 5: an empty loop()

void loop() {
}

Not a mistake. With the async server, requests are handled in the background, so nothing is left for loop() to do. That free loop() is exactly what makes room for Project 8 to poll a button while still serving pages.

Key concepts: what each line really does

CodeWhat it does
AsyncWebServer server(80)An event-driven server: you register handlers and it deals with clients itself, unlike WiFiServer where you read the request byte by byte.
PROGMEMKeeps the page in flash rather than RAM. The ESP32 has around 520 KB of RAM but megabytes of flash.
R"rawliteral(...)rawliteral"A raw string: quotes and newlines need no escaping, so HTML can be pasted in as-is.
server.on(path, HTTP_GET, handler)Registers a route: a URL plus the function to run for it.
request->hasParam("relay")Checks a query parameter exists before reading it, so a malformed URL cannot crash the sketch.
request->getParam("relay")->value()Reads ?relay=1 and hands back "1", replacing the indexOf/substring parsing of Project 6.
send_P(200, "text/html", index_html, processor)Sends a page from flash (_P) and runs processor over its placeholders on the way out.
digitalWrite(pin, !wantOn)Inverts the command for active-low hardware: LOW energises a normally-open relay.
void loop() {}Deliberately empty: the async server needs no polling.
Predict, then check. The relay is wired Normally Open. Predict: does the ESP32 write HIGH or LOW to the relay pin to turn the load ON? Check your answer against the !wantOn line above, then confirm on the real relay.

6Demonstration

Animated demo: phone toggles relay on and off, clicking an LED
Toggle from your phone; the relay clicks and the load follows.

7Knowledge Check

Test your understanding.

1. On a normally-open (NO) relay module, what happens when the control pin goes HIGH?

2. What does `PROGMEM` do when storing the web page HTML?

3. Why does the sketch use `!wantOn` when writing to the relay pin?

4. What is the advantage of ESP Async WebServer over the hand-rolled HTTP parser?

5. How does the template processor `%BUTTONPLACEHOLDER%` work?

8Wrapping Up: What You've Learned

You can now switch a real electrical load from a browser, and you have met the server library the rest of the kit uses.

  • A relay is an electrically operated switch: a few milliamps from a GPIO drive a coil that closes contacts rated for mains voltage, with the two sides physically isolated.
  • COM/NO/NC: use COM and NO for a load that should be off by default, COM and NC for one that should be on by default.
  • Many relay modules are active low. Read your module rather than assuming, and put outputs in a safe state during setup().
  • ESP Async WebServer replaces the manual request loop with routes, handling connections in the background, which is why loop() can be empty.
  • Query parameters (?relay=1&state=1) are the normal way to pass values in a URL, and hasParam() / getParam() read them safely.
  • Storing the page in PROGMEM keeps RAM free, and a template processor fills in the changing parts at send time.

Now try

  • Auto-off timer: Add a relay state variable that turns the relay off automatically after 10 seconds, preventing a load from being left on indefinitely.
  • Both channels: Wire a second LED through channel 2's COM/NO terminals and verify that both toggles on the web page work independently.

9Troubleshooting

SymptomLikely causeFix
Compile error on ESP Async WebServerLibraries missingInstall ESP Async WebServer and AsyncTCP
Relay clicks, load doesn't switchWired to NCUse COM → NO
Toggle seems invertedNO‑mode inversion (normal)Leave the sketch as‑is
No click at allIN mis‑wired / module unpoweredIN → GPIO 26/27; VCC→VIN, GND→GND
Stuck on "Connecting…"Wrong credentials / 5 GHzFix credentials; use 2.4 GHz