Home Security System: Camera + App Alerts on Intrusion Detection
Intermediate1/8/2026- Author: IoTSpark Maker

Home Security System: Camera + App Alerts on Intrusion Detection

An ESP32-CAM paired with a PIR sensor: it only takes a photo and fires a webhook/Telegram-style alert when it genuinely detects motion, with debounce logic to prevent false alarms.

ESP32-CAMPIRCameraSecuritySmart HomeWebhook
0 steps2 components

A complete "home security" project using an ESP32-CAM with its built-in OV2640 camera, paired with an HC-SR501 PIR motion sensor as the trigger. It only takes a photo and sends an alert when motion is genuinely detected, saving bandwidth and power compared to continuously streaming the camera.

Detailed guide

An ESP32-CAM paired with a PIR sensor takes a photo and sends a webhook/Telegram-style alert only when motion is genuinely detected — full 8-section guide, wiring diagram, and compile-verified code.

1. Introduction

This home security system uses an ESP32-CAM with its built-in OV2640 camera, paired with an HC-SR501 PIR motion sensor as the "trigger" — it only takes a photo and sends an alert when motion is genuinely detected, saving bandwidth and power compared to continuously streaming the camera.

When the PIR detects motion (after debouncing to prevent false alarms), the ESP32-CAM captures a single JPEG frame and sends an alert as an HTTP POST webhook (which you can point at the Telegram Bot API or an internal webhook, depending on your alerting setup).

2. Components Needed

Component

Qty

Notes

ESP32-CAM (AI-Thinker) (board_esp32_cam)

1

A board with a built-in OV2640 2MP camera and WiFi — programmed over FTDI/UART

HC-SR501 — PIR Motion Sensor (mod_hcsr501_pir)

1

Triggers a photo capture on motion within a ~110°, 3–7m detection zone

3. Wiring Diagram

Module pin

ESP32

PIR VCC

5V

PIR GND

GND

PIR OUT

GPIO13

OV2640 camera

Built into the board, no separate wiring needed

Built-in flash LED

GPIO4 (used as a local indicator light when an alert fires)

Note: GPIO13 on the ESP32-CAM overlaps with the SD_D3 pin if you're using an SD card — since this project doesn't use the SD slot, it's safe to reuse for the PIR. The OV2640 camera is soldered directly onto the AI-Thinker board; its pins (XCLK/SIOD/SIOC/Y2-Y9/VSYNC/HREF/PCLK) are handled internally by the esp_camera library and don't appear as separate nodes in the diagram.

4. Example #1 — Reading the PIR Sensor & Debouncing False Alarms

This snippet shows the debounce logic: alerts are allowed no more often than every 15 seconds, preventing spam during continuous motion:

const unsigned long MOTION_DEBOUNCE_MS = 15000; // chong bao gia lien tuc

void loop() {
  int pirState = digitalRead(PIN_PIR_OUT);
  bool motion = (pirState == HIGH);
  unsigned long now = millis();

  if (motion && (now - lastAlertMs >= MOTION_DEBOUNCE_MS)) {
    lastAlertMs = now;
    Serial.println("Phat hien chuyen dong -> gui canh bao.");
    // sendMotionAlert(); see code Example #2
  }
}

5. Example #2 — Capturing a Photo & Sending a Webhook Alert

