What You Will Build
Home security does not have to be expensive or complicated, and building an alarm is one of the most satisfying ways to learn ESP32 development. In this project you will assemble a thief detection system around an ESP32 and an infrared (IR) obstacle sensor. When something enters the sensor's field of view, the system reacts in three ways at once:
- A red LED lights up.
- A buzzer sounds a rapid two-tone siren.
- A 16×2 I2C LCD shows ObjectDetected and a running count of how many separate intrusions have happened since power-up.
When the object leaves, the LED and buzzer stop, the screen returns to No Object, and the counter keeps its value.
The project teaches four skills that carry over to almost everything else you will build: reading a digital sensor, detecting a change of state instead of a level, driving an I2C display, and generating audio tones with the ESP32's LEDC hardware.
Time: about one hour. Skill level: beginner. No soldering required.
Components
9 itemsNeed these parts? Get this build sheet by email
Parts list with buy links, plus new builds as they're published. By subscribing you agree to our Privacy Policy. Unsubscribe anytime.
How the System Works
The logic is a simple loop that repeats thousands of times a second: sense, decide, alert.
- Sense: the ESP32 reads the IR sensor's output pin.
- Decide: it compares the new reading with the previous one. A change from "nothing there" to "something there" is a new intrusion and increases the counter. A reading that stays the same is just the same intruder still standing there.
- Alert: depending on the result, the LED, buzzer and LCD are updated.
The diagram below shows one pass through loop().
Choosing the Right Sensor
An IR obstacle module is a good learning sensor, but it helps to know what it really does. The board contains an infrared LED that constantly shines, and an infrared photodiode next to it. If an object is close enough to reflect that light back, the photodiode sees it, a comparator chip compares the signal with a threshold you set using the small potentiometer, and the output pin goes LOW. A small on-board LED usually lights up at the same moment, which makes testing easy.
That design has consequences worth understanding before you mount it on a door:
- It senses reflection, not people. Anything that reflects IR (a hand, a box, a wall) counts. Matt black surfaces absorb IR and may not be seen at all.
- Range is short. Typical modules detect from a few centimetres to roughly 30 cm, set by the potentiometer.
- Sunlight and some lamps contain infrared. Strong ambient IR can cause false triggers or blind the sensor, so it works best indoors.
If you want to protect a real doorway or room, other sensors are a better fit:
| Sensor | How it detects | Best for |
|---|---|---|
| IR reflective module (this project) | Reflected infrared light | Short-range proximity, learning, desk demos |
| PIR motion sensor (HC-SR501) | Change in body heat across a wider angle | Detecting people entering a room |
| Ultrasonic (HC-SR04) | Echo time of a sound pulse | Measuring distance, wider cone than IR |
| Break-beam pair (emitter + receiver) | Interrupted beam across a gap | Doorways and hallways, very reliable |
| Magnetic reed switch | Magnet moves away from a switch | Doors and windows opening |
The good news is that the code below works with any of them that outputs a clean digital signal. Swap the sensor, flip the logic level if needed, and the rest of the project stays the same.
Software Setup
This sketch uses the pin-based LEDC functions (ledcAttach and ledcWriteTone) that were introduced in ESP32 Arduino core 3.0, so you need version 3.0 or newer.
- In File → Preferences, add this to Additional boards manager URLs:
https://espressif.github.io/arduino-esp32/package_esp32_index.json - Open Tools → Board → Boards Manager, search for esp32 and install esp32 by Espressif Systems (3.0.0 or later).
- Select Tools → Board → esp32 → ESP32 Dev Module.
- Open Sketch → Include Library → Manage Libraries, search for LiquidCrystal I2C and install the one by Frank de Brabander.
The Arduino IDE may warn that the library "claims to run on avr architecture(s) and may be incompatible" with your ESP32. For this library the warning is harmless and the code compiles and runs normally.
⚠️ Warning
If you are stuck on an older ESP32 core (2.x), the sketch will fail with "ledcAttach was not declared". Either update the core, or replace the buzzer lines using the channel-based API: in setup() use ledcSetup(0, 1000, 8); ledcAttachPin(BUZZER_PIN, 0); and in the loop call ledcWriteTone(0, 1000) and ledcWriteTone(0, 2000) with channel 0 instead of the pin number.
Wiring the Security System
The ESP32 has plenty of GPIO pins, but they need routing with some care. The sketch defines custom I2C pins with Wire.begin(21, 19), so the display's SDA goes to GPIO21 and its SCL goes to GPIO19. Because the LED sits on GPIO22, which is the board's default SCL pin, moving the I2C clock to GPIO19 keeps the two from colliding.
| Part | Pin | ESP32 pin |
|---|---|---|
| IR sensor | VCC | 3V3 |
| IR sensor | GND | GND |
| IR sensor | OUT | GPIO23 |
| I2C LCD | VCC | VIN (5 V) |
| I2C LCD | GND | GND |
| I2C LCD | SDA | GPIO21 |
| I2C LCD | SCL | GPIO19 |
| Red LED | Anode (long leg) | GPIO22 through a 220 Ω resistor |
| Red LED | Cathode (short leg) | GND |
| Buzzer | + | GPIO25 |
| Buzzer | − | GND |
Powering the IR sensor from 3.3 V keeps its output at 3.3 V logic, which is what the ESP32 expects.
⚠️ Warning
Most I2C LCD backpacks have pull-up resistors on SDA and SCL that connect to the backpack's own VCC. When the display is powered from 5 V, those pull-ups hold the lines at 5 V, which is above the ESP32's 3.6 V pin limit. Many makers run this setup for a long time without trouble, but it is outside the specification. For a robust build, either power the backpack from 3V3 (the backlight is dimmer and the contrast may need adjusting), use a small bidirectional I2C level shifter between the ESP32 and the display, or remove the backpack's pull-up resistors and rely on the ESP32's pull-ups.
💡 Tip
A small piezo buzzer draws only a few milliamps, so it is fine to drive directly from GPIO25 as in this build. If you use a larger or louder 5 V buzzer, do not connect it straight to a GPIO pin. Switch it with an NPN transistor (for example a 2N2222 or BC547 with a 1 kΩ resistor between GPIO25 and the base) or a MOSFET module, and power it from 5 V.
The Complete Code
Copy the sketch below into a new Arduino IDE window and upload it.
How the Code Works
There are a few techniques in this sketch that make it behave like a proper alarm rather than a basic beginner script.
1. Active-LOW detection
Most IR obstacle modules pull their output pin LOW when they see something and leave it HIGH otherwise. The line bool objectDetected = (digitalRead(IR_SENSOR) == LOW); converts that into a plain true/false variable, so the rest of the code reads naturally. If your module is wired the opposite way, change == LOW to == HIGH and nothing else needs to change.
2. Counting an intrusion once
If a thief stands in front of the sensor for five seconds, the loop runs thousands of times in that period. Counting every pass would send the counter into the thousands. The sketch avoids this by remembering the last reading in previousState. The counter goes up only when the reading changes from not detected to detected. This is called edge detection, and you will use the same trick for buttons, doors, pulse counters and more.
3. The LEDC siren
An Arduino Uno has a tone() function, but early ESP32 cores did not, and the ESP32 generates square waves with its LEDC (LED Control) hardware peripheral instead. ledcAttach(BUZZER_PIN, 1000, 8) hands GPIO25 to LEDC. After that, ledcWriteTone(BUZZER_PIN, 1000) outputs a 1 kHz tone, and ledcWriteTone(BUZZER_PIN, 0) silences it. The loop swaps between 1000 Hz and 2000 Hz every 80 ms, which makes the classic two-tone alarm sound that is much harder to ignore than a flat beep.
This is where your buzzer choice matters. A passive buzzer has no internal oscillator, so it plays exactly the frequency you send, and you hear a clean siren. An active buzzer has its own fixed-pitch oscillator and only needs power. Driven with a 1 kHz and 2 kHz signal it still sounds an alarm, but you hear two different rough warble rates rather than two clean notes. Both are loud enough for this project.
4. Padding text on the LCD
An LCD does not clear characters you do not overwrite. Switching from "ObjectDetected" (14 characters) to "No Object" (9 characters) would leave "No Objectted" on the screen. The sketch avoids that by padding shorter strings with trailing spaces, such as "No Object ", which wipes the leftovers. The same applies to the counter line, which is padded with four spaces so the number never leaves stray digits behind.
5. Blocking delays
The two delay(80) calls make the siren simple to read, but they also mean the sketch cannot do anything else while the alarm is sounding. That is fine for this project. If you later add Wi-Fi alerts or a keypad, replace them with timing based on millis(), as shown in the improvements section.
Testing and Calibration
Step 1: Find your LCD's I2C address
Most backpacks use address 0x27, but some use 0x3F. Upload this scanner first if you are not sure:
Open the Serial Monitor at 115200 baud. If the scanner prints 0x3F instead of 0x27, change the first line of the main sketch to LiquidCrystal_I2C lcd(0x3F, 16, 2);.
Step 2: Tune the IR sensor, then the rest
Steps
- 1Wire everything and upload the main sketch. The LCD should show IR SECURITY / SYSTEM READY for two seconds, then No Object / Count:0.
- 2If the screen is blank but the backlight is on, turn the small blue contrast screw on the back of the I2C backpack until the characters appear.
- 3With nothing in front of the sensor, check that the sensor’s own indicator LED is off. If it is on, turn the potentiometer slowly until it switches off.
- 4Hold your hand about 10 cm from the sensor. The indicator LED should light, the red LED should light, the siren should sound and the LCD should show ObjectDetected, Count:1.
- 5Pull your hand away. The LED and siren should stop, and the screen should show No Object with the count still at 1.
- 6Wave your hand in and out three times. The counter should end at 4. If it counts more, the sensor is flickering at its threshold, so reduce the range slightly with the potentiometer.
- 7Mount the sensor where it will face the area you want to protect, away from windows and direct sunlight, and test again in place.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Alarm is always on, or never triggers | Sensor range set too high or too low | Turn the module's potentiometer until its indicator LED lights only when your hand is in front of it. |
| Alarm triggers near a window or lamp | Ambient infrared light | Move the sensor away from sunlight, or shade it with a short tube. |
| LCD shows blank boxes or nothing | Contrast too low, or wrong I2C address | Turn the contrast screw on the backpack, then run the I2C scanner and use the address it reports. |
| Backlight on but no text | Wrong SDA/SCL pins | The sketch expects SDA on GPIO21 and SCL on GPIO19. Wiring SCL to GPIO22 by habit leaves the screen blank. |
Compile error: ledcAttach not declared | ESP32 core older than 3.0 | Update the board package, or use the channel-based LEDC functions shown above. |
Compile error about tone | Using Arduino Uno code | Use ledcWriteTone as in this sketch. |
| Siren is very quiet | Buzzer type or voltage | Use a passive piezo, or drive a 5 V buzzer through a transistor. |
| Count increases by more than 1 per pass | Sensor flickers at its threshold | Reduce the range and avoid reflective, uneven surfaces. |
| Board resets when the siren starts | Weak USB supply | Use a better cable or a 5 V supply that can deliver at least 500 mA. |
Making It Better
A siren that does not block
Replace the two delay(80) calls with a timer so the loop keeps running during the alarm. Add these two variables above setup(), and a function:
More upgrades to try
- Debounce the sensor. Require the sensor to stay in the new state for about 50 ms before accepting the change. This removes false counts from flicker.
- Arming delay. Add a push button that arms the system after a ten-second countdown so you can leave the room without triggering it.
- Wi-Fi alerts. Because the ESP32 has Wi-Fi built in, you can send a silent notification to your phone through a Telegram bot (the Universal Arduino Telegram Bot library) or publish the event to a home-automation broker over MQTT with the PubSubClient library.
- Latching alarm. Keep the siren running until a reset button is pressed, so a quick intruder still triggers a full alarm.
- Switch a light. Add a relay module on another GPIO to turn on a lamp when the alarm trips, always keeping mains wiring in an insulated enclosure.
- Better placement. Mount the sensor on the door frame at hip height, pointing across the opening, and use a pair of break-beam sensors if you need reliable doorway detection.
An Honest Note on Security
This is a learning project. It will teach you how an alarm works, but it has no tamper protection, no battery backup and no approved certification. Do not rely on it as the only protection for valuables or people.
Frequently Asked Questions
Can I use a PIR sensor instead of the IR module? Yes. Connect its output to GPIO23, power it from 5 V as its datasheet requires, and change the detection test to == HIGH, because PIR modules output HIGH on motion. Allow about a minute for it to settle after power-up.
Why does the counter reset when I unplug the board? The count lives in RAM. To keep it across power cycles, store it with the ESP32 Preferences library.
Can I power the project from a battery? Yes, through the 5 V VIN pin from a regulated supply or a power bank. The LCD backlight is the biggest drain, so switch it off with lcd.noBacklight() when idle.
Why GPIO23 for the sensor? It is a general-purpose pin with no boot-time function, so nothing interferes with startup. Avoid GPIO0, 2, 5, 12 and 15 for sensors that might hold a level during reset.
Summary
You built a complete ESP32 intruder alarm: an IR sensor feeding an edge-detecting counter, a two-tone LEDC siren, a flashing LED and a live LCD readout. Along the way you learned how reflective IR sensing works and where it fails, why the I2C lines need care when a display runs at 5 V, how to count events once instead of continuously, and how to bring the project up to a professional standard with non-blocking timing and Wi-Fi notifications.

