ESP32-CAM + PIR: Send Photo Alerts via Telegram on Motion
Advanced1/8/2026- Author: IoTSpark Maker

ESP32-CAM + PIR: Send Photo Alerts via Telegram on Motion

An HC-SR501 PIR sensor triggers the ESP32-CAM to take a photo and send it instantly through the Telegram Bot API (binary sendPhoto, no intermediate upload) whenever motion is detected, with a cooldown to prevent spam.

ESP32-CAMPIRHC-SR501Telegrammotion-alertesp32
0 steps3 components

Combines an HC-SR501 PIR sensor with an ESP32-CAM to build a security alert system that sends photos straight to Telegram.

When the PIR detects motion (a rising edge on OUT), the device captures a JPEG frame and sends it via UniversalTelegramBot's sendPhotoByBinary, reading directly from the camera's frame buffer byte-by-byte with no intermediate memory allocation.

A 30-second cooldown between alerts prevents spam while the PIR output stays HIGH.

Detailed guide

An HC-SR501 PIR sensor triggers the ESP32-CAM to take a photo and send it instantly via Telegram — full 8-section guide, wiring diagram, and compile-verified code.

1. Introduction

Pairs an HC-SR501 PIR sensor with an ESP32-CAM to build a security alert system that sends photos straight to Telegram — no intermediate server, no cloud storage required.

When the PIR detects motion (a rising edge on the OUT pin), the device captures a JPEG frame and sends it directly through the Telegram Bot API using sendPhotoByBinary, reading bytes straight from the camera's frame buffer.

2. Components Needed

Component

Qty

Notes

ESP32-CAM (AI-Thinker), OV2640

1

Built-in camera + WiFi, 4MB PSRAM

FTDI USB-to-Serial programmer

1

Required for flashing firmware over UART

HC-SR501 PIR Motion Sensor

1

Detects motion within 3–7m, outputs digital HIGH when someone's present

Male-to-female jumper wires

7

For wiring the FTDI + PIR to the ESP32-CAM

A Telegram bot (created via @BotFather) + chat_id

1

Get the TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID before flashing the firmware

3. Wiring Diagram

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 was chosen because this project doesn't use an SD card (SD_MMC also uses GPIO12–15), so those pins are free to reuse as regular GPIO. If you want to use an SD card and the PIR at the same time, pick a different pin for the PIR or drop the SD card.

4. Environment Setup

Arduino IDE, board set to AI Thinker ESP32-CAM.

Install two extra libraries via the Library Manager: UniversalTelegramBot (by Brian Lough, tested with version 1.3.0) and ArduinoJson (a dependency, version 7.x).

Create a Telegram bot via @BotFather, and get the token and chat_id (message the bot, then call https://api.telegram.org/bot<TOKEN>/getUpdates to retrieve the chat_id).

5. Sample Code — PIR Trigger + Sending a Photo to Telegram

Compiles to about 1,106,177 bytes of 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. Code Walkthrough

  • telegramGetNextByte() / telegramMoreDataAvailable(): UniversalTelegramBot::sendPhotoByBinary requires plain C function pointers (no capturing lambdas), so this uses global static variables pointing at the frame buffer and the current read offset — reading directly byte-by-byte with no intermediate memory allocation.

  • Rising-edge debounce: only sends a photo when pirState == HIGH && lastPirState == LOW (a rising edge), rather than repeatedly while the PIR stays HIGH.

  • ALERT_COOLDOWN_MS (30s): blocks repeated sends during a burst of consecutive motion, preventing Telegram spam and wasted bandwidth.

  • secureClient.setInsecure(): skips TLS certificate verification to keep the demo simple — a production setup should pin the correct certificate for api.telegram.org.

  • SVGA resolution when PSRAM is available: balances alert-photo quality against Telegram upload speed (a UXGA photo would take noticeably longer to send).

7. Common Issues

Issue

Cause

Fix

Compile error: "no matching function for sendPhotoByBinary"

Passed a capturing lambda or the wrong argument count — the library requires exactly 7 plain C function-pointer arguments

Use global static callbacks like in the sample code (don't capture local variables)

PIR keeps triggering even when no one's around

Placed near a heat source (AC unit, a sun-lit window), or the sensitivity trimmer is set too high

Adjust the sensitivity trimmer on the HC-SR501 module, and avoid placing it near heat/airflow sources

No photo arrives on Telegram

Wrong TELEGRAM_BOT_TOKEN/CHAT_ID, or you've never messaged the bot (Telegram blocks bots from messaging first)

Send /start to the bot first, and get the correct chat_id via getUpdates

Sending photos is very slow or times out

Weak WiFi, or the image resolution is too high

Drop to QVGA/VGA, and place the ESP32-CAM closer to the router

8. Summary

This project demonstrates a complete security-alert pipeline: a physical sensor (PIR) → on-device photo capture → a direct send through a third-party API (Telegram), with no backend of your own needed.

The byte-by-byte callback technique reading straight from the camera's frame buffer is a useful pattern for any library that requires plain C function pointers on the Arduino/ESP32 platform.