Motion-Activated Alarm Easy

Build a motion-activated alarm using a PIR sensor, buzzer, and LED

🔗Goal

Build a motion-activated alarm system using a PIR sensor, a buzzer, and an LED. When the system is armed and motion is detected, the LED turns on and the buzzer sounds a rapid alarm for 5 seconds. A push button lets you arm and disarm the system, with distinct audio feedback for each state.

Here is how the system works at a high level:

graph LR
    A[PIR Sensor] -->|GPIO 13| B[ESP32]
    F[Push Button] -->|GPIO 14| B
    B -->|GPIO 25| C[LED]
    B -->|GPIO 26| D[Buzzer]
    B -->|Serial| E[Monitor]

The PIR (Passive Infrared) sensor detects changes in infrared radiation caused by a warm body moving through its field of view. When triggered, it outputs a HIGH signal on its data pin. The ESP32 uses an interrupt to catch this signal instantly, without polling.

🔗Prerequisites

You will need the following components:

ComponentQtyNotesBuy
ESP32 dev board1AliExpress | Amazon.de .co.uk .com
PIR sensor (HC-SR501)1Has adjustable sensitivity and delay potentiometersAliExpress | Amazon.de .co.uk .com
Active buzzer13.3V or 5V compatibleAliExpress | Amazon.de .co.uk .com
LED (red)1Any standard 5mm LEDAliExpress | Amazon.de .co.uk .com
220 ohm resistor1Current-limiting resistor for the LEDAliExpress | Amazon.de .co.uk .com
Push button (tactile)1Momentary tactile switchAliExpress | Amazon.de .co.uk .com
10k ohm resistor1Pull-down resistor for the push buttonAliExpress | Amazon.de .co.uk .com
Breadboard1AliExpress | Amazon.de .co.uk .com
Jumper wires~12Male-to-male and male-to-femaleAliExpress | Amazon.de .co.uk .com

Links marked Amazon/AliExpress are affiliate links. We may earn a small commission at no extra cost to you.

The current through the LED is limited by the resistor. For a red LED with a forward voltage of $V_f = 2.0\,\text{V}$:

$$I = \frac{V_{supply} - V_f}{R} = \frac{3.3 - 2.0}{220} \approx 5.9\,\text{mA}$$

This is well within the ESP32 GPIO limit of $12\,\text{mA}$ per pin.

See our PIR sensor guide for more on how PIR sensors work, including adjusting the sensitivity and trigger delay.

See our buzzer guide for the difference between active and passive buzzers.

🔗Tutorial

🔗Step 1: Wiring

Connect all the components to the ESP32 as shown in the table below:

ComponentPinESP32 PinNotes
PIR sensorVCC5V (VIN)HC-SR501 requires 5V power
PIR sensorGNDGND
PIR sensorOUTGPIO 13HIGH when motion detected
LED (anode, long leg)--GPIO 25Through 220 ohm resistor
LED (cathode, short leg)--GND
Active buzzer (+)--GPIO 26
Active buzzer (-)--GND
Push buttonSide AGPIO 14
Push buttonSide B3.3V
10k ohm resistorGPIO 14 to GNDPull-downKeeps pin LOW when button is not pressed

The HC-SR501 PIR sensor needs 5V to operate, but its output signal is 3.3V, which is safe for ESP32 GPIO pins. Use the VIN (5V) pin on the ESP32 to power it.

Pin labels and GPIO numbers vary between ESP32 boards. Always check your board's pinout diagram and datasheet.

🔗Step 2: Adjust the PIR sensor

The HC-SR501 has two small potentiometers on the back:

PotentiometerFunctionRecommended setting
SensitivityDetection range (3m to 7m)Turn clockwise for maximum range
Time delayHow long OUT stays HIGH after detection (3s to 300s)Turn fully counter-clockwise for minimum (about 3 seconds)

There is also a jumper that selects the trigger mode:

  • H (repeatable trigger): OUT stays HIGH as long as motion continues. Use this mode.
  • L (single trigger): OUT goes HIGH once, then LOW, even if motion continues.

🔗Step 3: Upload the code

The system uses an interrupt on the PIR pin to detect motion instantly, and another interrupt on the button pin to toggle the armed/disarmed state.

#define PIR_PIN    13
#define LED_PIN    25
#define BUZZER_PIN 26
#define BUTTON_PIN 14

// ----- State -----
volatile bool motionDetected = false;
volatile bool armed = false;
volatile unsigned long lastButtonPress = 0;

// ----- ISR: PIR sensor -----
void IRAM_ATTR onMotion() {
    if (armed) {
        motionDetected = true;
    }
}

// ----- ISR: Button press -----
void IRAM_ATTR onButtonPress() {
    unsigned long now = millis();
    // Debounce: ignore presses within 300ms of the last one
    if (now - lastButtonPress > 300) {
        lastButtonPress = now;
        armed = !armed;
    }
}

