IoT & Embedded Systems

Building My ESP32 Smart Switch: From Prototype to Custom Hardware

How I built an ESP-based Wi-Fi smart switch, from early hardware experiments to relay control, firmware, debugging, and custom PCB design.

Building My ESP32 Smart Switch: From Prototype to Custom Hardware

Introduction: Beyond the LED Blink Demo

Almost every beginner's journey into microcontrollers begins with an LED. You connect a 220Ω resistor to a digital output pin on an Arduino or ESP board, call digitalWrite(pin, HIGH), and watch a tiny light emit a steady glow.

While that first blinking diode is exciting, it doesn't solve a real problem in the physical world. In a real home, nobody controls miniature 5-milliamp LEDs. We live with high-voltage alternating current: 230-volt ceiling fans, heavy tube lights, room heaters, and appliances switched through thick copper wires behind plastic wall switchboards.

More importantly, most internet-of-things (IoT) smart switches sold commercially have an infuriating flaw: they are slaves to the cloud.

If your broadband connection drops, if the home Wi-Fi router reboots, or if a third-party server suffers an outage, your smart lights stop responding. Even worse, many commercial smart modules ignore existing mechanical wall switches entirely. When your family members flip the switch on the wall, the app loses sync, and nobody knows whether the circuit is truly open or closed.

I built Smart Switch because I wanted to solve this problem from an engineering standpoint. I wanted to build an embedded home automation system grounded in an uncompromising offline-first philosophy:

  • Physical wall switches must always work, with or without Wi-Fi.
  • Power cuts must not cause unpredictable relay toggling.
  • The device must host its own local web interface so smartphones can control appliances directly without third-party internet dependencies.
  • The system must transition responsibly from a breadboard prototype toward a robust hardware architecture.

Here is the story of how I built it—from early microcontroller experiments to multi-channel relay logic, firmware architecture, serial telemetry, and hardware design.


Why I Wanted to Build a Smart Switch

Living and building projects in Teghra, Bihar, the reality of everyday electrical infrastructure is immediate. Power interruptions happen, voltage fluctuations occur, and mobile data or Wi-Fi connectivity is not always 100% stable.

In this environment, a smart switch that relies entirely on a remote cloud server is practically unusable. Imagine walking into a dark room during an internet outage and not being able to turn on a light because a cloud handshake timed out. That isn't smart home technology—it's a regression.

My design criteria were clear from day one:

  1. Zero-Latency Physical Control: Toggling a household wall switch must trigger the relay instantly (under 20 milliseconds), completely debounced and immune to electrical contact bounce.
  2. Deterministic Boot-Time State Sync: When electricity returns after a power outage, the microcontroller must immediately read the physical switches and restore the exact expected state.
  3. Local-First Network Control: The device must host an onboard asynchronous web server and captive portal so any phone on the local network can access a control dashboard without an active internet connection.
  4. Galvanic Isolation: The low-voltage digital brain must remain strictly isolated from 230V mains current.

Starting with the ESP: Early Experiments & Powering

Before building a full 4-channel household system, I started with fundamental hardware experiments using the ESP architecture.

Figure 1: Early bench experimentation with an ESP module, TP4056 charging board, and lithium cell power rail.
Figure 1: Early bench experimentation with an ESP module, TP4056 charging board, and lithium cell power rail.

My initial explorations in late 2025 focused on understanding microcontroller behavior under real-world conditions:

  • Power Rail Stability: Powering an ESP module through battery rails (using a TP4056 charging circuit and single-cell lithium battery) taught me how sensitive microcontrollers are to voltage sag during Wi-Fi transmission bursts. When an ESP chip transmits RF packets, current draw can spike up to 300–400mA for brief microseconds. Without adequate decoupling capacitors across the 3.3V rail, brownout resets trigger instantly.
  • Bootloader Dynamics: Observing how different GPIO pins behave during boot. Certain pins on ESP chips are "strapping pins"—if pulled LOW or HIGH during the brief microsecond window when the chip powers on, the microcontroller enters download bootloader mode instead of executing application firmware.

These preliminary experiments provided the foundation I needed before connecting microcontrollers to real mechanical relays and household switches.


Making a Real Relay Control System