This function captures a JPEG frame from the camera and sends a JSON payload to the alert webhook (swap in the Telegram Bot API's sendPhoto endpoint or your own internal webhook as needed):

void sendMotionAlert() {
  camera_fb_t *fb = cameraReady ? esp_camera_fb_get() : nullptr;
  size_t photoSize = fb ? fb->len : 0;

  HTTPClient http;
  http.begin(ALERT_WEBHOOK_URL);
  http.addHeader("Content-Type", "application/json");
  char payload[160];
  snprintf(payload, sizeof(payload),
           "{\"event\":\"motion_detected\",\"photoBytes\":%u,\"device\":\"esp32-cam-security\"}",
           (unsigned)photoSize);
  int httpCode = http.POST(payload);
  Serial.printf("HTTP status=%d, photoBytes=%u\n", httpCode, (unsigned)photoSize);
  http.end();

  if (fb) esp_camera_fb_return(fb);
}

6. Example #3 — The Complete Application: PIR Trigger + Camera + Webhook Alert

The full firmware: boots up, initializes the camera with clear error handling (it won't crash if the camera fails — it degrades to logging "camera=null"), reconnects WiFi with a timeout, and logs periodic telemetry:

/*
  He thong an ninh nha cua: camera + canh bao app khi phat hien xam nhap
  Board: ESP32-CAM AI-Thinker (board_esp32_cam)
  - PIR HC-SR501 (mod_hcsr501_pir): OUT -> GPIO13, VCC -> 5V, GND -> GND
  - Camera OV2640 tich hop san tren board ESP32-CAM (khong can catalog node rieng)
  - GPIO4 = flash LED tich hop tren board, dung lam den bao cuc bo khi co canh bao

  Luong: PIR phat hien chuyen dong -> debounce chong bao gia -> chup anh (framebuffer)
  -> gui canh bao (webhook/Telegram-style HTTP POST) kem kich thuoc anh.
  Day la ban rut gon: gui HTTP POST toi webhook URL cau hinh san (co the tro vao
  Telegram Bot API sendPhoto endpoint hoac webhook noi bo tuy nhu cau).
*/

#include "esp_camera.h"
#include <WiFi.h>
#include <HTTPClient.h>

// ==== Cau hinh chan camera AI-Thinker ESP32-CAM (chuan, co dinh theo board) ====
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22

// ==== Cau hinh nguoi dung ====
const char *WIFI_SSID = "YOUR_WIFI_SSID";
const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char *ALERT_WEBHOOK_URL = "https://example.com/api/alert-webhook"; // doi thanh Telegram/webhook that

// ==== Chan ket noi phan cung phu tro ====
const int PIN_PIR_OUT = 13;   // OUT cua HC-SR501 (SD_D3 khi khong dung the SD)
const int PIN_FLASH_LED = 4;  // flash LED tich hop tren board ESP32-CAM

const unsigned long MOTION_DEBOUNCE_MS = 15000;  // chong bao gia lien tuc: toi thieu 15s giua 2 lan canh bao
const unsigned long WIFI_RETRY_INTERVAL_MS = 5000;
const unsigned long WIFI_CONNECT_TIMEOUT_MS = 10000;
const unsigned long TELEMETRY_INTERVAL_MS = 5000;

unsigned long lastWifiAttemptMs = 0;
unsigned long lastAlertMs = 0;
unsigned long lastTelemetryMs = 0;
bool cameraReady = false;

void connectWiFiNonBlocking() {
  if (WiFi.status() == WL_CONNECTED) return;
  unsigned long now = millis();
  if (now - lastWifiAttemptMs < WIFI_RETRY_INTERVAL_MS) return;
  lastWifiAttemptMs = now;

  Serial.printf("[WiFi] Dang ket noi toi SSID \"%s\" ...\n", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < WIFI_CONNECT_TIMEOUT_MS) {
    delay(200);
    Serial.print(".");
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("[WiFi] Da ket noi. IP: %s\n", WiFi.localIP().toString().c_str());
  } else {
    Serial.println("[WiFi] Ket noi that bai, se thu lai sau.");
  }
}

bool initCamera() {
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_VGA;
  config.jpeg_quality = 12;
  config.fb_count = 1;

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("[Camera] Khoi tao that bai, ma loi 0x%x\n", err);
    return false;
  }
  return true;
}

void sendMotionAlert() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[Alert] Bo qua gui canh bao - chua co WiFi.");
    return;
  }

  camera_fb_t *fb = nullptr;
  size_t photoSize = 0;
  if (cameraReady) {
    fb = esp_camera_fb_get();
    if (fb) {
      photoSize = fb->len;
    } else {
      Serial.println("[Camera] Chup anh that bai (fb null).");
    }
  }

  HTTPClient http;
  http.begin(ALERT_WEBHOOK_URL);
  http.addHeader("Content-Type", "application/json");
  char payload[160];
  snprintf(payload, sizeof(payload),
           "{\"event\":\"motion_detected\",\"photoBytes\":%u,\"device\":\"esp32-cam-security\"}",
           (unsigned)photoSize);
  int httpCode = http.POST(payload);
  Serial.printf("[Alert] Da gui webhook, HTTP status=%d, photoBytes=%u\n", httpCode, (unsigned)photoSize);
  http.end();

  if (fb) {
    esp_camera_fb_return(fb);
  }

  digitalWrite(PIN_FLASH_LED, HIGH);
  delay(150);
  digitalWrite(PIN_FLASH_LED, LOW);
}

