An alarm you control from your phone. The web page shows whether the alarm is armed, with buttons to arm and disarm it. When the PIR sensor detects motion while armed, the buzzer sounds and the LED flashes for five seconds, and the event is added to a list with the time.
What you need
- ESP32 board, expansion board and USB cable
- PIR motion sensor (HC-SR501)
- Active buzzer
- Red LED and 1 resistor, 220 Ω
- Breadboard and jumper wires (all from the ESP32 IoT Starter Kit)
- A 2.4 GHz Wi-Fi network
Good to know
Plug the ESP32 into the expansion board from the kit, or place it next to the breadboard, and connect the pins with jumper wires.
The PIR sensor needs 5V power, but its output signal is 3.3 V, so it's safe for the ESP32.
Wiring

| From | To |
|---|---|
| PIR VCC | 5V (breadboard + rail) |
| PIR OUT | ESP32 GPIO 27 |
| PIR GND | GND |
| Buzzer + (longer leg) | ESP32 GPIO 26 |
| Buzzer − | GND |
| ESP32 GPIO 25 | 220 Ω resistor, then the LED's long leg |
| LED short leg | GND |
| ESP32 5V and GND | Breadboard + and − rails |
Build it step by step
- Connect the ESP32's 5V and GND to the breadboard rails.
- Wire the PIR sensor, the buzzer and the LED.
- Enter your Wi-Fi details, upload the code and open the Serial Monitor at 115200 baud.
- Keep still for the 30-second warm-up, then open the address on your phone.
- Tap Arm, walk past the sensor and watch the list update.
The code
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details. The ESP32 only connects to 2.4 GHz Wi-Fi. After uploading, open the Serial Monitor at 115200 baud to see the address to open on your phone. The phone must be on the same Wi-Fi network.
#include <WiFi.h>
#include <WebServer.h>
#include <time.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TIME_ZONE = "EET-2EEST,M3.5.0/3,M10.5.0/4"; // Estonia; change for your country
const int PIR_PIN = 27;
const int BUZZER_PIN = 26;
const int LED_PIN = 25;
const unsigned long ALARM_TIME = 5000;
WebServer server(80);
bool armed = false;
unsigned long alarmEnd = 0; // 0 = no alarm running
const int LOG_SIZE = 10;
String events[LOG_SIZE];
int eventCount = 0;
String timeNow() {
struct tm t;
if (getLocalTime(&t, 10)) {
char text[20];
strftime(text, sizeof(text), "%d.%m %H:%M:%S", &t);
return String(text);
}
return String(millis() / 1000) + " s after start";
}
void addEvent(String text) {
for (int i = LOG_SIZE - 1; i > 0; i--) events[i] = events[i - 1];
events[0] = text;
if (eventCount < LOG_SIZE) eventCount++;
}
String page() {
String html = "<!DOCTYPE html><html><head><meta charset='utf-8'>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<meta http-equiv='refresh' content='5'><title>Motion alarm</title></head>";
html += "<body style='font-family:sans-serif;padding:16px'><h1>Motion alarm</h1>";
html += armed ? "<p style='font-size:24px;color:#c00'><b>ARMED</b></p>" : "<p style='font-size:24px;color:#555'><b>Disarmed</b></p>";
html += "<p><a href='/arm'><button style='font-size:20px'>Arm</button></a> ";
html += "<a href='/disarm'><button style='font-size:20px'>Disarm</button></a></p>";
html += "<h2>Recent detections</h2>";
if (eventCount == 0) html += "<p>None yet.</p>";
for (int i = 0; i < eventCount; i++) html += "<p>" + events[i] + "</p>";
html += "</body></html>";
return html;
}
void backHome() {
server.sendHeader("Location", "/");
server.send(303);
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Open http://");
Serial.println(WiFi.localIP());
configTzTime(TIME_ZONE, "pool.ntp.org");
server.on("/", []() { server.send(200, "text/html", page()); });
server.on("/arm", []() { armed = true; backHome(); });
server.on("/disarm", []() { armed = false; alarmEnd = 0; backHome(); });
server.begin();
Serial.println("Warming up the PIR sensor (30 seconds)...");
delay(30000);
Serial.println("Ready");
}
void loop() {
server.handleClient();
if (armed && alarmEnd == 0 && digitalRead(PIR_PIN) == HIGH) {
alarmEnd = millis() + ALARM_TIME;
addEvent(timeNow());
Serial.println("Motion detected!");
}
if (alarmEnd != 0 && millis() < alarmEnd) {
bool on = (millis() / 200) % 2; // beep and flash
digitalWrite(BUZZER_PIN, on);
digitalWrite(LED_PIN, on);
} else {
alarmEnd = 0;
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(LED_PIN, armed ? HIGH : LOW); // steady light = armed
}
}
How it works
The web server has three addresses: the status page, /arm and /disarm. The buttons simply open those addresses, which change armed and send you back to the status page. The alarm uses millis() instead of delay(), so the ESP32 keeps answering your phone while the buzzer is sounding. Each detection is stored with the time from an internet time server.
Make it your own
- Add a 10-second delay after arming so you can leave the room.
- Show how long ago each detection happened.
- Use the relay lesson to switch on a low-voltage light when motion is detected.
Troubleshooting
- False alarms: keep the sensor away from heaters and windows, and turn the sensitivity knob down.
- The buzzer is quiet: active buzzers are louder at 5V. That's normal at 3.3 V.
- The page doesn't open: make sure your phone is on the same Wi-Fi network.
Learn the basics first
New to these parts? These lessons explain them step by step: Detect motion with a PIR sensor and Control a relay from your phone over Wi-Fi.
0 comments