Controlling a high-voltage household appliance requires an electromechanical relay. A relay functions as an electrically operated magnetic switch: a low-power 5V control coil pulls an armature down, mechanically connecting the high-voltage common terminal (COM) to the normally open terminal (NO).

  [ ESP32 3.3V GPIO ]
           │
           ▼
  [ Optocoupler LED ] ── (Optical Air Gap) ──► [ Phototransistor ]
                                                       │
                                                       ▼
                                              [ NPN Driver Circuit ]
                                                       │
                                                       ▼
                                              [ 5V Relay Coil + Diode ]
                                                       │
                                                       ▼
                                         [ 230V AC Mechanical Contacts ]
                                         (Galvanically Isolated Mains)

For Smart Switch, I utilized a 4-Channel 5V Relay Module equipped with Songle SRD-05VDC-SL-C relays rated for switching AC loads up to 250V / 10A.

The Active-LOW Relay Trap

Most commercial hobbyist relay boards are Active-LOW. This means:

  • Driving the input pin LOW (0V) completes the optocoupler circuit, energizing the coil and turning the relay ON.
  • Driving the input pin HIGH (3.3V/5V) turns the optocoupler off, de-energizing the coil and turning the relay OFF.

This presents an immediate engineering danger: when an ESP32 boots, default GPIO configurations can momentarily float or default to LOW. If your firmware isn't written defensively, every relay will click ON the instant power is applied!

To prevent this in Smart Switch, I architected the RelayController class to enforce a guaranteed safe boot state:

// src/control/relay_controller.cpp
#define RELAY_ACTIVE_LOW true

void RelayController::begin() {
    LOG_RELAY("Initializing relay controller");

    /* Configure relay pins as outputs */
    pinMode(RELAY_1_PIN, OUTPUT);
    pinMode(RELAY_2_PIN, OUTPUT);
    pinMode(RELAY_3_PIN, OUTPUT);
    pinMode(RELAY_4_PIN, OUTPUT);

    /* Immediately assert safe OFF state before enabling inputs */
    // In Active-LOW logic: HIGH = Relay OFF
    digitalWrite(RELAY_1_PIN, RELAY_ACTIVE_LOW ? HIGH : LOW);
    digitalWrite(RELAY_2_PIN, RELAY_ACTIVE_LOW ? HIGH : LOW);
    digitalWrite(RELAY_3_PIN, RELAY_ACTIVE_LOW ? HIGH : LOW);
    digitalWrite(RELAY_4_PIN, RELAY_ACTIVE_LOW ? HIGH : LOW);

    LOG_OK("All relays set to OFF (safe state)");
}

By writing HIGH to every relay pin during initialization, the firmware ensures zero false clicks or load toggles during startup.


The Physical Switch Problem & Real Bench Setup

The defining challenge of smart home design is reconciling mechanical switches with software control.

In a normal home, when someone turns on a light switch, the circuit is closed. If your smart system only accepts software toggles, physical wall switches become useless. To solve this, I built a dedicated physical test bench using a standard Indian household 4-gang modular switchboard:

Figure 2: The physical Smart Switch test bench: 4-gang Indian modular switchboard connected via low-voltage signal wiring to an ESP32 Dev Module and 4-channel optocoupled relay board.
Figure 2: The physical Smart Switch test bench: 4-gang Indian modular switchboard connected via low-voltage signal wiring to an ESP32 Dev Module and 4-channel optocoupled relay board.

Low-Voltage Signal Wiring

Instead of routing dangerous 230V AC through the wall switch contacts to the microcontroller, the wall switches are wired exclusively as low-voltage digital logic inputs:

  • Each mechanical switch connects between one ESP32 GPIO and common GND.
  • The ESP32 configures each pin with an internal pull-up resistor (INPUT_PULLUP).
  • When the wall switch is open, the pin reads HIGH (3.3V).
  • When the wall switch is closed, the pin is pulled to ground and reads LOW (0V).

Non-Blocking Debounce Engine

Mechanical switches are spring-loaded metal contacts. When you flick a switch, the contacts bounce against each other for 5 to 20 milliseconds before settling into continuous contact. If a microcontroller reads that raw pin directly, it will register dozens of rapid ON/OFF triggers in a fraction of a second.

