Motion Detection Security System using Raspberry pi pico Wokwi Simulator

PIR Motion Detection Security System – Raspberry Pi Pico in Wokwi | MakeMindz
🔒 Intermediate Security Project — #15

PIR Motion Detection
Security System
with Raspberry Pi Pico

Build a professional motion detection security system using a PIR sensor — trigger alarms, activate LED alerts, and display real-time status on a 16×2 LCD. Test everything free in Wokwi before touching hardware.

🐍 MicroPython 📡 HC-SR501 PIR 📟 16×2 I2C LCD 🔴 LED Alerts 🔔 Buzzer Alarm 🧪 Wokwi Ready
▶ Open Free Simulation
01 — Overview

Project Overview

This project implements a complete, professional-grade motion detection security system on a Raspberry Pi Pico. A PIR sensor detects human movement up to 7 metres, triggering a 5-second buzzer alarm, blinking red LED and LCD alert — all controlled through a clean 2-state machine with arm/disarm via a push button.

📡

PIR Detection

HC-SR501 senses movement up to 7m. Output HIGH triggers the alarm sequence.

🔒

Arm / Disarm

Single button toggles system state with confirmation beep and green LED.

🔴

Red LED Alert

Blinks rapidly during the full 5-second alarm duration.

🟢

Green LED Armed

Stays on while system is armed as a constant visual status indicator.

🔔

Buzzer Alarm

Pulse pattern for 5 seconds. Single beep on arm, double beep on disarm.

📟

LCD Status

16×2 I2C display shows ARMED / ALERT / detection counter in real time.

🔢

Event Counter

Tracks total intrusion detections, resets when system is re-armed.

⏱️

3s Cooldown

Prevents repeated triggers from a single movement event.


02 — Hardware

Components Required

Raspberry Pi Pico — RP2040, simulated in Wokwi
HC-SR501 PIR Sensor — Motion detection up to 7m
16×2 LCD (I2C) — Status display at address 0x27
Red LED — Alert indicator (motion detected)
Green LED — Armed indicator (system active)
Active Buzzer — Alarm sound output
Push Button — ARM / DISARM toggle
2× 220Ω Resistors — LED current limiting
Jumper Wires — Circuit connections
MicroPython Firmware — Installed on Pico
💡

No hardware needed: All components above are available in the Wokwi simulator. The diagram.json in Section 4 wires everything automatically — just click ▶ Run.


03 — Wiring

Circuit Connections

ComponentPin / SignalPico GPIONotes
PIR OUTMotion signalGP15Digital IN, HIGH = motion
PIR VCCPowerVBUS (5V)HC-SR501 needs 5V
PIR GNDGroundGND.3
LCD SDAI2C DataGP0I2C0
LCD SCLI2C ClockGP1I2C0 @ 400 kHz
LCD VCCPowerVBUS (5V)Backlight needs 5V
LCD GNDGroundGND.8
Red LED AnodeAlert outputGP16 → 220ΩBlinks during alarm
Green LED AnodeArmed outputGP17 → 220ΩON when armed
LED CathodesGroundGND.1Both LEDs share
Buzzer +Digital OUTGP14Alarm pulse
Buzzer −GroundGND.3
Button pin 1ARM/DISARMGP18PULL_DOWN enabled
Button pin 2Logic HIGH3.3VReads 1 when pressed
Built-in LEDStatus blinkGP25Slow blink when armed

04 — Wokwi

diagram.json

Paste the block below into the diagram.json tab in Wokwi to load the complete circuit instantly — Pico, PIR sensor, 16×2 LCD, red & green LEDs, resistors, buzzer and push button, all pre-wired.

