Practical case: Garage Web Control with ESP32

Practical case: Garage Web Control with ESP32 — hero

case-device-block-diagram,
.prometeo-educational-note,
.prometeo-device-postcode-section,
.prometeo-device-section-card {
margin: 2.4rem 0;
padding: 1.45rem 1.55rem;
border: 1px solid rgba(148, 163, 184, 0.30);
border-radius: 14px;
background:
linear-gradient(135deg, rgba(30, 41, 59, 0.50), rgba(15, 23, 42, 0.18)),
rgba(17, 24, 39, 0.48);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.20);
}
.case-objective > h2:first-of-type,
.case-device-block-diagram > h2:first-of-type,
.prometeo-device-postcode-section > h2:first-of-type,
.prometeo-device-section-card > h2:first-of-type {
margin-top: 0;
}
.prometeo-device-section-card > :last-child,
.case-objective > :last-child,
.case-device-block-diagram > :last-child,
.prometeo-device-postcode-section > :last-child {
margin-bottom: 0;
}
.prometeo-device-section-card.prometeo-device-section-card-code {
border-left: 4px solid rgba(56, 189, 248, 0.86);
background:
linear-gradient(135deg, rgba(8, 47, 73, 0.42), rgba(15, 23, 42, 0.18)),
rgba(15, 23, 42, 0.58);
}
.prometeo-device-section-card.prometeo-device-section-card-compact {
padding: 1.2rem 1.35rem;
}
.prometeo-device-section-card pre,
.case-objective pre,
.case-device-block-diagram pre,
.prometeo-device-postcode-section pre {
max-width: 100%;
}
.prometeo-device-postcode-section .prometeo-device-flow-item {
background:
linear-gradient(135deg, rgba(15, 23, 42, 0.50), rgba(30, 41, 59, 0.28)),
rgba(15, 23, 42, 0.28);
}
@media print {
.case-objective,
.case-device-block-diagram,
.prometeo-educational-note,
.prometeo-device-postcode-section,
.prometeo-device-section-card {
background: #fff;
color: #111827;
box-shadow: none;
break-inside: avoid;
page-break-inside: avoid;
}
}

Objective and use case

What you’ll build: You will build a standalone ESP32 web server that monitors a garage door’s physical state using a magnetic reed switch and securely actuates a relay to open or close it. This educational prototype provides a localized, cloud-free web interface for remote access control.

Why it matters / Use cases

  • Remote Status Verification: Real-time confirmation of the physical door state via a smartphone browser with sub-100ms latency, bypassing third-party clouds.
  • Access Control Integration: Replaces physical keys with authenticated network requests for localized, secure entry systems.
  • Smart Home Bridging: Exposes legacy garage hardware over standard HTTP for seamless integration into platforms like Home Assistant.

Expected outcome

  • A responsive local web UI displaying real-time open/closed status via continuous GPIO polling.
  • Reliable relay actuation using a ~500ms trigger pulse to safely mimic a physical wall button press.
  • A standalone IoT device operating entirely on local Wi-Fi with no external cloud dependencies.

Audience: IoT hobbyists and smart home developers; Level: Intermediate

Architecture/flow: Web Browser → HTTP GET/POST (Local Wi-Fi) → ESP32 Web Server → GPIO Read (Reed Switch) / GPIO Write (5V Relay) → Garage Door Opener

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

Local button

ESP32 BLE

Advertising packet

Status LED

Phone scanner

Conceptual flow: local configuration, BLE advertising and phone-side reading.

Validation path

Source code

PlatformIO build

Flash

Serial monitor

Conceptual summary of the tools used to check the published ESP32 project.

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. For this ESP32 DevKitC profile, the project was checked as a PlatformIO project: the validator extracted platformio.ini and src/main.cpp, created a temporary project and ran pio run against platform = espressif32, board = esp32dev and framework = arduino. It also checked article structure, copy/paste-safe ASCII command options, and unsupported stacks such as direct ESP-IDF or non-scoped ESP32 boards.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 4 sections, 3 tables and 2 code blocks detected before publication.
  • Checked code: 1 PlatformIO config + 1 ESP32 source/pio run.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published code, but it does not replace physical testing on your exact ESP32 DevKitC board, wiring, power supply and local WiFi environment.