In manual_switch.cpp, I implemented a non-blocking debouncing state machine utilizing millis():

// src/control/manual_switch.cpp
void ManualSwitch::update() {
    for (int i = 0; i < 4; i++) {
        bool reading = digitalRead(switchPins[i]);

        /* Detect raw edge transition */
        if (reading != lastReadState[i]) {
            lastDebounceTime[i] = millis();
            lastReadState[i] = reading;

            LOG_SWITCH("SW" + String(i + 1) + 
                       " | GPIO=" + String(switchPins[i]) + 
                       " | RAW -> " + (reading == HIGH ? "HIGH" : "LOW"));
        }

        /* Verify stable state after debounce window */
        if ((millis() - lastDebounceTime[i]) > debounceDelay) {
            if (reading != lastStableState[i]) {
                lastStableState[i] = reading;

                // Active-LOW input: LOW reading = switch engaged
                bool relayState = (reading == LOW);

                LOG_SWITCH("SW" + String(i + 1) + 
                           " | STABLE=" + (reading == HIGH ? "HIGH" : "LOW") + 
                           " | CMD RELAY=" + (relayState ? "ON" : "OFF"));

                relay.setRelay(i + 1, relayState);
            }
        }
    }
}

Safe GPIO Selection: A Crucial Hardware Discovery

One of the most valuable lessons I learned during the prototype build came from selecting GPIO pins.

On an ESP32 with 30 or 38 exposed pins, beginners often assume every pin labeled "GPIO" can be used interchangeably for any input or output. That assumption can break hardware quickly.

The Hard Truth About GPIO 34–39

In my initial pin layout planning, I considered using pins 34 and 35 for manual switches because they were located near the ground rail. However, during testing, the inputs floated unpredictably, triggering random ghost switches even when nobody touched the board.

Upon reviewing the Espressif ESP32 technical reference manual, I discovered the root cause:

GPIO 34, 35, 36, and 39 (GPI pins) are INPUT-ONLY pins and DO NOT possess internal pull-up or pull-down resistors.

Calling pinMode(34, INPUT_PULLUP) fails silently in hardware because the internal pull-up resistor silicon simply does not exist on those silicon channels!

To establish rock-solid stability, I restructured the pin configuration entirely:

  • Manual Switches (INPUT_PULLUP): GPIO 32 (Yellow), GPIO 33 (Green), GPIO 18 (Orange), GPIO 19 (Blue).
  • Relay Outputs: GPIO 27, GPIO 14, GPIO 12, GPIO 13.
  • Status LED: GPIO 15 (with current-limiting resistor).

This clean separation eliminated floating inputs and created a deterministic hardware layer.


Boot-Time State Synchronization

Consider this scenario: you go to sleep with your bedroom light switch turned OFF. At 3:00 AM, the local power grid suffers a blackout. Thirty minutes later, electricity is restored.

What should your smart switch do?

In poorly architected smart devices, the microcontroller boots up, defaults its relays to ON, or waits endlessly for a cloud connection to figure out what state it was in. The result? Your bedroom lights turn on full blast in the middle of the night.

In Smart Switch, I engineered deterministic boot-time state synchronization:

  1. During boot_manager.cpp execution, the firmware asserts safe HIGH on all relay outputs.
  2. The ManualSwitch::begin() routine immediately performs a digitalRead on all 4 physical wall switch inputs.
  3. If Switch 1 is physically in the ON position, Relay 1 is commanded ON. If it is physically OFF, Relay 1 remains OFF.

The relays reflect the physical reality of the room within 50 milliseconds of boot—without waiting for Wi-Fi, without consulting a server, and without user intervention.


Connecting the Switch to Wi-Fi: Captive Portal & Local Control

Once the physical switching was bulletproof, I layered on the wireless capabilities.

Figure 3: The complete Smart Switch system: Laptop running PlatformIO serial telemetry, smartphone hosting the local captive portal at 192.168.4.1/dash, physical switchboard, and active 4-channel relay module.
Figure 3: The complete Smart Switch system: Laptop running PlatformIO serial telemetry, smartphone hosting the local captive portal at 192.168.4.1/dash, physical switchboard, and active 4-channel relay module.

