Hệ thống an ninh nhà cửa: camera + cảnh báo app khi phát hiện xâm nhập
Trung cấp1/8/2026- Tác giả: IoTlabs Maker

Hệ thống an ninh nhà cửa: camera + cảnh báo app khi phát hiện xâm nhập

ESP32-CAM kết hợp cảm biến PIR: chỉ chụp ảnh và gửi cảnh báo webhook/Telegram-style khi thực sự phát hiện chuyển động, có debounce chống báo giả.

ESP32-CAMPIRCameraAn ninhSmart HomeWebhook

Dự án hoàn chỉnh "Hệ thống an ninh" dùng ESP32-CAM tích hợp sẵn camera OV2640, kết hợp cảm biến chuyển động PIR HC-SR501 làm "trigger", chỉ chụp ảnh và gửi cảnh báo khi thực sự có chuyển động, tiết kiệm băng thông/điện năng so với việc stream camera liên tục.

Hướng dẫn chi tiết

ESP32-CAM + PIR chụp ảnh và gửi cảnh báo webhook khi phát hiện chuyển động, có debounce chống báo giả — hướng dẫn đầy đủ 8 mục, sơ đồ đấu nối và code compile pass.

1. Giới thiệu

Hệ thống an ninh dùng ESP32-CAM tích hợp sẵn camera OV2640, kết hợp cảm biến chuyển động PIR HC-SR501 làm "trigger" — chỉ chụp ảnh và gửi cảnh báo khi thực sự có chuyển động, tiết kiệm băng thông/điện năng so với việc stream camera liên tục.

Khi PIR phát hiện chuyển động (đã qua debounce chống báo giả), ESP32-CAM chụp 1 khung hình JPEG và gửi cảnh báo dạng webhook HTTP POST (có thể trỏ vào Telegram Bot API hoặc webhook nội bộ tuỳ hạ tầng cảnh báo của bạn).

2. Linh kiện cần có

Linh kiện

Số lượng

Ghi chú

ESP32-CAM (AI-Thinker) (board_esp32_cam)

1

Board tích hợp camera OV2640 2MP, WiFi — lập trình qua FTDI/UART

HC-SR501 – PIR Phát hiện chuyển động (mod_hcsr501_pir)

1

Kích hoạt trigger chụp ảnh khi có chuyển động trong vùng ~110°, 3–7m

3. Sơ đồ kết nối

Chân module

ESP32

PIR VCC

5V

PIR GND

GND

PIR OUT

GPIO13

Camera OV2640

Tích hợp sẵn trên board, không cần đấu dây rời

Flash LED tích hợp

GPIO4 (dùng làm đèn báo cục bộ khi có cảnh báo)

Ghi chú: GPIO13 trên ESP32-CAM trùng với chân SD_D3 nếu dùng thẻ SD — dự án này không dùng khe SD nên tái sử dụng an toàn cho PIR. Camera OV2640 hàn sẵn trên board AI-Thinker, các chân camera (XCLK/SIOD/SIOC/Y2-Y9/VSYNC/HREF/PCLK) do thư viện esp_camera quản lý nội bộ, không xuất hiện là node rời trên sơ đồ.

4. Code ví dụ #1 — Đọc cảm biến PIR & debounce chống báo giả

Đoạn trích minh hoạ logic debounce: chỉ cho phép gửi cảnh báo tối thiểu cách nhau 15 giây, tránh spam khi có chuyển động liên tục:

const unsigned long MOTION_DEBOUNCE_MS = 15000; // chống báo giả liên tục

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(); xem code ví dụ #2
  }
}

5. Code ví dụ #2 — Chụp ảnh & gửi cảnh báo webhook

Đoạn trích hàm chụp khung hình JPEG từ camera và gửi payload JSON tới webhook cảnh báo (đổi URL sang Telegram Bot API sendPhoto hoặc webhook nội bộ tuỳ nhu cầu):

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. Code ví dụ #3 — Ứng dụng thực tế đầy đủ — PIR trigger + Camera + Webhook alert

Code đầy đủ khởi động, khởi tạo camera với xử lý lỗi rõ ràng (không crash nếu camera lỗi — degrade "camera=null" trong log), WiFi reconnect có timeout, telemetry định kỳ:

/*
  Hệ thống an ninh nhà cửa: camera + cảnh báo app khi phát hiện xâm nhập
  Board: ESP32-CAM AI-Thinker (board_esp32_cam)
  - PIR HC-SR501 (mod_hcsr501_pir): OUT -> GPIO13, VCC -> 5V, GND -> GND
  - Camera OV2640 tích hợp sẵn trên board ESP32-CAM (không cần catalog node riêng)
  - GPIO4 = flash LED tích hợp trên board, dùng làm đèn báo cục bộ khi có cảnh báo

  Luồng: PIR phát hiện chuyển động -> debounce chống báo giả -> chụp ảnh (framebuffer)
  -> gửi cảnh báo (webhook/Telegram-style HTTP POST) kèm kích thước ảnh.
  Đây là bản rút gọn: gửi HTTP POST tới webhook URL cấu hình sẵn (có thể trỏ vào
  Telegram Bot API sendPhoto endpoint hoặc webhook nội bộ tuỳ nhu cầu).
*/

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

// ==== Cấu hình chân camera AI-Thinker ESP32-CAM (chuẩn, cố định 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

// ==== Cấu hình người dùng ====
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"; // đổi thành Telegram/webhook thật

// ==== Chân kết nối phần cứng phụ trợ ====
const int PIN_PIR_OUT = 13;   // OUT của HC-SR501 (SD_D3 khi không dùng thẻ SD)
const int PIN_FLASH_LED = 4;  // flash LED tích hợp trên board ESP32-CAM

const unsigned long MOTION_DEBOUNCE_MS = 15000;  // chống báo giả liên tục: tối thiểu 15s giữa 2 lần cảnh báo
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. Lỗi thường gặp

Lỗi

Nguyên nhân

Cách sửa

Serial in "[Camera] Khoi tao that bai, ma loi 0x..."

Sai chân cấu hình camera, nguồn cấp yếu (dùng cáp USB kém chất lượng), hoặc board không phải AI-Thinker chuẩn

Dùng đúng bộ chân cấu hình mẫu AI-Thinker trong code, cấp nguồn 5V/2A ổn định qua adapter ngoài thay vì cổng USB máy tính khi test camera

PIR báo động liên tục dù không ai qua lại

Biến trở độ nhạy trên module PIR đặt quá cao, hoặc đặt gần nguồn nhiệt/gió (điều hòa, cửa sổ)

Giảm biến trở độ nhạy, tránh đặt PIR hướng thẳng nguồn nhiệt/gió, tăng MOTION_DEBOUNCE_MS nếu vẫn báo dày

Gửi webhook nhưng photoBytes luôn = 0

cameraReady = false do camera init lỗi từ lúc boot

Xem lại log [Camera] lúc khởi động, đảm bảo đã cấp đủ nguồn và đúng board ESP32-CAM AI-Thinker

Không nạp được firmware qua UART

ESP32-CAM cần chân GPIO0 nối GND khi nạp code (chế độ boot flash), rồi tháo ra khi chạy bình thường

Nối GPIO0-GND trước khi nhấn reset để vào chế độ nạp, tháo dây sau khi upload xong rồi reset lại

Lưu ý an toàn

Camera + PIR cảnh báo xâm nhập cần chống báo giả kỹ để tránh "báo động mỏi" (alert fatigue) khiến người dùng bỏ qua cảnh báo thật — luôn giữ debounce tối thiểu và cân nhắc thêm điều kiện phụ (ví dụ chỉ cảnh báo trong khung giờ vắng nhà) trước khi coi hệ thống sẵn sàng lắp thật. Đặt PIR tránh hướng trực tiếp cửa sổ/nguồn nhiệt để giảm báo giả do môi trường.

8. Tổng kết

Hệ thống kết hợp PIR (rẻ, phản ứng nhanh, ít tốn điện) làm trigger và camera ESP32-CAM chỉ chụp khi cần, phù hợp làm nền tảng an ninh nhà cửa DIY.

Có thể mở rộng: lưu ảnh vào thẻ SD kèm timestamp, tích hợp Telegram Bot thật để nhận ảnh trực tiếp trên điện thoại, hoặc thêm nhận diện khuôn mặt để phân biệt người quen/lạ.