ESP32-CAM: Video Surveillance with a PIR Sensor on Telegram

Quick Overview & Specifications

AttributeDetails
PublicationVinjo Engineering & Technology
CategoryHardware & IoT
AuthorVinay
Article TypeTechnical Tutorial & Case Study
Published2026-10-24
Last Updated2026-10-24
License / AccessOpen Educational Resource
Core Engineering Highlights:
  1. Verified hardware architecture, schematics, and pinout diagrams.
  2. Comprehensive firmware routines, protocol benchmarks, and build steps.
  3. Production design rules, tolerances, and thermal considerations.
Related Topic Taxonomy:
  • #ESP32-CAM
  • #IoT
  • #Arduino
  • #Video Surveillance
  • #Telegram Bot
  • #PIR Sensor

ESP32-CAM board with camera module and PIR motion sensor wired for a Telegram video surveillance project

Security and video surveillance systems have shifted radically from bulky, expensive closed-circuit television (CCTV) rigs to compact, network-connected microcontrollers. Modern embedded solutions demand minimal power consumption, diminutive physical footprints, and direct integration with existing mobile channels. By combining low-cost edge processing with instant messaging networks, developers can assemble custom intruder alert systems without recurring cloud subscription fees.

When architecting a DIY surveillance solution, system designers must evaluate three primary criteria:

  • Compute and Memory Overhead: Capability to buffer and stream image frames without exhausting memory.
  • Trigger Fidelity: Reliable physical sensing (via passive infrared or switch actuation) to avoid false-positive alert floods.
  • Transport Reliability: Low-latency API transmission over standard protocols (HTTP/HTTPS or MQTT) compatible with mobile clients.

Detailed Hardware Breakdown & Configuration

AI-Thinker ESP32-CAM

The core processor and image capture engine is the AI-Thinker ESP32-CAM development board. Measuring 40.5 × 27 × 4.5 mm, this board integrates WiFi, Bluetooth, and an external RAM module into a footprint suitable for tight enclosures such as doorframes, garages, and single-room access points.

Pin-out diagram of the ESP32-Cam module

Key Specs: ESP32-CAM

  • Microcontroller: ESP32 dual-core 32-bit Xtensa processor @ 120 MHz.
  • Memory: 4MB external PSRAM alongside internal SRAM.
  • Storage: Integrated MicroSD card slot supporting capacities up to 4GB.
  • Sensor Interface: Flex ribbon connector supporting OV2640 (2 Megapixel default) or OV7670 camera modules.
  • Capture Throughput: Up to 8 SVGA (800×600) JPEG frames per second.
  • Illumination: Integrated high-power flash LED, with addressable GPIOs for auxiliary lighting.

Pro-Tip: When provisioning the ESP32-CAM in the Arduino IDE, always set the Partition Scheme to "Huge APP (3MB No OTA)". The image processing routines, secure TCP sockets, and camera framebuffers demand substantial flash memory allocation that will fail under standard partition maps.


Detection Sensors: Passive Infrared (PIR) vs. Physical Contacts

Intrusion detection requires a hardware trigger to wake the camera routine and capture a frame. The standard deployment relies on a Passive Infrared (PIR) radar module (such as the D203S), which senses infrared radiation signatures emitted by moving bodies.

Passive infrared PIR motion sensor module used in the project

For environments unsuitable for thermal motion detection, a physical momentary pushbutton or pressure mat breakout board can be wired across the trigger pins.

Pushbutton mounted on a small breakout board

Key Specs: Trigger Interfaces

  • Operating Logic (PIR): Active-High (logic 0 at rest, transitions to logic 1 upon motion detection).
  • Power Requirement: 3.3 V DC supplied directly from the ESP32-CAM power rail.
  • Hardware Alternative: Microswitch / tactile button tied to ground (requires internal pull-up logic inversion).
  • Dedicated Trigger GPIO: GPIO13 on the ESP32-CAM.

Hardware Wiring and Flashing Setup

The ESP32-CAM lacks an integrated USB-to-UART interface. Firmware programming requires an external USB programmer such as an FTDI module or an Arduino Uno board configured as a bridge.

Table mapping the Arduino and ESP32-Cam connections

During programming, connect U0TXD (GPIO1) and U0RXD (GPIO3) to the respective RX and TX lines of the programming interface. Ground GPIO0 during boot to place the ESP32 into flash mode.

Wiring diagram between Arduino and ESP32-Cam for programming