// ----- Buzzer patterns -----
void beepShort(int count) {
    for (int i = 0; i < count; i++) {
        digitalWrite(BUZZER_PIN, HIGH);
        delay(100);
        digitalWrite(BUZZER_PIN, LOW);
        delay(100);
    }
}

void beepLong() {
    digitalWrite(BUZZER_PIN, HIGH);
    delay(500);
    digitalWrite(BUZZER_PIN, LOW);
}

void alarmSequence() {
    unsigned long start = millis();
    // Rapid beeping for 5 seconds
    while (millis() - start < 5000) {
        digitalWrite(BUZZER_PIN, HIGH);
        digitalWrite(LED_PIN, HIGH);
        delay(100);
        digitalWrite(BUZZER_PIN, LOW);
        digitalWrite(LED_PIN, LOW);
        delay(100);
    }
    // Ensure both are off after alarm ends
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(LED_PIN, LOW);
}

void setup() {
    Serial.begin(115200);

    pinMode(PIR_PIN, INPUT);
    pinMode(LED_PIN, OUTPUT);
    pinMode(BUZZER_PIN, OUTPUT);
    pinMode(BUTTON_PIN, INPUT);  // External pull-down resistor

    // Attach interrupts
    attachInterrupt(digitalPinToInterrupt(PIR_PIN), onMotion, RISING);
    attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonPress, RISING);

    // Start disarmed
    armed = false;
    digitalWrite(LED_PIN, LOW);
    digitalWrite(BUZZER_PIN, LOW);

    Serial.println("Motion Alarm System ready.");
    Serial.println("Press the button to arm/disarm.");
    Serial.println("Status: DISARMED");

    // Give the PIR sensor time to stabilize (20-60 seconds)
    Serial.println("Waiting for PIR sensor to stabilize...");
    delay(2000);  // Minimum warm-up; HC-SR501 may take up to 60 seconds for full accuracy
    Serial.println("PIR sensor ready.");
}

// Track previous armed state to detect changes
bool previousArmed = false;

void loop() {
    // Handle arm/disarm state change
    if (armed != previousArmed) {
        previousArmed = armed;
        if (armed) {
            Serial.println("Status: ARMED");
            beepShort(3);  // 3 short beeps = armed
        } else {
            Serial.println("Status: DISARMED");
            beepLong();    // 1 long beep = disarmed
            motionDetected = false;  // Clear any pending detection
            digitalWrite(LED_PIN, LOW);
        }
    }

    // Handle motion detection
    if (motionDetected) {
        motionDetected = false;
        unsigned long timestamp = millis() / 1000;
        Serial.print("[");
        Serial.print(timestamp);
        Serial.println("s] MOTION DETECTED! Alarm triggered.");
        alarmSequence();
    }

    delay(50);  // Small delay to reduce CPU usage
}

🔗Step 4: Test the system

  1. Upload the sketch and open the Serial Monitor at 115200 baud
  2. Wait a few seconds for the PIR sensor to stabilize — the Serial Monitor will tell you when it is ready
  3. Press the push button — you should hear 3 short beeps and see "Status: ARMED" in the Serial Monitor
  4. Walk in front of the PIR sensor — the LED should flash and the buzzer should beep rapidly for 5 seconds
  5. Press the button again to disarm — you should hear 1 long beep

The state machine for the system:

stateDiagram-v2
    [*] --> Disarmed
    Disarmed --> Armed : Button press (3 beeps)
    Armed --> Disarmed : Button press (1 long beep)
    Armed --> Alarm : Motion detected
    Alarm --> Armed : After 5 seconds

🔗Step 5: Fine-tune the PIR sensor

If the alarm triggers too easily (false positives) or not at all:

  • False triggers: Turn the sensitivity potentiometer counter-clockwise to reduce range. Avoid pointing the sensor at windows (sunlight and moving curtains cause false triggers)
  • No detection: Turn the sensitivity clockwise. Make sure the jumper is set to H (repeatable trigger)
  • Detection lingers too long: Turn the time delay potentiometer counter-clockwise for shorter output pulse duration

🔗Common Issues and Solutions

ProblemCauseFix
PIR triggers constantlySensor pointed at heat source, window, or air ventReposition the sensor. Avoid direct sunlight and moving heat sources
PIR never triggersSensor not warmed up, or sensitivity too lowWait 60 seconds after power-on. Turn sensitivity potentiometer clockwise
Button does not toggle arm/disarmWiring issue or missing pull-down resistorEnsure the 10k ohm pull-down resistor connects GPIO 14 to GND. Without it, the pin floats
Buzzer is quiet or silentWrong buzzer type or insufficient voltageConfirm you have an active buzzer (not passive). Check that it is rated for 3.3V or 5V
LED does not light upLED inserted backwardsThe longer leg (anode) goes to GPIO 25 through the resistor. The shorter leg (cathode) goes to GND
Alarm triggers immediately on armingPIR output is still HIGH from warm-upAdd a longer delay after arming, or clear the interrupt flag before arming
Serial Monitor shows garbled textWrong baud rateSet the Serial Monitor to 115200 baud