Educational safety note

This project is a low-voltage educational prototype for learning how to read a door state and drive a local output, not a certified garage-door opener or real access-control product. Test first with an LED or dummy load, connect the relay only to an isolated low-voltage dry-contact input on the opener, and never to mains, motors, or power wiring. Keep the door’s original limit switches, photocells, and physical safety protections in place.

Prerequisites

Before beginning this tutorial, ensure you have the following:
* A computer running Windows, macOS, or Linux.
* Visual Studio Code (VS Code) installed.
* The PlatformIO IDE extension installed in VS Code.
* A micro-USB or USB-C cable that supports both power and data transfer.
* A local 2.4GHz WiFi network (ESP32 microcontrollers do not support 5GHz networks).

Materials

For this practical case, you must use exactly the following device model and components:
* Microcontroller: ESP32 DevKitC (standard 38-pin or 30-pin version, featuring the ESP-WROOM-32 module).
* Sensor: Magnetic reed switch (standard normally-open door/window sensor with two wire leads).
* Actuator: 5V Relay module (1-channel, with built-in optoisolator and transistor driver).
* Prototyping: Standard solderless breadboard and assorted male-to-male and female-to-male jumper wires.

Setup/Connection

The hardware setup requires mapping the ESP32 GPIO pins to the respective components. The ESP32 operates at 3.3V logic, but most standard single-channel relay modules require 5V for the relay coil while accepting a 3.3V logic signal on their input pin. The ESP32 DevKitC provides a 5V (or VIN) pin that passes through the USB voltage, which we use to power the relay coil.

Wiring Table

ComponentComponent Pin / WireESP32 DevKitC PinNotes
Relay ModuleVCC / DC+5V / VINPowers the relay coil (requires USB power).
Relay ModuleGND / DC-GNDCommon ground.
Relay ModuleIN / SignalGPIO 53.3V logic signal to trigger the relay.
Relay ModuleNO & COM terminalsTarget DeviceConnects to the garage opener wall-button terminals.
Reed SwitchWire 1GPIO 18Polarity does not matter.
Reed SwitchWire 2GNDUses ESP32 internal pull-up resistor.

Connection Instructions

  1. Mount the ESP32: Place the ESP32 DevKitC onto the breadboard, ensuring the pins straddle the center divider.
  2. Connect the Relay Module: Use female-to-male jumper wires to connect the relay module to the ESP32. Connect VCC to the ESP32 5V pin, GND to the ground rail, and the IN pin to GPIO 5.
  3. Connect the Reed Switch: Connect one wire of the magnetic reed switch to GPIO 18. Connect the other wire to the ground rail. We will configure GPIO 18 in software to use its internal pull-up resistor. When the magnet is near the switch, the circuit closes, pulling GPIO 18 to LOW (0V). When the magnet is removed, the pin is pulled HIGH (3.3V).
  4. Establish Common Ground: Ensure all components sharing the ESP32’s logic have their ground connections tied to the ESP32’s GND pin.

Validated Code

The project uses PlatformIO. Create a new project and replace the default configuration and source files with the code provided below.

The code includes an embedded HTML, CSS, and JavaScript string. The JavaScript uses the modern fetch API to poll the ESP32 for the door status every 2 seconds, ensuring the web page reflects the physical reality of the door without requiring manual browser refreshes.

platformio.ini

This file configures the build environment, specifying the board, framework, and serial monitor speed.

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200

src/main.cpp

This is the primary source file containing the WiFi logic, the web server routing, and the GPIO hardware control. Update the WIFI_SSID and WIFI_PASSWORD macros to match your testing router.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// --- Configuration ---
#define WIFI_SSID "LabNetwork"
#define WIFI_PASSWORD "LabPassword123"