After flashing, disconnect GPIO0 from GND to resume standard operational mode. Integrate the PIR sensor across 3V3, GND, and GPIO13. The system requires a clean, stabilized power supply between 8 V and 12 V into the Arduino board (or 5 V directly to the ESP32 rail) with a minimum current headroom of 500 mA to prevent brownouts during WiFi transmissions.

Complete circuit diagram including the PIR sensor


Development Environment Setup

Configure the Arduino IDE to target ESP32 hardware using the official Espressif boards package.

  1. Navigate to File -> Preferences and append the following URL to the Additional Boards Manager URLs field:
    https://dl.espressif.com/dl/package_esp32_index.json
    

Adding the board manager URL in the Arduino IDE preferences

  1. Open Tools -> Board -> Boards Manager, search for esp32, and install the package.

Boards Manager window in the Arduino IDE with the esp32 package selected for installation

  1. Set the target board configuration under Tools:
    • Board: "ESP32 Wrover Module"
    • Partition Scheme: "Huge APP (3MB No OTA/1MB SPIFFS)"

Arduino IDE Tools menu showing the ESP32 Wrover Module board and the Huge APP partition scheme selected


Firmware Implementation

The firmware initializes the camera sensor pins, connects to local WiFi, monitors GPIO13, and transfers captured images directly to the Telegram Bot API via TLS multipart/form-data.

Pin Definitions and State Loop

#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

int gpioPIR = 13; // PIR Motion Sensor input pin

void loop()
{
  pinMode(gpioPIR, INPUT_PULLUP);
  int v = digitalRead(gpioPIR);
  if (v == 1) {
    alerts2Telegram(token, chat_id);
    delay(5000); // Debounce delay to prevent burst triggers
  }
  delay(500);
}

Framebuffer Capture & HTTPS Multipart Dispatch

The routine below acquires the frame buffer pointer from the OV2640 driver, establishes a secure socket with api.telegram.org:443, constructs the multipart MIME headers, and streams the buffer in 1024-byte packets.

String alerts2Telegram(String token, String chat_id)
{
  const char* myDomain = "api.telegram.org";
  String getAll = "", getBody = "";

  camera_fb_t * fb = NULL;
  fb = esp_camera_fb_get();
  if(!fb)
  {
    Serial.println("Camera capture failed");
    delay(1000);
    ESP.restart();
    return "Camera capture failed";
  }

  WiFiClientSecure client_tcp;

  if (client_tcp.connect(myDomain, 443))
  {
    Serial.println("Connected to " + String(myDomain));

    String head = "--India\r\nContent-Disposition: form-data; name=\"chat_id\"; \r\n\r\n" + chat_id + "\r\n--India\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"esp32-cam.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n";
    String tail = "\r\n--India--\r\n";

    uint16_t imageLen = fb->len;
    uint16_t extraLen = head.length() + tail.length();
    uint16_t totalLen = imageLen + extraLen;

    client_tcp.println("POST /bot" + token + "/sendPhoto HTTP/1.1");
    client_tcp.println("Host: " + String(myDomain));
    client_tcp.println("Content-Length: " + String(totalLen));
    client_tcp.println("Content-Type: multipart/form-data; boundary=India");
    client_tcp.println();
    client_tcp.print(head);

    uint8_t *fbBuf = fb->buf;
    size_t fbLen = fb->len;

    for (size_t n = 0; n < fbLen; n = n + 1024)
    {
      if (n + 1024 < fbLen)
      {
        client_tcp.write(fbBuf, 1024);
        fbBuf += 1024;
      }
      else if (fbLen % 1024 > 0)
      {
        size_t remainder = fbLen % 1024;
        client_tcp.write(fbBuf, remainder);
      }
    }

    client_tcp.print(tail);
    esp_camera_fb_return(fb);

    int waitTime = 10000; // 10 second timeout
    long startTime = millis();
    boolean state = false;

    while ((startTime + waitTime) > millis())
    {
      Serial.print(".");
      delay(100);
      while (client_tcp.available())
      {
        char c = client_tcp.read();
        if (c == '\n')
        {
          if (getAll.length() == 0) state = true;
          getAll = "";
        }
        else if (c != '\r')
          getAll += String(c);
        if (state == true) getBody += String(c);
        startTime = millis();
      }
      if (getBody.length() > 0) break;
    }
    client_tcp.stop();
    Serial.println(getBody);
  }
  else {
    getBody = "Connection to telegram failed.";
    Serial.println("Connection to telegram failed.");
  }

  return getBody;
}

