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.

⬇ Download the sketch Save Project_7_ESP32_Relay_Web_Server.ino, then open it in the Arduino IDE (setup: Foundation 3). Needs the ESPAsyncWebServer + 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.

🎯 Objective

By the end of this project, you will:

1 · What 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

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.

2 · Install the required libraries

This project uses the asynchronous server libraries: install both:

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

3 · Wiring 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.

4 · Upload & 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.

5 · Code 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";

%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.

6 · Demonstration

Relay module and web toggles controlling an LED
Toggle from your phone; the relay clicks and the load follows.

7 · Wrapping 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.

8 · Troubleshooting

SymptomLikely causeFix
Compile error on ESPAsyncWebServerLibraries missingInstall ESPAsyncWebServer 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
← Project 6 · RGB LED Web Server Project 8 · Output Sync →