Instead of requiring users to download a heavy mobile app from an app store, Smart Switch runs an embedded asynchronous HTTP web server (ESPAsyncWebServer) directly on the ESP32:

1. Captive Portal AP Mode (SmartSwitch-AP)

If the switch is not yet configured with home Wi-Fi credentials, it boots into Access Point (AP) mode. When you connect your smartphone to the open SmartSwitch-AP network, a captive portal automatically redirects your browser to:

http://192.168.4.1/dash

Figure 4: Real-time relay control through the embedded web dashboard at 192.168.4.1/dash, showing instant physical relay actuation and active-LOW channel LEDs.
Figure 4: Real-time relay control through the embedded web dashboard at 192.168.4.1/dash, showing instant physical relay actuation and active-LOW channel LEDs.

2. Live Web Dashboard

The dashboard is served entirely out of ESP32 flash memory as compressed HTML5, CSS, and JavaScript. It provides:

  • Individual toggle controls for Relays 1 through 4 with live status tags.
  • Master All ON and All OFF action buttons.
  • A built-in Wi-Fi network scanner (/api/wifi-scan) that searches for local 2.4 GHz access points, displaying signal strength (RSSI) bars and encryption status.
  • Wi-Fi credential provisioning (/api/wifi-connect) that securely writes network credentials to Non-Volatile Storage (NVS).

3. Local Network Resolution via mDNS

Once enrolled in the home Wi-Fi network, the device joins Station (STA) mode and registers with the local multicast DNS daemon:

http://smartswitch.local

Family members can control household appliances from any laptop, tablet, or phone connected to the home Wi-Fi simply by navigating to smartswitch.local, completely bypassing the public internet.


ESP32 vs. ESP8266: Why I Standardized on ESP32

During early planning, I evaluated whether to build Smart Switch around the ubiquitous ESP8266 (NodeMCU / ESP-12F) or the newer ESP32.

While the ESP8266 is lower cost, hands-on development revealed significant constraints:

  • Limited Usable GPIOs: The ESP8266 has very few safe GPIOs that do not interfere with serial programming, boot modes, or onboard flash memory. Running 4 manual switch inputs, 4 relay outputs, status LEDs, and recovery buttons simultaneously is nearly impossible on an ESP8266 without external I2C port expanders (like the PCF8574).
  • Single-Core Bottleneck: On an ESP8266, running an asynchronous web server, Wi-Fi networking, software debouncing, and cryptographic routines on a single core frequently leads to watchdog timer resets if a network packet blocks execution.
  • ESP32 Dual-Core Advantage: The ESP32's dual-core Xtensa processor allows clean task partitioning: one core handles high-throughput Wi-Fi networking and HTTP requests, while the second core focuses on real-time GPIO polling and debouncing.

For a 4-channel production-grade smart switch, the ESP32 proved to be the superior and far more reliable architectural choice.


Modular Firmware Architecture

Rather than dumping hundreds of lines of code into a single chaotic .ino sketch, I organized the firmware into a production-minded, modular directory structure under PlatformIO:

smart-switch-firmware/
├── platformio.ini         # Environment, baud rates, and library dependencies
├── src/
│   ├── config/            # pin_config.h, device_config.h, build_config.h
│   ├── control/           # manual_switch.cpp, relay_controller.cpp
│   ├── core/              # boot_manager.cpp, state_manager.cpp
│   ├── network/           # wifi_manager.cpp, captive_portal.cpp, mdns.cpp
│   ├── storage/           # nvs_manager.cpp (Preferences library)
│   ├── system/            # logger.cpp, watchdog.cpp, auth.cpp
│   ├── ui/                # led_manager.cpp, button_manager.cpp
│   ├── web/               # web_server.cpp (ESPAsyncWebServer routes)
│   └── webpages/          # dashboard_page.h, login_page.h

Non-Volatile Storage (NVS) State Memory

To remember states across reboots, nvs_manager.cpp leverages the ESP32's internal Non-Volatile Storage partition via the Arduino Preferences library:

  • Wi-Fi SSIDs and encrypted passwords persist across power cuts.
  • User authentication tokens and device configuration parameters remain intact.
  • Optional last-known relay states can be stored and recalled when needed.

Debugging with Serial Telemetry & Cloud Bridge Simulation

