ESP32-CAM + PIR: Gửi ảnh cảnh báo qua Telegram khi có chuyển động
Nâng cao1/8/2026- Tác giả: IoTlabs Maker

ESP32-CAM + PIR: Gửi ảnh cảnh báo qua Telegram khi có chuyển động

Cảm biến PIR HC-SR501 kích hoạt ESP32-CAM chụp ảnh và gửi ngay qua Telegram Bot API (sendPhoto dạng nhị phân, không cần upload trung gian) khi phát hiện chuyển động, có cooldown chống spam.

ESP32-CAMPIRHC-SR501Telegrammotion-alertesp32

Kết hợp cảm biến PIR HC-SR501 với ESP32-CAM để xây dựng hệ thống cảnh báo an ninh gửi ảnh trực tiếp qua Telegram.

Khi PIR phát hiện chuyển động (cạnh lên OUT), thiết bị chụp một khung JPEG và gửi qua UniversalTelegramBot bằng sendPhotoByBinary (đọc trực tiếp từ camera frame buffer theo từng byte, không cần cấp phát bộ nhớ trung gian).

Có cơ chế cooldown 30s giữa các lần cảnh báo để tránh spam khi PIR còn ở trạng thái HIGH.

Hướng dẫn chi tiết

Hướng dẫn ESP32-CAM kết hợp PIR HC-SR501 gửi ảnh cảnh báo qua Telegram Bot API khi phát hiện chuyển động.

1. Giới thiệu

Kết hợp cảm biến PIR HC-SR501 với ESP32-CAM để xây dựng hệ thống cảnh báo an ninh gửi ảnh trực tiếp qua Telegram — không cần server trung gian, không cần lưu ảnh lên cloud storage.

Khi PIR phát hiện chuyển động (cạnh lên của chân OUT), thiết bị chụp một khung JPEG và gửi thẳng qua Telegram Bot API bằng sendPhotoByBinary, đọc trực tiếp byte từ camera frame buffer.

2. Linh kiện cần có

Linh kiện

Số lượng

Ghi chú

ESP32-CAM (AI-Thinker), OV2640

1

Camera + WiFi tích hợp, 4MB PSRAM

Mạch nạp FTDI USB-to-Serial

1

Bắt buộc để nạp firmware qua UART

HC-SR501 PIR Motion Sensor

1

Phát hiện chuyển động 3–7m, ngõ ra digital HIGH khi có người

Dây cắm breadboard (male-female)

7

Nối FTDI + PIR ↔ ESP32-CAM

Bot Telegram (tạo qua @BotFather) + chat_id

1

Lấy TELEGRAM_BOT_TOKEN và TELEGRAM_CHAT_ID trước khi nạp firmware

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

FTDI

ESP32-CAM

TX

GPIO3 (RX)

RX

GPIO1 (TX)

GND

GND

5V

5V

HC-SR501 PIR

ESP32-CAM

VCC

5V

GND

GND

OUT

GPIO13

GPIO13 được chọn vì project này không dùng thẻ SD (SD_MMC cũng dùng GPIO12–15), nên các chân này rảnh để tái sử dụng làm GPIO thường. Nếu muốn dùng đồng thời SD card + PIR, cần chọn chân khác cho PIR hoặc bỏ SD.

4. Cài đặt môi trường

Arduino IDE, board AI Thinker ESP32-CAM.

Cài thêm 2 thư viện qua Library Manager: UniversalTelegramBot (bởi Brian Lough, đã kiểm thử bản 1.3.0) và ArduinoJson (dependency, bản 7.x).

