Skip to content

ESP32 · Build guide

ESP32 Thief Detection System with IR Sensor, LCD & Buzzer

Build an ESP32 intruder alarm with an IR sensor, I2C LCD, LED and buzzer. Wiring, tested code, a clear explanation of the siren and counter logic, sensor choices and fixes for common problems.

Difficulty
Beginner
Read time
14 min
Board
ESP32
Author
Manish Pal
Published
Oct 24, 2026
Views
0
ESP32 Thief Detection System with IR Sensor, LCD & Buzzer

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:

  1. A red LED lights up.
  2. A buzzer sounds a rapid two-tone siren.
  3. 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.

Image pending: The finished thief detection system: ESP32, IR sensor, I2C LCD, red LED and buzzer on a breadboard

Components

9 items
NameQtyLink
ESP32 DevKit V1 (30 or 38 pin)×1—
IR obstacle avoidance sensor module (FC-51 type)×1—
16×2 LCD display with I2C backpack (PCF8574)×1—
Piezo buzzer (passive recommended, active also works)×1—
5 mm LED, red×1—
220 Ω resistor for the LED×1—
Breadboard×1—
Jumper wires (male-to-male and male-to-female)×15—
USB data cable×1—

Need 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().

Logic flow of one pass through loop(): read the sensor, count new detections, drive the LED, siren and LCD

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:

SensorHow it detectsBest for
IR reflective module (this project)Reflected infrared lightShort-range proximity, learning, desk demos
PIR motion sensor (HC-SR501)Change in body heat across a wider angleDetecting people entering a room
Ultrasonic (HC-SR04)Echo time of a sound pulseMeasuring distance, wider cone than IR
Break-beam pair (emitter + receiver)Interrupted beam across a gapDoorways and hallways, very reliable
Magnetic reed switchMagnet moves away from a switchDoors 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.

  1. In File → Preferences, add this to Additional boards manager URLs: https://espressif.github.io/arduino-esp32/package_esp32_index.json
  2. Open Tools → Board → Boards Manager, search for esp32 and install esp32 by Espressif Systems (3.0.0 or later).
  3. Select Tools → Board → esp32 → ESP32 Dev Module.
  4. 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.

PartPinESP32 pin
IR sensorVCC3V3
IR sensorGNDGND
IR sensorOUTGPIO23
I2C LCDVCCVIN (5 V)
I2C LCDGNDGND
I2C LCDSDAGPIO21
I2C LCDSCLGPIO19
Red LEDAnode (long leg)GPIO22 through a 220 Ω resistor
Red LEDCathode (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.

Wiring diagram: IR sensor on GPIO23, I2C LCD on GPIO21 and GPIO19, LED on GPIO22 and buzzer on GPIO25

⚠️ 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.

C++

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:

C++

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

  1. 1Wire everything and upload the main sketch. The LCD should show IR SECURITY / SYSTEM READY for two seconds, then No Object / Count:0.
  2. 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.
  3. 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.
  4. 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.
  5. 5Pull your hand away. The LED and siren should stop, and the screen should show No Object with the count still at 1.
  6. 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.
  7. 7Mount the sensor where it will face the area you want to protect, away from windows and direct sunlight, and test again in place.
Image pending: The LCD showing ObjectDetected and the detection counter while the red LED is lit

Troubleshooting

SymptomLikely causeFix
Alarm is always on, or never triggersSensor range set too high or too lowTurn the module's potentiometer until its indicator LED lights only when your hand is in front of it.
Alarm triggers near a window or lampAmbient infrared lightMove the sensor away from sunlight, or shade it with a short tube.
LCD shows blank boxes or nothingContrast too low, or wrong I2C addressTurn the contrast screw on the backpack, then run the I2C scanner and use the address it reports.
Backlight on but no textWrong SDA/SCL pinsThe sketch expects SDA on GPIO21 and SCL on GPIO19. Wiring SCL to GPIO22 by habit leaves the screen blank.
Compile error: ledcAttach not declaredESP32 core older than 3.0Update the board package, or use the channel-based LEDC functions shown above.
Compile error about toneUsing Arduino Uno codeUse ledcWriteTone as in this sketch.
Siren is very quietBuzzer type or voltageUse a passive piezo, or drive a 5 V buzzer through a transistor.
Count increases by more than 1 per passSensor flickers at its thresholdReduce the range and avoid reflective, uneven surfaces.
Board resets when the siren startsWeak USB supplyUse 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:

C++

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.

Topicsesp32securityir-sensoralarmi2c-lcdbuzzerledledc

Questions & Answers