diagram.json — PIR Security System
{
  "version": 1,
  "author": "Motion Detection Security System",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-pi-pico",         "id": "pico",    "top": 0,    "left": 0,   "attrs": {} },
    { "type": "wokwi-pir-motion-sensor", "id": "pir1",   "top": -60,  "left": 300, "attrs": {} },
    {
      "type": "wokwi-lcd1602", "id": "lcd1",
      "top": 150, "left": 300,
      "attrs": { "pins": "i2c" }
    },
    { "type": "wokwi-buzzer", "id": "buzzer1", "top": -100, "left": 450, "attrs": {} },
    {
      "type": "wokwi-led", "id": "led1",
      "top": -100, "left": -150,
      "attrs": { "color": "red", "label": "ALERT" }
    },
    {
      "type": "wokwi-led", "id": "led2",
      "top": -50, "left": -150,
      "attrs": { "color": "green", "label": "ARMED" }
    },
    {
      "type": "wokwi-pushbutton", "id": "btn1",
      "top": 50, "left": -150,
      "attrs": { "color": "blue", "label": "ARM/DISARM" }
    },
    { "type": "wokwi-resistor", "id": "r1", "top": -90, "left": -220, "attrs": { "value": "220" } },
    { "type": "wokwi-resistor", "id": "r2", "top": -40, "left": -220, "attrs": { "value": "220" } }
  ],
  "connections": [
    ["pico:GP0",    "lcd1:SDA",    "green",  ["v0"]],
    ["pico:GP1",    "lcd1:SCL",    "blue",   ["v0"]],
    ["lcd1:VCC",   "pico:VBUS",   "red",    ["v0"]],
    ["lcd1:GND",   "pico:GND.8", "black",  ["v0"]],
    ["pir1:VCC",   "pico:VBUS",   "red",    ["v0"]],
    ["pir1:GND",   "pico:GND.3", "black",  ["v0"]],
    ["pir1:OUT",   "pico:GP15",  "orange", ["v0"]],
    ["buzzer1:1", "pico:GP14",  "purple", ["v0"]],
    ["buzzer1:2", "pico:GND.3", "black",  ["v0"]],
    ["r1:1",      "pico:GP16",  "red",    ["v0"]],
    ["r1:2",      "led1:A",     "",       ["v0"]],
    ["led1:C",    "pico:GND.1", "black",  ["v0"]],
    ["r2:1",      "pico:GP17",  "green",  ["v0"]],
    ["r2:2",      "led2:A",     "",       ["v0"]],
    ["led2:C",    "pico:GND.1", "black",  ["v0"]],
    ["btn1:1.l", "pico:GP18",  "blue",   ["v0"]],
    ["btn1:1.r", "pico:3V3",   "red",    ["v0"]]
  ],
  "dependencies": {}
}

05 — Logic

How the System Works

2-State Machine

⚪ DISARMED

No monitoring.
Green LED off.

→ Press Button →

🟢 ARMED

PIR monitored.
Green LED on.

→ PIR HIGH →

🔴 ALARM

5s alarm.
Red LED blinks.

→ 5s elapsed →

🟢 ARMED

Back to monitoring
after cooldown.

EventSystem ActionDuration
Button pressed (DISARMED)Arm system, green LED ON, single beep, reset counterInstant
Button pressed (ARMED)Disarm system, green LED OFF, red LED OFF, double beepInstant
PIR HIGH (in cooldown)Ignored — spam protection active3s block
PIR HIGH (after cooldown)Alarm: red LED ON, buzzer pulses, LCD shows ALERT, counter++5 seconds
Alarm endsRed LED OFF, buzzer OFF, LCD returns to ARMED, cooldown starts3s cooldown
Armed + idleBuilt-in LED slow blink (0.5 Hz status), LCD shows Alerts countContinuous
⚙️

Button debouncing: The code enforces a 300 ms hardware debounce window. Pressing the button faster than this is ignored, ensuring reliable ARM/DISARM toggling without false triggers.


06 — Display

LCD Display Examples

The 16×2 I2C LCD updates in real time as the system changes state. Each message is centred on its row.

System ready (disarmed)
Press Button to ARM system
Armed — monitoring
SYSTEM ARMED Monitoring...
Motion detected!
!! ALERT !! Motion #3
After alarm clears
SYSTEM ARMED Detects: 3

07 — MicroPython

Full MicroPython Code

Paste into main.py in Wokwi. Includes a complete embedded LCD I2C driver — no external library required.

main.py — Motion Detection Security System
"""
Motion Detection Security System
Raspberry Pi Pico + PIR Sensor (HC-SR501)
Wokwi Simulator Compatible
"""

from machine import Pin, I2C
import time

# ── Pin Configuration ────────────────────────────────────────
PIR_PIN      = 15   # PIR sensor output
BUZZER_PIN   = 14   # Buzzer / alarm
LED_ALERT    = 16   # Red LED  – motion detected
LED_ARMED    = 17   # Green LED – system armed
BUTTON_PIN   = 18   # Push button – arm/disarm
LED_STATUS   = 25   # Built-in LED – status blink

ALARM_DURATION = 5  # Alarm sounds for 5 seconds
COOLDOWN_TIME  = 3  # 3 s cooldown between detections

# ── Hardware Initialisation ──────────────────────────────────
pir        = Pin(PIR_PIN, Pin.IN)
buzzer     = Pin(BUZZER_PIN, Pin.OUT)
led_alert  = Pin(LED_ALERT, Pin.OUT)
led_armed  = Pin(LED_ARMED, Pin.OUT)
led_status = Pin(LED_STATUS, Pin.OUT)
button     = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_DOWN)
i2c        = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)