Hardware debugging requires continuous visibility into what the microcontroller is experiencing.

Figure 5: Dual-terminal development environment: Python Firestore cloud bridge simulator on the left, and PlatformIO serial monitor (115200 baud) on the right showing mDNS resolution and state transitions.
Figure 5: Dual-terminal development environment: Python Firestore cloud bridge simulator on the left, and PlatformIO serial monitor (115200 baud) on the right showing mDNS resolution and state transitions.

Debug-Grade Serial Logging

In logger.cpp, I created a categorized logging framework that prefixes serial output with colorized subsystem tags:

  • [BOOT]: Firmware startup sequence, MAC address detection, partition layout.
  • [RELAY]: Relay index, target pin, requested logical state, output voltage level.
  • [SWITCH]: Raw pin edge transitions, debounced stable confirmation.
  • [WIFI]: AP mode initialization, STA connection attempts, RSSI metrics.
  • [INFO]: Hardware read-back verification confirming that physical voltage levels match software expectations.

Python Cloud Bridge Simulation

In Phase 2 development, I explored extending Smart Switch toward remote cloud capabilities. To test how the ESP32 handles bidirectional synchronization without relying on third-party proprietary clouds, I built a local Python simulator (simulator.py) that interfaces with Google Firestore. As shown in Figure 5, the bridge listens for remote command events, forwards them to the device REST API, and updates real-time Firestore documents whenever a manual wall switch is flipped.


Firmware Security & Reverse Engineering Auditing

As an engineering student with a strong passion for cybersecurity, I wanted to understand how secure IoT devices actually are in the field.

Most commercial smart-switch manufacturers ship devices with unprotected flash memory, allowing attackers with physical access to extract firmware binaries, recover Wi-Fi credentials, and reverse engineer proprietary protocols.

To evaluate this firsthand, I set up a dedicated firmware security lab:

  1. Using the esptool.py utility, I extracted raw flash binary images from the ESP32: bootloader.bin, partitions.bin, and firmware.bin.
  2. I loaded firmware.elf and decompiled binary symbols into Ghidra, the open-source software reverse engineering suite.
  3. By analyzing function calls, memory maps, and string tables in Ghidra, I audited how sensitive data (such as Wi-Fi credentials in NVS and API authentication tokens) was represented in memory.

This security auditing exercise reinforced the vital importance of enabling ESP32 Flash Encryption and Secure Boot v2 before deploying embedded devices in production environments.


From Breadboard Prototype to Custom PCB Design

A breadboard with jumper wires is acceptable for proving firmware logic on a workbench. However, breadboards are fundamentally unsafe for permanent installation inside a wall cavity. Jumper wires loosen over time, exposed pins risk accidental shorts, and high-voltage AC conductors must never share a flimsy prototype board with delicate 3.3V logic.

Figure 6: Top-down hardware bench setup showing the ESP32 microcontroller, decoupled battery power pack, and 4-channel relay module.
Figure 6: Top-down hardware bench setup showing the ESP32 microcontroller, decoupled battery power pack, and 4-channel relay module.

To transition Smart Switch toward a production-grade form factor, I developed a comprehensive 2-Layer Custom PCB Architecture:

1. Galvanic Separation & Physical Isolation Slots

On a custom PCB, the board is physically divided into two distinct zones:

  • Low-Voltage Zone (DC): Houses the ESP32, 3.3V voltage regulation circuitry, status LEDs, and low-voltage screw terminals for the manual wall switches.
  • High-Voltage Zone (AC): Houses the 230V AC mains input terminals, fast-blow fuses, relays, and load output screw terminals.
  • Milled Isolation Slots: Physical air gaps (isolation slots) milled directly through the FR4 fiberglass substrate beneath the optocouplers and relay contact pads to eliminate surface creepage breakdown under high ambient humidity.

2. Onboard Isolated AC-DC Power Supply

Rather than powering the switch through an external micro-USB cable, the custom hardware design incorporates an embedded Hi-Link HLK-PM01 isolated AC-DC converter module:

  • Ingests 90–264V AC household mains.
  • Outputs a regulated, isolated 5V DC rail to power the relay coils and ESP32 3.3V LDO regulator.
  • Features built-in thermal protection, short-circuit protection, and galvanic isolation rated up to 3000V AC.

