
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.
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::sendPhotoByBinaryyê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.