Smart plugs switch things on from an app and off again with a timer. In this lesson you build the electronics behind one: a relay you control from the Blynk app or from a push button on the breadboard. Both always show the same state, and a countdown switches the relay off after the time you set. For safety, this project switches low-voltage things only.
What you'll learn
- How to keep a physical button and an app switch in sync
- How active-LOW relay modules work
- How to build a countdown timer with
millis() - Why the device, not the app, decides the state after a restart
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- 2-channel relay module (ESP32 IoT Starter Kit)
- Button module (45-in-1 Sensor Kit)
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
- A free Blynk account and the Blynk IoT app on a phone
- A 2.4 GHz Wi-Fi network
Safety first
Never connect the relay to mains electricity (the sockets in your wall). This lesson only switches the relay itself: you hear it click and see its LED. To switch a real load, use a battery-powered one, such as the DC motor with the 9 V battery from the kit.
Wiring

| From | To |
|---|---|
| Relay VCC | 5V |
| Relay GND | GND |
| Relay IN1 | ESP32 GPIO 23 |
| Button module S | ESP32 GPIO 27 |
| Button module + | 3V3 |
| Button module − | GND |
Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.
Set up Blynk
In the Blynk console, create a new template called Smart switch (hardware ESP32, connection type Wi-Fi) and add these datastreams. Lesson 1 shows each step in detail.
| Virtual pin | Name | Data type | Range | Phone widget |
|---|---|---|---|---|
| V0 | Power | Integer | 0 – 1 | Button (switch mode) |
| V1 | Auto-off | Integer | 0 – 60, units min | Slider |
| V2 | Timer | String | n/a | Value Display |
Save the template, create a new device from it and copy its three BLYNK_… lines. In the phone app, open the device, tap the wrench icon and add a Button in switch mode (V0), a Slider (V1) and a Value Display (V2). Link each widget to its datastream.

The sketch
Replace the three BLYNK_… lines at the top with the ones from your device's Device info page, and add your Wi-Fi name and password (2.4 GHz only). The BLYNK_… lines must stay above #include <BlynkSimpleEsp32.h>.
Some versions of this module switch their output to HIGH when they trigger, others to LOW. The sketch reads the output when it starts (keep things still for a moment) and treats any change from that resting state as a trigger, so it works with both.
#define BLYNK_TEMPLATE_ID "YOUR_TEMPLATE_ID"
#define BLYNK_TEMPLATE_NAME "Smart switch"
#define BLYNK_AUTH_TOKEN "YOUR_AUTH_TOKEN"
#define BLYNK_PRINT Serial // show connection messages in the Serial Monitor
#include <WiFi.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>
char ssid[] = "YOUR_WIFI_NAME";
char pass[] = "YOUR_WIFI_PASSWORD";
const int RELAY_PIN = 23;
const int BUTTON_PIN = 27;
const int RELAY_ON = LOW; // most relay modules switch on with LOW
const int RELAY_OFF = HIGH;
BlynkTimer timer;
bool relayOn = false;
unsigned long onSince = 0;
int autoOffMinutes = 0; // 0 = no timer
int buttonRest;
bool wasPressed = false;
void setRelay(bool on) {
relayOn = on;
digitalWrite(RELAY_PIN, on ? RELAY_ON : RELAY_OFF);
onSince = millis();
Blynk.virtualWrite(V0, on); // keep the app's switch in sync
Serial.println(on ? "Relay on" : "Relay off");
}
BLYNK_WRITE(V0) { setRelay(param.asInt()); }
BLYNK_WRITE(V1) { autoOffMinutes = param.asInt(); }
BLYNK_CONNECTED() {
Blynk.syncVirtual(V1); // get the timer setting
Blynk.virtualWrite(V0, relayOn); // tell the app the real relay state
}
void checkButton() {
bool pressed = digitalRead(BUTTON_PIN) != buttonRest;
if (pressed && !wasPressed) setRelay(!relayOn); // toggle on each press
wasPressed = pressed;
}
void updateTimer() {
if (!relayOn) { Blynk.virtualWrite(V2, "Off"); return; }
if (autoOffMinutes == 0) { Blynk.virtualWrite(V2, "On, no timer"); return; }
long left = autoOffMinutes * 60L - (long)((millis() - onSince) / 1000);
if (left <= 0) {
setRelay(false);
Blynk.virtualWrite(V2, "Switched off by the timer");
} else {
Blynk.virtualWrite(V2, String(left / 60) + " min " + String(left % 60) + " s left");
}
}
void setup() {
Serial.begin(115200);
digitalWrite(RELAY_PIN, RELAY_OFF); // make sure it starts off
pinMode(RELAY_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT);
delay(200);
buttonRest = digitalRead(BUTTON_PIN);
Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
timer.setInterval(30L, checkButton);
timer.setInterval(1000L, updateTimer);
}
void loop() {
Blynk.run();
timer.run();
}
How it works
Every change goes through one function, setRelay(), whether it comes from the app, the button or the timer. That function switches the relay and also writes the new state back to V0, so the app's switch moves when you press the real button. This is called two-way sync. After a restart the relay always starts off, and BLYNK_CONNECTED() tells the app so, instead of asking the app what to do. For a switch, that is the safer choice: a power cut never switches something on by surprise. The countdown compares millis() with the moment the relay was switched on, so it keeps working while Blynk runs.
Try this next
- Use the second relay channel (IN2) with a second button widget.
- Add a Time Input widget to switch on at a set time each day.
- Count how long the relay has been on today and show it in the app.
Troubleshooting
-
The device shows "Offline" in the app: check the Serial Monitor at 115200 baud. "Invalid auth token" means the
BLYNK_AUTH_TOKENline is wrong; if it never connects, check the Wi-Fi name and password, and that your network is 2.4 GHz. -
The relay is on when the app says off: your module is active HIGH. Swap
LOWandHIGHinRELAY_ONandRELAY_OFF. - The relay LED lights but it doesn't click: check that VCC is on 5V, not 3V3.
- One press toggles twice: increase the button check interval from 30 to 60 ms.