Telegram Bot Integration

Telegram acts as the endpoint receiver, handling real-time image reception on smartphones without local port forwarding.

  1. Open Telegram, search for @BotFather, and start a conversation.

Telegram search screen with BotFather found in the results

  1. Issue the command /newbot and follow instructions to specify a name and username. BotFather will return the HTTP API access token.

BotFather chat showing the /newbot command and the generated bot token

  1. Locate your bot in Telegram search, click Start, and send an initial message (such as "Hello").
  2. Query the bot's update endpoint via web browser:
    https://api.telegram.org/bot<BOTToken>/getUpdates
    
  3. Extract the numerical chat.id field from the returned JSON payload:
    {
      "ok": true,
      "result": [
        {
          "update_id": 987654321,
          "message": {
            "message_id": 18,
            "chat": {
              "id": 123456789,
              "first_name": "Maker",
              "type": "private"
            },
            "text": "Hello"
          }
        }
      ]
    }
    
  4. Assign token and chat_id into the firmware sketch.

Remote Arming Control (LAMP Server Integration)

To enable and disable intrusion capture remotely without cutting physical power, the ESP32 can poll a remote state file hosted on a lightweight web server or Raspberry Pi LAMP stack (Linux, Apache, MySQL, PHP).

Backend PHP Scripts

Writer Service (lock.php):

<?php
$myfile = fopen("lock.txt", "w") or die("Unable to open file!");
$txt = $_GET['status'];
fwrite($myfile, $txt);
fclose($myfile);
?>

Reader Service (read.php):

<?php
$myfile = fopen("lock.txt", "r") or die("Unable to open file!");
echo fread($myfile, filesize("lock.txt"));
fclose($myfile);
?>

The arming status is stored as raw text (on or off) inside lock.txt.

Firmware State Evaluation

Update the ESP32-CAM sketch to check server state before transmitting:

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "esp_camera.h"

boolean control() {
  HTTPClient http;
  boolean ret = false;
  String serverPath = "http://SERVER_ADDRESS/read.php";

  http.begin(serverPath.c_str());
  int httpResponseCode = http.GET();

  if (httpResponseCode > 0) {
    String payload = http.getString();
    if (payload == "on") {
      ret = true;
    }
  } else {
    Serial.print("State read error: ");
    Serial.println(httpResponseCode);
  }

  http.end();
  return ret;
}

Integrate this check into the transmission pipeline:

if (client_tcp.connect(myDomain, 443) && control()) {
  Serial.println("Connected and armed. Sending frame...");
  // Proceed with multipart data transfer
}

Toggle the surveillance node via any browser:

  • Arm: http://SERVER_ADDRESS/lock.php?status=on
  • Disarm: http://SERVER_ADDRESS/lock.php?status=off

Communication Protocol Comparison

Protocol Architecture Transmission Medium Latency Payload Structure Overhead / Infrastructure Needed
Telegram Bot API (HTTPS POST) RESTful Webhook / TCP 443 1.0 – 2.5s Multipart MIME (JPEG binary) No custom broker needed; uses public cloud endpoints.
MQTT Protocol Broker Publish (TCP 1883/8883) < 300ms JSON text / Base64 binary Requires dedicated broker (Mosquitto/HiveMQ); lightweight.
PHP State Poller (HTTP GET) RESTful HTTP (TCP 80) 100 – 400ms Plaintext (on/off) Requires LAMP stack or local web server instance.

Summary & Implementation Checklist

The combination of the ESP32-CAM and a passive infrared sensor produces a compact, decoupled security appliance capable of sending immediate graphical alerts to mobile devices.

Key takeaways for implementation:

  1. Power Decoupling: Always ensure a stable 5 V rail with at least 500 mA capacity to avoid brownout loops when initializing the camera module and WiFi radio concurrently.
  2. Flash Pin Management: Disconnect GPIO0 from GND immediately after burning firmware, or the board will fail to execute standard runtime routines.
  3. Trigger Sensitivity: Calibrate the PIR sensor potentiometers (sensitivity and hold time) to prevent continuous re-triggering under ambient thermal shifts.

Which communication workflow do you rely on for edge IoT monitoring: direct Telegram bot dispatches, or local MQTT pipelines piped into Home Assistant? Share your hardware setup and optimizations in the comments below.