// --- GPIO Pin Definitions ---
const int RELAY_PIN = 5;
const int REED_SWITCH_PIN = 18;

// --- Web Server Initialization ---
// Initialize the web server on standard HTTP port 80
WebServer server(80);

// --- HTML/CSS/JS Payload ---
// Using a raw string literal to embed the frontend code cleanly
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Garage Door Web Monitor</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f4f4f9;
            color: #333;
            text-align: center;
            padding: 50px 20px;
        }
        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            max-width: 400px;
            margin: auto;
        }
        h1 { font-size: 1.5em; margin-bottom: 20px; }
        .status {
            font-size: 2em;
            font-weight: bold;
            margin: 20px 0;
            padding: 10px;
            border-radius: 5px;
        }
        .closed { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .open { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        button {
            background-color: #007bff;
            color: white;
            border: none;
            padding: 15px 30px;
            font-size: 1.2em;
            border-radius: 5px;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        button:hover { background-color: #0056b3; }
        button:active { background-color: #004085; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Garage Door Monitor</h1>
        <div id="door-status" class="status closed">Loading...</div>
        <button onclick="triggerRelay()">Toggle Door</button>
    </div>

    <script>
        // Function to fetch the current door status
        function fetchStatus() {
            fetch('/status')
                .then(response => response.json())
                .then(data => {
                    const statusDiv = document.getElementById('door-status');
                    if (data.state === "Closed") {
                        statusDiv.innerText = "CLOSED";
                        statusDiv.className = "status closed";
                    } else {
                        statusDiv.innerText = "OPEN";
                        statusDiv.className = "status open";
                    }
                })
                .catch(error => console.error('Error fetching status:', error));
        }

        // Function to trigger the relay via POST request
        function triggerRelay() {
            fetch('/trigger', { method: 'POST' })
                .then(response => {
                    if(response.ok) {
                        console.log("Relay triggered successfully.");
                        // Immediately fetch status to reflect potential changes
                        setTimeout(fetchStatus, 1000); 
                    }
                })
                .catch(error => console.error('Error triggering relay:', error));
        }
// ...

🔒 This content is premium. With the monthly subscription (7-day free trial) you can unlock the full didactic material and the print-ready PDF pack.🔓 Unlock it — 7-day free trial
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// --- Configuration ---
#define WIFI_SSID "LabNetwork"
#define WIFI_PASSWORD "LabPassword123"

// --- GPIO Pin Definitions ---
const int RELAY_PIN = 5;
const int REED_SWITCH_PIN = 18;

// --- Web Server Initialization ---
// Initialize the web server on standard HTTP port 80
WebServer server(80);

// --- HTML/CSS/JS Payload ---
// Using a raw string literal to embed the frontend code cleanly
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Garage Door Web Monitor</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f4f4f9;
            color: #333;
            text-align: center;
            padding: 50px 20px;
        }
        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            max-width: 400px;
            margin: auto;
        }
        h1 { font-size: 1.5em; margin-bottom: 20px; }
        .status {
            font-size: 2em;
            font-weight: bold;
            margin: 20px 0;
            padding: 10px;
            border-radius: 5px;
        }
        .closed { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .open { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        button {
            background-color: #007bff;
            color: white;
            border: none;
            padding: 15px 30px;
            font-size: 1.2em;
            border-radius: 5px;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        button:hover { background-color: #0056b3; }
        button:active { background-color: #004085; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Garage Door Monitor</h1>
        <div id="door-status" class="status closed">Loading...</div>
        <button onclick="triggerRelay()">Toggle Door</button>
    </div>

    <script>
        // Function to fetch the current door status
        function fetchStatus() {
            fetch('/status')
                .then(response => response.json())
                .then(data => {
                    const statusDiv = document.getElementById('door-status');
                    if (data.state === "Closed") {
                        statusDiv.innerText = "CLOSED";
                        statusDiv.className = "status closed";
                    } else {
                        statusDiv.innerText = "OPEN";
                        statusDiv.className = "status open";
                    }
                })
                .catch(error => console.error('Error fetching status:', error));
        }

        // Function to trigger the relay via POST request
        function triggerRelay() {
            fetch('/trigger', { method: 'POST' })
                .then(response => {
                    if(response.ok) {
                        console.log("Relay triggered successfully.");
                        // Immediately fetch status to reflect potential changes
                        setTimeout(fetchStatus, 1000); 
                    }
                })
                .catch(error => console.error('Error triggering relay:', error));
        }

        // Poll the ESP32 every 2 seconds for status updates
        setInterval(fetchStatus, 2000);

        // Fetch initial status on load
        window.onload = fetchStatus;
    </script>
</body>
</html>
)rawliteral";

// --- Route Handlers ---

// Serve the main HTML page
void handleRoot() {
    server.send(200, "text/html", index_html);
}

// Return the current state of the reed switch as JSON
void handleStatus() {
    // Read the reed switch. 
    // LOW means the magnet is present (door closed).
    // HIGH means the magnet is away (door open).
    int sensorValue = digitalRead(REED_SWITCH_PIN);
    String state = (sensorValue == LOW) ? "Closed" : "Open";

    String jsonResponse = "{\"state\": \"" + state + "\"}";
    server.send(200, "application/json", jsonResponse);
}

// Pulse the relay to simulate a button press
void handleTrigger() {
    if (server.method() != HTTP_POST) {
        server.send(405, "text/plain", "Method Not Allowed");
        return;
    }

    // Most relays are Active HIGH. If yours is Active LOW, invert these states.
    digitalWrite(RELAY_PIN, HIGH);
    delay(500); // Hold the relay closed for 500ms
    digitalWrite(RELAY_PIN, LOW);

    server.send(200, "text/plain", "Triggered");
}

// Handle 404 Not Found
void handleNotFound() {
    server.send(404, "text/plain", "404: Not Found");
}

// --- Main Setup and Loop ---

void setup() {
    // Initialize Serial Monitor
    Serial.begin(115200);
    delay(100);
    Serial.println("\n--- Garage Door Web Monitor ---");

    // Initialize GPIOs
    pinMode(RELAY_PIN, OUTPUT);

    // Ensure relay starts in the OFF state immediately
    digitalWrite(RELAY_PIN, LOW); 

    // Use internal pull-up for the reed switch
    pinMode(REED_SWITCH_PIN, INPUT_PULLUP);

    // Connect to WiFi
    Serial.print("Connecting to WiFi: ");
    Serial.println(WIFI_SSID);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    // Wait for connection
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }

    Serial.println("\nWiFi connected successfully.");
    Serial.print("IP Address: ");
    Serial.println(WiFi.localIP());

    // Configure Web Server Routes
    server.on("/", HTTP_GET, handleRoot);
    server.on("/status", HTTP_GET, handleStatus);
    server.on("/trigger", HTTP_POST, handleTrigger);
    server.onNotFound(handleNotFound);

    // Start the server
    server.begin();
    Serial.println("HTTP server started.");
}

void loop() {
    // Listen for incoming client requests
    server.handleClient();

    // Small delay to yield to the ESP32's underlying WiFi/OS tasks
    delay(2); 
}

Build/Flash/Run commands

Use the PlatformIO Core CLI to compile and upload the firmware. Open the terminal within VS Code and ensure you are in the root directory of your project.

Command Table

ActionCommand
Initialize Project (if needed)pio project init --board esp32dev
Build Firmwarepio run
Upload to ESP32pio run --target upload
Open Serial Monitorpio device monitor

Workflow

  1. Create a new directory for your project and open it in VS Code.
  2. Initialize the project using the PlatformIO CLI.
  3. Replace the contents of platformio.ini and src/main.cpp with the Validated Code provided above.
  4. Connect your ESP32 DevKitC to your computer via USB.
  5. Execute pio run --target upload in the terminal to compile the code and flash it to the microcontroller.
  6. Execute pio device monitor to view the serial output. Note the IP address printed once the WiFi connects.

Step‑by‑step Validation

To ensure the prototype functions correctly and safely, perform the following grouped checkpoints.

  1. Network Initialization Check
    • Action: Observe the Serial Monitor output immediately after a board reset.
    • Expected Observation: The terminal prints “Connecting to WiFi…”, followed by a series of dots, and finally “WiFi connected successfully” along with an IP address (e.g., 192.168.1.50).
    • Pass Condition: The ESP32 successfully acquires a local IP address.
  2. Web Interface Loading
    • Action: Open a web browser on a computer or smartphone connected to the same WiFi network and enter the ESP32’s IP address in the URL bar.
    • Expected Observation: The “Garage Door Monitor” page loads. The UI displays a large status block and a “Toggle Door” button.
    • Pass Condition: The page renders correctly without missing CSS styles or JavaScript errors in the browser console.
  3. Sensor State Monitoring (Closed)
    • Action: Bring the magnetic part of the reed switch close to the wired sensor body.
    • Expected Observation: Within 2 seconds, the web interface status block turns green and displays “CLOSED”.
    • Pass Condition: The ESP32 correctly reads the pulled-LOW state of GPIO 18 and serves the updated JSON payload to the polling browser.
  4. Sensor State Monitoring (Open)
    • Action: Pull the magnet away from the wired sensor body.
    • Expected Observation: Within 2 seconds, the web interface status block turns red and displays “OPEN”.
    • Pass Condition: The internal pull-up resistor successfully pulls GPIO 18 HIGH, and the web interface reflects the change automatically.
  5. Actuator Trigger Validation
    • Action: Click the “Toggle Door” button on the web interface.
    • Expected Observation: You should hear an audible “click” from the relay module, followed by a second “click” exactly half a second later.
    • Pass Condition: The ESP32 correctly receives the HTTP POST request, drives GPIO 5 HIGH for 500ms, and returns it to LOW, simulating a momentary button press.

Troubleshooting

SymptomLikely causeFix
Serial Monitor prints continuous dots; never connects.Incorrect WiFi credentials or out of range.Verify WIFI_SSID and WIFI_PASSWORD exactly match your 2.4GHz network. Ensure the ESP32 is within range of the router.
Web page loads, but status stays on “Loading…”.JavaScript fetch error or browser blocking local requests.Check the browser’s developer console (F12) for CORS or network errors. Ensure you are accessing via http://, not https://.
Relay clicks immediately on boot and stays triggered.Relay module is “Active LOW” instead of “Active HIGH”.Change digitalWrite(RELAY_PIN, LOW) to HIGH in setup(), and swap HIGH/LOW in the handleTrigger() function.
Door status randomly flips between Open and Closed.Floating pin or loose wire on the reed switch.Ensure the reed switch is firmly connected to GPIO 18 and GND. Verify pinMode is strictly set to INPUT_PULLUP.
Relay LED lights up, but there is no audible “click”.Insufficient power to the relay coil.Ensure the relay VCC is connected to the ESP32 5V (VIN) pin, not the 3.3V pin. Ensure the USB port provides sufficient current (500mA+).

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary purpose of the ESP32 web server described in the text?




Question 2: What component is used to monitor the physical state of the garage door?




Question 3: How does the ESP32 actuate the garage door to open or close it?




Question 4: What is a key feature of the web interface provided by this prototype?




Question 5: What is the expected latency for real-time confirmation of the door state via a smartphone browser?




Question 6: How does the system safely mimic a physical wall button press?




Question 7: How does the web UI display the real-time open/closed status of the door?




Question 8: What platform is explicitly mentioned as an example for smart home integration?




Question 9: What network dependency does this standalone IoT device have?




Question 10: Who is the intended audience for this project?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top