# ── LCD 16×2 I2C Driver ──────────────────────────────────────
class LCD_I2C:
    def __init__(self, i2c, addr=0x27, rows=2, cols=16):
        self.i2c = i2c; self.addr = addr
        self.LCD_CLEARDISPLAY  = 0x01; self.LCD_ENTRYMODESET = 0x04
        self.LCD_DISPLAYCONTROL= 0x08; self.LCD_FUNCTIONSET  = 0x20
        self.LCD_SETDDRAMADDR  = 0x80
        self.LCD_DISPLAYON     = 0x04; self.LCD_CURSOROFF    = 0x00
        self.LCD_BLINKOFF      = 0x00; self.LCD_ENTRYLEFT    = 0x02
        self.LCD_ENTRYSHIFTDEC = 0x00
        self.LCD_4BITMODE      = 0x00; self.LCD_2LINE        = 0x08
        self.LCD_5x8DOTS       = 0x00
        self.LCD_BACKLIGHT     = 0x08; self.LCD_NOBACKLIGHT  = 0x00
        self.backlight_state   = self.LCD_BACKLIGHT
        self.init_display()

    def write_byte(self, byte, mode):
        hi = mode|(byte&0xF0)|self.backlight_state
        lo = mode|((byte<<4)&0xF0)|self.backlight_state
        self.i2c.writeto(self.addr, bytearray([hi])); self.toggle_enable(hi)
        self.i2c.writeto(self.addr, bytearray([lo])); self.toggle_enable(lo)

    def toggle_enable(self, byte):
        time.sleep_us(1)
        self.i2c.writeto(self.addr, bytearray([byte|0x04]))
        time.sleep_us(1)
        self.i2c.writeto(self.addr, bytearray([byte&~0x04]))
        time.sleep_us(50)

    def init_display(self):
        time.sleep_ms(50)
        for v in (0x03,0x03,0x03,0x02): self.write_byte(v,0); time.sleep_ms(5)
        self.write_byte(self.LCD_FUNCTIONSET|self.LCD_4BITMODE|self.LCD_2LINE|self.LCD_5x8DOTS,0)
        self.write_byte(self.LCD_DISPLAYCONTROL|self.LCD_DISPLAYON|self.LCD_CURSOROFF|self.LCD_BLINKOFF,0)
        self.clear()
        self.write_byte(self.LCD_ENTRYMODESET|self.LCD_ENTRYLEFT|self.LCD_ENTRYSHIFTDEC,0)

    def clear(self): self.write_byte(0x01,0); time.sleep_ms(2)

    def set_cursor(self, row, col):
        self.write_byte(self.LCD_SETDDRAMADDR|(col+[0x00,0x40][row]),0)

    def print(self, text, row=0, col=0):
        self.set_cursor(row,col)
        for c in str(text): self.write_byte(ord(c),1)

# ── LCD Init ─────────────────────────────────────────────────
try:
    lcd = LCD_I2C(i2c); lcd_ok = True
    print("LCD initialised")
except:
    lcd_ok = False
    print("LCD not found – serial monitor only")

# ── Global State ─────────────────────────────────────────────
system_armed      = False
detection_count   = 0
last_detection    = 0

# ── Helper Functions ─────────────────────────────────────────
def startup_sequence():
    print("="*48)
    print("   MOTION DETECTION SECURITY SYSTEM")
    print("   Raspberry Pi Pico + HC-SR501 PIR")
    print("="*48)
    if lcd_ok:
        lcd.clear(); lcd.print("Security System",0,0)
        lcd.print("Initializing...",1,0)
    # Test LEDs + buzzer
    led_alert.on(); led_armed.on(); time.sleep(0.5)
    led_alert.off(); led_armed.off()
    buzzer.on(); time.sleep(0.2); buzzer.off()
    time.sleep(1)
    if lcd_ok:
        lcd.clear(); lcd.print("Press Button",0,0)
        lcd.print("to ARM system",1,0)
    print("System ready. Press button to ARM/DISARM.\n")

def toggle_system():
    global system_armed, detection_count
    system_armed = not system_armed
    if system_armed:
        led_armed.on(); detection_count = 0
        print("\n🔒 SYSTEM ARMED – Monitoring for motion...")
        if lcd_ok:
            lcd.clear(); lcd.print("SYSTEM ARMED",0,2)
            lcd.print("Monitoring...",1,1)
        buzzer.on(); time.sleep(0.1); buzzer.off()
    else:
        led_armed.off(); led_alert.off(); buzzer.off()
        print("\n🔓 SYSTEM DISARMED – Standing by...")
        if lcd_ok:
            lcd.clear(); lcd.print("DISARMED",0,3)
            lcd.print("Press to ARM",1,1)
        for _ in range(2):
            buzzer.on(); time.sleep(0.1); buzzer.off(); time.sleep(0.1)