3. Mains Protection Circuitry

To protect against voltage surges and lightning transients common on regional electrical grids, the input stage incorporates:

  • A 2A Fast-Blow Fuse to disconnect mains current in the event of an internal short.
  • A Metal Oxide Varistor (MOV - 14D471K) connected across Live and Neutral to clamp high-voltage spikes.
  • A Flyback Diode (1N4007) across each relay coil to suppress inductive voltage kickback when the magnetic coil de-energizes.

Implemented vs. Designed vs. Planned

To maintain absolute technical integrity, here is the exact status of each capability across the Smart Switch project:

Feature / Architecture LayerStatusTechnical Implementation Details
4-Channel Active-LOW Relay ControlImplementedSongle relays on GPIOs 27, 14, 12, 13 with safe-boot OFF enforcement
4-Gang Physical Wall Switch InputsImplementedDebounced mechanical inputs on GPIOs 32, 33, 18, 19 with INPUT_PULLUP
Boot-Time Deterministic State SyncImplementedReads physical switches at boot; eliminates power-cut relay chatter
Local Captive Portal & Web DashboardImplementedAsync web server on port 80; controls relays at 192.168.4.1/dash
Local Network mDNS ResolutionImplementedResolves to http://smartswitch.local on home Wi-Fi network
NVS Persistent State StorageImplementedSaves Wi-Fi credentials and preferences using ESP32 Preferences library
Modular PlatformIO C++ CodebaseImplementedStructured separation of config, control, core, network, and system layers
Firmware Reverse Engineering AuditImplementedBinary dumps analyzed in Ghidra for credential exposure testing
Custom 2-Layer PCB & AC-DC PowerDesignedIsolation slots, HLK-PM01 AC-DC module, fuse + MOV protection layout
HLW8032 Energy MonitoringPlannedCurrent and voltage sensing IC for future energy analytics
BLE Mobile ProvisioningPlannedNative mobile onboarding over Bluetooth Low Energy

Mains Safety Considerations

Working with mains electricity is serious. 230V AC can cause severe injury, electrocution, and electrical fires if handled carelessly.

Throughout this project, I maintained strict safety protocols:

  • Zero AC on the Microcontroller: The ESP32 and physical wall switches operate exclusively on low-voltage DC signals (3.3V / 5V). At no point does alternating current pass through the wall switch lines or ESP32 GPIOs.
  • Galvanic Optocoupler Isolation: The relay board uses optical transmission—an infrared LED illuminating a phototransistor across an air gap—ensuring no electrical path exists between high-voltage contacts and low-voltage logic.
  • Proper Enclosures Required: A prototype on a desk must never be installed inside a home wall cavity without a flame-retardant, properly rated electrical junction enclosure.

This build story is an engineering case study in embedded systems design, not a casual DIY electrical guide.


What I Learned Building Smart Switch

Building Smart Switch taught me lessons that software tutorials and simulator tools never reveal:

  1. Hardware Dictates Software Architecture: You cannot write clean embedded code in an abstract vacuum. Hardware quirks—such as floating inputs on GPIO 34–39, contact bounce on mechanical toggles, and active-LOW relay polarities—directly shape how your C++ state machines must be structured.
  2. Offline-First Is Not Optional in IoT: The moment an IoT device fails because an internet server is down, it ceases to be useful. Designing systems that work locally by default creates products that users can genuinely trust.
  3. Debugging Requires Full-Stack Visibility: From testing lithium battery voltage sag with a multimeter to inspecting serial telemetry at 115200 baud and auditing binary firmware dumps in Ghidra, building hardware forces you to master every layer between physical electrons and application code.

Conclusion

Smart Switch started as a desire to build something tangible—a project that crossed the boundary between lines of code and real, clicking electrical hardware.

By combining the processing power of the ESP32, the reliability of active-LOW relays, a deterministic boot synchronization algorithm, and a local captive portal web interface, it evolved from early battery experiments into a robust, working home automation prototype.

Building it reinforced my belief that great engineering isn't about making devices that look flashy on a cloud dashboard; it's about making systems that work every single time someone flips a switch on the wall.


Explore Smart Switch & Related Work

Written by Javed HussainLast updated on 2026-03-15