Tạo bot Telegram qua @BotFather, lấy token và chat_id (gửi tin nhắn cho bot rồi gọi https://api.telegram.org/bot<TOKEN>/getUpdates để lấy chat_id).

5. Code ví dụ — PIR trigger + gửi ảnh Telegram

Đã biên dịch khoảng 1106177 bytes flash, 35%.

/*
  ESP32-CAM + PIR: Gui anh canh bao qua Telegram khi co chuyen dong
  Board: ESP32-CAM (AI-Thinker) + HC-SR501 PIR

  PIR.OUT -> GPIO13 (khong dung SD card trong project nay nen GPIO13/14/15
  ranh de tai su dung). Khi PIR bao HIGH (co chuyen dong), chup 1 anh JPEG
  va gui qua Telegram Bot API (sendPhoto) toi CHAT_ID cau hinh san.
  Co debounce/cooldown de tranh spam lien tuc khi PIR con "dinh" HIGH.
*/

#include "esp_camera.h"
#include "Arduino.h"
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>

#ifndef WIFI_SSID
#define WIFI_SSID "YOUR_WIFI_SSID"
#endif
#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "YOUR_WIFI_PASSWORD"
#endif
#ifndef TELEGRAM_BOT_TOKEN
#define TELEGRAM_BOT_TOKEN "YOUR_BOT_TOKEN"
#endif
#ifndef TELEGRAM_CHAT_ID
#define TELEGRAM_CHAT_ID "YOUR_CHAT_ID"
#endif

// ---------- Chan camera co dinh cua AI-Thinker ESP32-CAM ----------
#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

#define PIR_PIN               13
#define ALERT_COOLDOWN_MS     (30UL * 1000UL) // toi thieu 30s giua 2 lan canh bao
#define WIFI_CONNECT_TIMEOUT_MS 15000
#define WIFI_RETRY_INTERVAL_MS  30000

WiFiClientSecure secureClient;
UniversalTelegramBot bot(TELEGRAM_BOT_TOKEN, secureClient);

bool cameraReady = false;
bool wifiReady = false;
unsigned long lastAlertMs = 0;
unsigned long lastWifiAttemptMs = 0;
int lastPirState = LOW;

static 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;

  if (psramFound()) {
    config.frame_size = FRAMESIZE_SVGA; // 800x600 - can bang toc do gui Telegram / chat luong
    config.jpeg_quality = 12;
    config.fb_count = 2;
    config.fb_location = CAMERA_FB_IN_PSRAM;
    Serial.println("[CAM] PSRAM OK -> SVGA JPEG, fb_count=2");
  } else {
    config.frame_size = FRAMESIZE_QVGA; // 320x240 khi khong co PSRAM
    config.jpeg_quality = 15;
    config.fb_count = 1;
    config.fb_location = CAMERA_FB_IN_DRAM;
    Serial.println("[CAM] Khong thay PSRAM -> giam xuong QVGA JPEG, fb_count=1");
  }

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

static bool connectWifi() {
  Serial.printf("[WIFI] Dang ket noi 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(300);
    Serial.print(".");
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("[WIFI] Da ket noi, IP: %s\n", WiFi.localIP().toString().c_str());
    secureClient.setInsecure(); // demo: bo qua kiem tra cert (production nen pin cert Telegram)
    return true;
  }
  Serial.println("[WIFI] Khong ket noi duoc trong thoi gian cho, se thu lai dinh ky.");
  return false;
}

// UniversalTelegramBot::sendPhotoByBinary yeu cau con tro ham C thuan (khong
// the dung lambda co capture). Dung bien static toan cuc de callback byte-by-byte
// doc truc tiep tu camera frame buffer, khong can cap phat/copy them bo nho.
static camera_fb_t *s_telegramFb = nullptr;
static size_t s_telegramOffset = 0;

static bool telegramMoreDataAvailable() {
  return s_telegramFb != nullptr && s_telegramOffset < s_telegramFb->len;
}

static byte telegramGetNextByte() {
  byte b = s_telegramFb->buf[s_telegramOffset];
  s_telegramOffset++;
  return b;
}

static bool sendTelegramPhoto() {
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("[CAM] Lay khung hinh that bai, khong gui duoc canh bao.");
    return false;
  }

  Serial.printf("[TELEGRAM] Dang gui anh (%u bytes) toi chat_id=%s...\n", fb->len, TELEGRAM_CHAT_ID);

  s_telegramFb = fb;
  s_telegramOffset = 0;

  String response = bot.sendPhotoByBinary(
      TELEGRAM_CHAT_ID, "image/jpeg", fb->len,
      telegramMoreDataAvailable,
      telegramGetNextByte,
      nullptr, nullptr);

  s_telegramFb = nullptr;
  esp_camera_fb_return(fb);

  bool ok = response.indexOf("\"ok\":true") >= 0;
  if (ok) {
    Serial.println("[TELEGRAM] Gui anh thanh cong.");
  } else {
    Serial.printf("[TELEGRAM] Gui anh that bai (kiem tra token/chat_id/mang). Response: %s\n", response.c_str());
  }
  return ok;
}