void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println();
  Serial.println("========================================");
  Serial.println(" IoTLabs Maker - He Thong An Ninh Camera");
  Serial.println(" Board: ESP32-CAM (AI-Thinker)");
  Serial.println("========================================");

  pinMode(PIN_PIR_OUT, INPUT);
  pinMode(PIN_FLASH_LED, OUTPUT);
  digitalWrite(PIN_FLASH_LED, LOW);

  cameraReady = initCamera();
  Serial.printf("[Camera] Trang thai: %s\n", cameraReady ? "san sang" : "loi/khong co");

  connectWiFiNonBlocking();
}

void loop() {
  connectWiFiNonBlocking();

  int pirState = digitalRead(PIN_PIR_OUT);
  bool motion = (pirState == HIGH);
  unsigned long now = millis();

  if (motion && (now - lastAlertMs >= MOTION_DEBOUNCE_MS)) {
    lastAlertMs = now;
    Serial.println("[PIR] Phat hien chuyen dong -> gui canh bao.");
    sendMotionAlert();
  }

  if (now - lastTelemetryMs >= TELEMETRY_INTERVAL_MS) {
    lastTelemetryMs = now;
    Serial.printf("[Telemetry] motion=%s camera=%s wifi=%s\n",
                  motion ? "true" : "false",
                  cameraReady ? "ready" : "null",
                  WiFi.status() == WL_CONNECTED ? "connected" : "disconnected");
  }

  delay(200);
}

7. Common Issues

Issue

Cause

Fix

Serial Monitor prints "[Camera] Khoi tao that bai, ma loi 0x..." (camera init failed)

Wrong camera pin configuration, weak power supply (a low-quality USB cable), or a board that isn't a genuine AI-Thinker

Use the exact AI-Thinker pin configuration in the code, and power it from a stable 5V/2A external adapter rather than a computer's USB port while testing the camera

PIR keeps triggering alerts even when no one is around

The PIR's sensitivity trimmer is set too high, or it's placed near a heat/airflow source (AC unit, window)

Lower the sensitivity trimmer, avoid pointing the PIR directly at a heat/airflow source, and increase MOTION_DEBOUNCE_MS if it still triggers too often

Webhook is sent but photoBytes is always 0

cameraReady is false because camera initialization failed at boot

Check the [Camera] log at boot, and make sure the board has adequate power and is a genuine ESP32-CAM AI-Thinker

Can't flash firmware over UART

The ESP32-CAM needs GPIO0 tied to GND to enter flashing mode, then disconnected for normal operation

Connect GPIO0 to GND before pressing reset to enter flash mode, then remove the wire and reset again after uploading

Safety Notes

A camera + PIR intrusion alert needs solid false-alarm prevention to avoid "alert fatigue" — where users start ignoring real alerts because of too many false ones. Always keep a minimum debounce and consider extra conditions (e.g. only alerting during hours when no one's home) before treating the system as ready for real deployment. Keep the PIR pointed away from windows and heat sources to reduce environmental false triggers.

8. Summary

Combining a PIR sensor (cheap, fast-reacting, low-power) as the trigger with an ESP32-CAM that only shoots when needed makes a solid foundation for a DIY home security system.

Possible extensions: save photos to an SD card with timestamps, integrate a real Telegram Bot to receive photos directly on your phone, or add face recognition to tell familiar faces from strangers.