def check_motion():
    return pir.value() == 1

def trigger_alarm():
    global detection_count, last_detection
    detection_count += 1
    print(f"\n{'!'*48}\n⚠️  MOTION DETECTED! Detection #{detection_count}\n{'!'*48}")
    if lcd_ok:
        lcd.clear(); lcd.print("!! ALERT !!",0,2)
        lcd.print(f"Motion #{detection_count}",1,2)
    led_alert.on()
    alarm_start = time.time()
    while time.time()-alarm_start < ALARM_DURATION:
        buzzer.on(); led_status.on();  time.sleep(0.2)
        buzzer.off(); led_status.off(); time.sleep(0.2)
    led_alert.off()
    last_detection = time.time()
    print(f"Alarm stopped. Total detections: {detection_count}")
    if lcd_ok:
        lcd.clear(); lcd.print("SYSTEM ARMED",0,2)
        lcd.print(f"Detects: {detection_count}",1,2)

def print_status():
    s = "ARMED" if system_armed else "DISARMED"
    m = "DETECTED" if check_motion() else "None"
    print(f"\rStatus: {s} | Motion: {m} | Detections: {detection_count}", end="")

# ── Main Loop ────────────────────────────────────────────────
def main():
    global last_detection
    startup_sequence()
    led_alert.off(); led_armed.off(); buzzer.off(); led_status.off()

    last_btn_state = 0; last_btn_time = 0; status_time = 0

    while True:
        try:
            now = time.time()

            # Button debounce + ARM/DISARM
            btn = button.value()
            if btn==1 and last_btn_state==0 and now-last_btn_time>0.3:
                toggle_system(); last_btn_time=now
            last_btn_state = btn

            # Motion detection (armed only)
            if system_armed:
                if check_motion() and now-last_detection>COOLDOWN_TIME:
                    trigger_alarm()
                    time.sleep(COOLDOWN_TIME)
                # Status LED slow blink at 0.5 Hz
                led_status.value(1 if int(now*2)%2==0 else 0)
            else:
                led_status.off()

            # Serial status every 2 s
            if now-status_time>2: print_status(); status_time=now

            time.sleep(0.1)

        except KeyboardInterrupt:
            print("\n\nSystem stopped.")
            led_alert.off(); led_armed.off(); buzzer.off(); led_status.off()
            if lcd_ok: lcd.clear(); lcd.print("System Stopped",0,0)
            break
        except Exception as e:
            print(f"\nError: {e}"); time.sleep(1)

if __name__ == "__main__":
    main()

08 — Architecture

Code Architecture

FunctionRoleTriggers
startup_sequence()Tests LEDs, buzzer, shows boot message on LCDOnce at startup
toggle_system()Flips ARMED/DISARMED state, updates LED & LCDButton press (debounced)
check_motion()Reads PIR GPIO — returns True if HIGHEvery 100 ms loop tick
trigger_alarm()5s buzzer pulse, red LED, LCD ALERT, counter++PIR HIGH + cooldown passed
print_status()Serial monitor status lineEvery 2 seconds
main()Main event loop — buttons, motion, LED blink, timingContinuous (100 ms cycle)
🔄

Loop timing: The main loop runs at 10 Hz (100 ms sleep). Button debounce = 300 ms. Alarm = 5 s (blocking). Cooldown = 3 s (enforced via time.time() delta). Status log = every 2 s.


09 — Learning

What You'll Learn

PIR sensor working principles & passive infrared detection
2-state machine system design (ARMED / DISARMED)
Button debouncing with time.time() deltas
I2C LCD 16×2 driver implementation from scratch
GPIO digital input/output in MicroPython
Cooldown timer logic to prevent alarm spam
Alarm timing and buzzer pulse patterns
Event counting and detection history
Built-in LED status blinking at 0.5 Hz
Serial monitor print formatting with \r
Exception handling in a hardware loop
Wokwi simulation and prototyping workflow

10 — Use Cases

Real-World Applications

Home Intrusion Detection — Perimeter motion alerts
Office After-Hours Monitoring — Unauthorised entry detection
Retail Entry Detection — Customer arrival chime
Warehouse Security — Zone breach detection
Elderly Movement Monitoring — Fall / inactivity detection
Smart Parking Detector — Bay occupancy sensing
Museum Exhibit Protection — No-touch zone alert
Automatic Lighting — Occupancy-based energy saving
Pet / Wildlife Monitoring — Trigger camera or log
Automatic Door Systems — Proximity activation

11 — More Projects

Raspberry Pi Pico Projects

© 2026 MakeMindz · Simulated with Wokwi

PIR Sensor · HC-SR501 · Raspberry Pi Pico · MicroPython · Security System · Wokwi

Comments

try for free