
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.

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.

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

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.

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.

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.

Development Environment Setup
Configure the Arduino IDE to target ESP32 hardware using the official Espressif boards package.
- 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

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

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

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.
- Open Telegram, search for
@BotFather, and start a conversation.

- Issue the command
/newbotand follow instructions to specify a name and username. BotFather will return the HTTP API accesstoken.

- Locate your bot in Telegram search, click Start, and send an initial message (such as "Hello").
- Query the bot's update endpoint via web browser:
https://api.telegram.org/bot<BOTToken>/getUpdates - Extract the numerical
chat.idfield 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" } } ] } - Assign
tokenandchat_idinto 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:
- 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.
- Flash Pin Management: Disconnect
GPIO0fromGNDimmediately after burning firmware, or the board will fail to execute standard runtime routines. - 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.