void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println();
  Serial.println("=== ESP32-CAM + PIR Telegram Alert boot ===");

  pinMode(PIR_PIN, INPUT);

  cameraReady = initCamera();
  if (!cameraReady) {
    Serial.println("[CAM] Camera khong san sang, se thu lai trong loop().");
  }

  wifiReady = connectWifi();
  lastWifiAttemptMs = millis();
}

void loop() {
  unsigned long now = millis();

  if (!wifiReady && (now - lastWifiAttemptMs > WIFI_RETRY_INTERVAL_MS)) {
    wifiReady = connectWifi();
    lastWifiAttemptMs = now;
  }

  if (!cameraReady) {
    static unsigned long lastCamRetry = 0;
    if (now - lastCamRetry > 10000) {
      Serial.println("[CAM] Retry khoi tao camera...");
      cameraReady = initCamera();
      lastCamRetry = now;
    }
  }

  int pirState = digitalRead(PIR_PIN);
  if (pirState == HIGH && lastPirState == LOW) {
    Serial.println("[PIR] Phat hien chuyen dong (canh len).");
    if (cameraReady && wifiReady && (now - lastAlertMs >= ALERT_COOLDOWN_MS)) {
      lastAlertMs = now;
      sendTelegramPhoto();
    } else if (now - lastAlertMs < ALERT_COOLDOWN_MS) {
      Serial.println("[PIR] Bo qua vi con trong thoi gian cooldown.");
    } else {
      Serial.println("[PIR] Bo qua vi camera/wifi chua san sang.");
    }
  }
  lastPirState = pirState;

  delay(100);
}

6. Giải thích code

  • telegramGetNextByte() / telegramMoreDataAvailable(): UniversalTelegramBot::sendPhotoByBinary yêu cầu con trỏ hàm C thuần (không lambda có capture), nên dùng biến static toàn cục trỏ tới frame buffer và offset đọc hiện tại — đọc trực tiếp byte-by-byte, không cấp phát bộ nhớ trung gian.

  • Debounce cạnh lên: chỉ gửi ảnh khi pirState == HIGH && lastPirState == LOW (cạnh lên), không gửi liên tục khi PIR giữ HIGH.

  • ALERT_COOLDOWN_MS (30s): chặn gửi lặp khi có nhiều chuyển động liên tiếp trong thời gian ngắn, tránh spam Telegram/tốn băng thông.

  • secureClient.setInsecure(): bỏ qua kiểm tra chứng chỉ TLS để đơn giản hoá demo — môi trường production nên pin đúng certificate của api.telegram.org.

  • Độ phân giải SVGA khi có PSRAM: cân bằng giữa chất lượng ảnh cảnh báo và tốc độ upload qua Telegram (ảnh UXGA sẽ mất nhiều giây hơn để gửi).

7. Lỗi thường gặp

Lỗi

Nguyên nhân

Cách sửa

Biên dịch lỗi "no matching function for sendPhotoByBinary"

Truyền lambda có capture hoặc thiếu tham số — thư viện yêu cầu đúng 7 tham số hàm con trỏ C thuần

Dùng callback static toàn cục như trong code mẫu (không capture biến local)

PIR báo liên tục dù không có người

Đặt gần nguồn nhiệt (điều hoà, cửa sổ nắng chiếu), độ nhạy biến trở quá cao

Chỉnh biến trở độ nhạy trên module HC-SR501, tránh đặt gần nguồn nhiệt/gió

Không nhận được ảnh trên Telegram

Sai TELEGRAM_BOT_TOKEN/CHAT_ID, hoặc chưa từng nhắn tin cho bot (Telegram chặn bot chủ động nhắn trước)

Nhắn /start cho bot trước, lấy đúng chat_id qua getUpdates

Gửi ảnh rất chậm hoặc timeout

WiFi yếu hoặc độ phân giải ảnh quá lớn

Giảm xuống QVGA/VGA, đặt ESP32-CAM gần router hơn

8. Tổng kết

Dự án minh họa một pipeline cảnh báo an ninh hoàn chỉnh: cảm biến vật lý (PIR) → capture ảnh on-device → gửi trực tiếp qua API bên thứ ba (Telegram), không cần backend riêng.

Kỹ thuật callback byte-by-byte đọc thẳng từ camera frame buffer là mẫu hình hữu ích cho các thư viện yêu cầu con trỏ hàm C thuần trên nền tảng Arduino/ESP32.