🔗Goal
Build a smart doorbell using the ESP32-CAM module. When a visitor presses the doorbell button, the ESP32-CAM takes a photo and sends it to your phone via a Telegram bot. Optionally, a PIR motion sensor can trigger automatic photo capture when someone approaches. A built-in web server also provides a live camera stream you can check from any device on your network.
graph LR
A[Doorbell Button] -->|GPIO| B[ESP32-CAM]
C[PIR Sensor] -->|GPIO| B
B -->|WiFi| D[Telegram Bot API]
D --> E[Your Phone]
B -->|WiFi| F[Web Stream<br/>Local Network]🔗About the ESP32-CAM
This project uses the ESP32-CAM module, which is a different board from the ESP32-WROOM-32 DevKit used in the rest of this site. Here are the key differences:
| Feature | ESP32 DevKit | ESP32-CAM |
|---|---|---|
| Camera | No | OV2640 (2MP) |
| MicroSD slot | No | Yes |
| USB port | Yes (built-in) | No -- needs FTDI adapter |
| GPIO pins available | ~25 usable | ~5 usable (most taken by camera) |
| Flash LED | No | Built-in bright white LED |
| PSRAM | Usually no | 4MB PSRAM (for image buffers) |
| Price | ~5 USD | ~6-8 USD |
| Form factor | Breadboard-friendly | Compact, not breadboard-friendly |
The ESP32-CAM packs a lot into a small board, but the trade-off is very limited GPIO pins and no USB port for programming. You will need an FTDI USB-to-serial adapter to upload code.
🔗Prerequisites
| Component | Qty | Notes |
|---|---|---|
| ESP32-CAM module (AI-Thinker) | 1 | With OV2640 camera |
| FTDI USB-to-serial adapter | 1 | 3.3V/5V selectable -- use 5V for power, 3.3V for logic |
| Push button | 1 | Momentary, normally open |
| PIR sensor (HC-SR501) | 1 | Optional -- for motion-triggered capture |
| Piezo buzzer (active) | 1 | 3.3V compatible |
| LED (any color) | 1 | Status indicator |
| 220 ohm resistor | 1 | For the LED |
| 10K ohm resistor | 1 | Pull-up for the button |
| Breadboard | 1 | |
| Jumper wires | ~12 | Male-to-female mostly |
| 5V power supply | 1 | At least 500mA (USB or wall adapter) |
🔗Software Requirements
In the Arduino IDE:
- Add ESP32 board support (if not already done): File > Preferences > Additional Board URLs, add
https://dl.espressif.com/dl/package_esp32_index.json - Select board: AI Thinker ESP32-CAM
- Install the UniversalTelegramBot library by Brian Lough via Library Manager
- Install the ArduinoJson library by Benoit Blanchon (version 6.x or 7.x)
🔗Setting Up the Telegram Bot
Before building the hardware, create your Telegram bot:
- Open Telegram and search for @BotFather
- Send
/newbotand follow the prompts to name your bot - BotFather gives you a bot token (looks like
110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw). Save this. - Search for @userinfobot (or @myidbot) in Telegram and send
/start. It replies with your chat ID (a number like123456789). Save this.
Keep your bot token private. Anyone with this token can control your bot.
🔗ESP32-CAM Pin Map
The ESP32-CAM has very few available GPIO pins because most are used by the camera and microSD card. Here is what is available:
| GPIO | Available? | Notes |
|---|---|---|
| GPIO 0 | Limited | Must be LOW during upload. Can use as input after boot |
| GPIO 1 | No | TX (serial output) |
| GPIO 2 | Limited | Connected to microSD and onboard LED |
| GPIO 3 | No | RX (serial input) |
| GPIO 4 | Limited | Connected to flash LED. Can use if flash not needed |
| GPIO 12 | Caution | Used by microSD. Free if SD not used |
| GPIO 13 | Yes | Best general-purpose GPIO |
| GPIO 14 | Caution | Used by microSD. Free if SD not used |
| GPIO 15 | Caution | Used by microSD. Free if SD not used |
| GPIO 16 | Yes | Available |
| GPIO 33 | Yes | Onboard small red LED (active LOW) |
For this project, we will use:
- GPIO 13 -- Doorbell button input
- GPIO 12 -- PIR sensor input (we are not using the microSD card)
- GPIO 2 -- Buzzer output
- GPIO 33 -- Onboard LED (status indicator, no external LED needed)
🔗System Architecture
graph TD
subgraph ESP32-CAM
CAM[OV2640 Camera]
BTN[GPIO 13: Button]
PIR[GPIO 12: PIR]
BUZ[GPIO 2: Buzzer]
LED[GPIO 33: LED]
WEB[Web Server :80]
end
BTN -->|Press detected| LOGIC[Main Logic]
PIR -->|Motion detected| LOGIC
LOGIC -->|Capture| CAM
CAM -->|JPEG| SEND[Send to Telegram]
SEND -->|HTTPS| TG[Telegram Bot API]
TG --> PHONE[Your Phone]
WEB -->|MJPEG stream| BROWSER[Browser on LAN]The program flow when the doorbell is pressed:
graph TD
A[Button pressed] --> B[Beep buzzer]
B --> C[Flash LED]
C --> D[Capture photo]
D --> E{Photo OK?}
E -->|Yes| F[Send to Telegram]
E -->|No| G[Retry capture]
G --> D
F --> H[Wait for cooldown<br/>5 seconds]
H --> I[Resume monitoring]🔗Tutorial
🔗Step 1: Wiring for Programming
To upload code to the ESP32-CAM, you need an FTDI adapter because the board has no USB port. Wire it as follows:
| FTDI Pin | ESP32-CAM Pin |
|---|---|
| 5V | 5V |
| GND | GND |
| TX | U0R (GPIO 3) |
| RX | U0T (GPIO 1) |
Critical: Connect GPIO 0 to GND with a jumper wire. This puts the ESP32-CAM into flash/upload mode. You must remove this jumper after uploading and before running the code.
graph LR
subgraph FTDI Adapter
FV[5V]
FG[GND]
FT[TX]
FR[RX]
end
subgraph ESP32-CAM
CV[5V]
CG[GND]
CR[U0R / GPIO 3]
CT[U0T / GPIO 1]
C0[GPIO 0]
end
FV --- CV
FG --- CG
FT --- CR
FR --- CT
C0 --- CGUpload procedure: (1) Connect GPIO 0 to GND. (2) Press the RST button on the ESP32-CAM (or unplug/replug power). (3) Click Upload in Arduino IDE. (4) When you see "Connecting...", the board is in flash mode. (5) After upload completes, disconnect GPIO 0 from GND. (6) Press RST again to run the program.
🔗Step 2: Wiring the Doorbell Components
After programming, remove the FTDI adapter (or keep it connected for serial debugging) and wire the doorbell components:
| Component | Pin | ESP32-CAM Pin |
|---|---|---|
| Push button | Leg 1 | GPIO 13 |
| Push button | Leg 2 | GND (through 10K pull-down) and 3.3V |
| PIR sensor (HC-SR501) | VCC | 5V |
| PIR sensor | GND | GND |
| PIR sensor | OUT | GPIO 12 |
| Active buzzer (+) | -- | GPIO 2 |
| Active buzzer (-) | -- | GND |
The button wiring uses a pull-down resistor configuration: one leg connects to 3.3V and GPIO 13, the other leg connects to GND through a 10K resistor. When pressed, GPIO 13 reads HIGH. When released, the resistor pulls it LOW.
PIR sensor adjustment: The HC-SR501 has two potentiometers on the back. The left one adjusts sensitivity (detection range) and the right one adjusts the delay time (how long the output stays HIGH after detection). For a doorbell, set the delay to minimum (~3 seconds) and sensitivity to about midway.
🔗Step 3: Arduino IDE Board Settings
Before uploading, configure these settings in the Arduino IDE under Tools:
| Setting | Value |
|---|---|
| Board | AI Thinker ESP32-CAM |
| Partition Scheme | Huge APP (3MB No OTA/1MB SPIFFS) |
| Upload Speed | 115200 |
| Port | Your FTDI adapter's COM port |
The "Huge APP" partition scheme gives maximum space for the program, which is needed because the camera library and HTTPS client are large.
🔗Step 4: Upload the Code
This is a long sketch because it handles the camera, WiFi, Telegram, web streaming, and input monitoring all at once.
#include "esp_camera.h"
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
// ==== CONFIGURATION ====
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// Telegram bot token and chat ID from BotFather / userinfobot
const char* botToken = "YOUR_BOT_TOKEN";
const char* chatId = "YOUR_CHAT_ID";
// ==== PIN DEFINITIONS ====
#define BUTTON_PIN 13
#define PIR_PIN 12
#define BUZZER_PIN 2
#define LED_PIN 33 // Onboard LED (active LOW)
#define FLASH_LED_PIN 4 // Onboard flash LED
// ==== CAMERA PIN DEFINITIONS (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
// ==== TIMING ====
#define DEBOUNCE_MS 200
#define COOLDOWN_MS 5000 // Min time between captures
#define BOT_CHECK_MS 2000 // Check Telegram every 2 seconds
#define PIR_ENABLED true // Set false to disable PIR
WiFiClientSecure secured;
UniversalTelegramBot bot(botToken, secured);
unsigned long lastCapture = 0;
unsigned long lastBotCheck = 0;
bool pirEnabled = PIR_ENABLED;
// Web server for live stream
WiFiServer streamServer(80);
void 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;
// Use higher resolution if PSRAM is available
if (psramFound()) {
config.frame_size = FRAMESIZE_UXGA; // 1600x1200
config.jpeg_quality = 10; // Lower = better quality
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_SVGA; // 800x600
config.jpeg_quality = 12;
config.fb_count = 1;
}
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x\n", err);
ESP.restart();
}
// Adjust settings for better doorbell photos
sensor_t *s = esp_camera_sensor_get();
s->set_framesize(s, FRAMESIZE_XGA); // 1024x768 -- good balance
s->set_quality(s, 10);
s->set_brightness(s, 1); // Slightly brighter
s->set_contrast(s, 1);
}
camera_fb_t* capturePhoto() {
// Turn on flash LED briefly for better photos
digitalWrite(FLASH_LED_PIN, HIGH);
delay(100);
camera_fb_t *fb = esp_camera_fb_get();
digitalWrite(FLASH_LED_PIN, LOW);
if (!fb) {
Serial.println("Camera capture failed!");
return nullptr;
}
Serial.printf("Photo captured: %d bytes (%dx%d)\n",
fb->len, fb->width, fb->height);
return fb;
}
bool sendPhotoTelegram(camera_fb_t *fb, const char* caption) {
Serial.println("Sending photo to Telegram...");
const char* boundary = "----ESP32CAMBoundary";
String head = String("--") + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n"
+ chatId + "\r\n"
+ "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"caption\"\r\n\r\n"
+ caption + "\r\n"
+ "--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"photo\"; "
+ "filename=\"doorbell.jpg\"\r\n"
+ "Content-Type: image/jpeg\r\n\r\n";
String tail = "\r\n--" + String(boundary) + "--\r\n";
uint32_t totalLen = head.length() + fb->len + tail.length();
secured.connect("api.telegram.org", 443);
if (!secured.connected()) {
Serial.println("Failed to connect to Telegram API");
return false;
}
secured.println("POST /bot" + String(botToken) + "/sendPhoto HTTP/1.1");
secured.println("Host: api.telegram.org");
secured.println("Content-Type: multipart/form-data; boundary=" + String(boundary));
secured.println("Content-Length: " + String(totalLen));
secured.println();
secured.print(head);
// Send photo data in chunks
uint8_t *buf = fb->buf;
size_t remaining = fb->len;
size_t chunkSize = 1024;
while (remaining > 0) {
size_t toSend = (remaining < chunkSize) ? remaining : chunkSize;
secured.write(buf, toSend);
buf += toSend;
remaining -= toSend;
}
secured.print(tail);
// Read response (wait up to 10 seconds)
unsigned long timeout = millis() + 10000;
while (!secured.available() && millis() < timeout) {
delay(100);
}
bool success = false;
if (secured.available()) {
String response = secured.readString();
success = response.indexOf("\"ok\":true") > 0;
if (!success) {
Serial.println("Telegram API error:");
Serial.println(response.substring(0, 200));
}
}
secured.stop();
return success;
}
void beepBuzzer(int beeps) {
for (int i = 0; i < beeps; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(100);
digitalWrite(BUZZER_PIN, LOW);
if (i < beeps - 1) delay(100);
}
}
void blinkLED(int times) {
for (int i = 0; i < times; i++) {
digitalWrite(LED_PIN, LOW); // Active LOW
delay(100);
digitalWrite(LED_PIN, HIGH);
if (i < times - 1) delay(100);
}
}
void handleTrigger(const char* source) {
unsigned long now = millis();
if (now - lastCapture < COOLDOWN_MS) {
Serial.println("Cooldown active, ignoring trigger");
return;
}
lastCapture = now;
Serial.printf("Triggered by: %s\n", source);
// Alert: beep and blink
beepBuzzer(2);
blinkLED(3);
// Capture photo
camera_fb_t *fb = capturePhoto();
if (fb) {
char caption[64];
snprintf(caption, sizeof(caption), "Doorbell: %s", source);
bool sent = sendPhotoTelegram(fb, caption);
if (sent) {
Serial.println("Photo sent successfully!");
beepBuzzer(1); // Confirmation beep
} else {
Serial.println("Failed to send photo");
beepBuzzer(3); // Error beeps
}
esp_camera_fb_return(fb);
}
}
void handleStream(WiFiClient &client) {
String response = "HTTP/1.1 200 OK\r\n"
"Content-Type: multipart/x-mixed-replace; "
"boundary=frame\r\n\r\n";
client.print(response);
while (client.connected()) {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Stream: capture failed");
break;
}
String header = "--frame\r\n"
"Content-Type: image/jpeg\r\n"
"Content-Length: " + String(fb->len) + "\r\n\r\n";
client.print(header);
client.write(fb->buf, fb->len);
client.print("\r\n");
esp_camera_fb_return(fb);
if (!client.connected()) break;
delay(100); // ~10 fps
}
}
void handleWebRequest(WiFiClient &client) {
String request = client.readStringUntil('\r');
client.flush();
if (request.indexOf("GET /stream") >= 0) {
handleStream(client);
}
else if (request.indexOf("GET /capture") >= 0) {
camera_fb_t *fb = capturePhoto();
if (fb) {
String header = "HTTP/1.1 200 OK\r\n"
"Content-Type: image/jpeg\r\n"
"Content-Length: " + String(fb->len) + "\r\n\r\n";
client.print(header);
client.write(fb->buf, fb->len);
esp_camera_fb_return(fb);
}
}
else {
// Serve the main page
String html = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"
"<!DOCTYPE html><html><head>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>ESP32-CAM Doorbell</title>"
"<style>"
"body{font-family:sans-serif;text-align:center;"
"background:#1a1a1a;color:#fff;margin:20px;}"
"img{max-width:100%;border:2px solid #444;border-radius:8px;}"
"a{color:#4fc3f7;font-size:1.2em;}"
"h1{color:#81c784;}"
"</style></head><body>"
"<h1>ESP32-CAM Doorbell</h1>"
"<p><img src='/stream' alt='Live Stream'></p>"
"<p><a href='/capture'>Take Snapshot</a></p>"
"</body></html>";
client.print(html);
}
}
void checkTelegramMessages() {
int numNew = bot.getUpdates(bot.last_message_received + 1);
while (numNew) {
for (int i = 0; i < numNew; i++) {
String text = bot.messages[i].text;
String fromId = bot.messages[i].chat_id;
if (fromId != String(chatId)) {
bot.sendMessage(fromId, "Unauthorized.", "");
continue;
}
if (text == "/photo" || text == "/snap") {
camera_fb_t *fb = capturePhoto();
if (fb) {
sendPhotoTelegram(fb, "Manual capture via Telegram");
esp_camera_fb_return(fb);
} else {
bot.sendMessage(chatId, "Failed to capture photo.", "");
}
}
else if (text == "/pir_on") {
pirEnabled = true;
bot.sendMessage(chatId, "PIR motion detection enabled.", "");
}
else if (text == "/pir_off") {
pirEnabled = false;
bot.sendMessage(chatId, "PIR motion detection disabled.", "");
}
else if (text == "/status") {
String status = "Doorbell online.\n"
"PIR: " + String(pirEnabled ? "ON" : "OFF") + "\n"
"IP: " + WiFi.localIP().toString() + "\n"
"RSSI: " + String(WiFi.RSSI()) + " dBm\n"
"Uptime: " + String(millis()/1000) + " sec";
bot.sendMessage(chatId, status, "");
}
else if (text == "/start" || text == "/help") {
String help = "ESP32-CAM Doorbell Commands:\n"
"/photo - Take a photo now\n"
"/pir_on - Enable motion detection\n"
"/pir_off - Disable motion detection\n"
"/status - Show system status";
bot.sendMessage(chatId, help, "");
}
}
numNew = bot.getUpdates(bot.last_message_received + 1);
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n=== ESP32-CAM Smart Doorbell ===");
// Initialize pins
pinMode(BUTTON_PIN, INPUT_PULLDOWN);
pinMode(PIR_PIN, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(FLASH_LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH); // OFF (active LOW)
digitalWrite(FLASH_LED_PIN, LOW); // OFF
digitalWrite(BUZZER_PIN, LOW);
// Initialize camera
initCamera();
Serial.println("Camera initialized");
// Connect WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nConnected! IP: %s\n",
WiFi.localIP().toString().c_str());
} else {
Serial.println("\nWiFi failed! Restarting...");
ESP.restart();
}
// Set time for HTTPS certificate validation
secured.setInsecure(); // Skip cert validation (simpler for IoT)
// Start web server
streamServer.begin();
Serial.println("Stream server started on port 80");
Serial.printf("View stream at: http://%s/stream\n",
WiFi.localIP().toString().c_str());
// Startup notification
bot.sendMessage(chatId, "Doorbell is online! Use /help for commands.", "");
beepBuzzer(1);
Serial.println("Ready. Waiting for button press or motion...");
}
void loop() {
// Check doorbell button (with debounce)
static unsigned long lastButtonPress = 0;
if (digitalRead(BUTTON_PIN) == HIGH) {
if (millis() - lastButtonPress > DEBOUNCE_MS) {
lastButtonPress = millis();
handleTrigger("Button press");
}
}
// Check PIR sensor
if (pirEnabled && digitalRead(PIR_PIN) == HIGH) {
handleTrigger("Motion detected");
}
// Check for Telegram commands periodically
if (millis() - lastBotCheck > BOT_CHECK_MS) {
lastBotCheck = millis();
checkTelegramMessages();
}
// Handle web server clients
WiFiClient client = streamServer.available();
if (client) {
handleWebRequest(client);
client.stop();
}
delay(10);
}🔗Step 5: Upload Process
The upload process for the ESP32-CAM is more involved than a regular DevKit:
- Wire the FTDI adapter as shown in Step 1 (including GPIO 0 to GND)
- In Arduino IDE, select the correct COM port for your FTDI adapter
- Press the RST button on the ESP32-CAM board
- Click Upload in the Arduino IDE
- Wait for "Connecting........" to appear, then for the upload to complete
- Disconnect GPIO 0 from GND (this is the most commonly forgotten step)
- Press RST again to boot the program
- Open Serial Monitor at 115200 baud to see the IP address and status messages
If you see "Failed to connect to ESP32: Timed out waiting for packet header", make sure GPIO 0 is connected to GND and press RST again before uploading.
🔗Step 6: Test the System
- Serial Monitor: Confirm WiFi connects and the IP address is shown
- Web stream: Open
http://<IP>/streamin a browser on the same network. You should see a live video feed. - Telegram: Send
/phototo your bot. It should reply with a photo within a few seconds. - Doorbell button: Press the button. You should hear a beep and receive a photo on Telegram.
- PIR sensor: Walk in front of the sensor. You should receive a "Motion detected" photo.
🔗Step 7: Enclosure and Mounting
For a permanent installation, you will want an enclosure. Some tips:
- Use a small project box (roughly $80 \times 50 \times 30\,\text{mm}$) or 3D-print a case
- Cut a hole for the camera lens and the PIR sensor dome
- Mount the push button on the front face where visitors can reach it
- Use a 5V USB power supply run through the wall (the ESP32-CAM draws about $200\,\text{mA}$ average, with peaks up to $400\,\text{mA}$ during WiFi transmission)
- Position the camera at roughly $1.5\,\text{m}$ height for face-level photos
🔗Image Quality and Resolution
The OV2640 camera supports several resolutions. The code uses XGA ($1024 \times 768$) as a good compromise between quality and transmission speed. Here are the options you can set in initCamera():
| Frame Size | Resolution | Typical JPEG Size | Best For |
|---|---|---|---|
| QQVGA | 160x120 | ~5 KB | Fastest, minimal bandwidth |
| QVGA | 320x240 | ~15 KB | Fast streaming |
| VGA | 640x480 | ~40 KB | Good streaming |
| SVGA | 800x600 | ~60 KB | Balanced |
| XGA | 1024x768 | ~80 KB | Good still photos |
| SXGA | 1280x1024 | ~120 KB | High quality photos |
| UXGA | 1600x1200 | ~180 KB | Maximum quality |
Higher resolutions mean larger files and slower Telegram uploads. XGA or SVGA are usually the sweet spot for a doorbell application.
The JPEG quality setting ranges from 0 (best quality, largest file) to 63 (worst quality, smallest file). A value of 10-12 gives good results.
🔗Power Considerations
The ESP32-CAM draws significantly more current than a regular ESP32 DevKit:
| State | Current Draw |
|---|---|
| Idle (WiFi connected, no capture) | ~120 mA |
| Camera active (streaming) | ~260 mA |
| Camera + Flash LED | ~350 mA |
| WiFi transmission peak | ~400 mA |
This means the ESP32-CAM is not practical for battery operation in always-on mode. Use a 5V USB power supply with at least $500\,\text{mA}$ capacity (most phone chargers provide $1{-}2\,\text{A}$ and work well).
🔗Common Issues and Solutions
| Problem | Cause | Fix |
|---|---|---|
| "Camera init failed" | Camera ribbon cable loose | Reseat the ribbon cable firmly. The connector has a small latch that lifts up |
| Brownout reset (constant rebooting) | Insufficient power supply | Use a 5V supply with at least 500mA. Avoid powering from a laptop USB hub |
| Upload fails ("Timed out") | GPIO 0 not grounded, or RST not pressed | Ensure GPIO 0 is connected to GND, press RST, then upload |
| Photo is dark | Flash LED not firing, or camera settings | Increase FLASH_LED_PIN on-time. Adjust brightness/exposure in camera settings |
| Photo is overexposed | Flash LED too bright at close range | Reduce flash duration or use s->set_exposure_ctrl(s, 1) for auto exposure |
| Telegram "Failed to connect" | WiFi not connected, or time not synced | Check WiFi status. setInsecure() should bypass cert issues |
| Stream is very slow | Resolution too high | Reduce to VGA or QVGA for smoother streaming |
| PIR triggers constantly | PIR sensitivity too high, or in direct sunlight | Reduce PIR sensitivity potentiometer. Avoid pointing at heat sources or windows |
| GPIO 12 boot failure | GPIO 12 affects flash voltage on boot | If PIR keeps GPIO 12 HIGH during boot, the ESP32 may not start. Add a 10K pull-down resistor on GPIO 12 or use a different pin |
| "Guru Meditation Error" | Memory issue with high resolution | Reduce frame size. Ensure PSRAM is detected (psramFound() returns true) |
| Bot doesn't respond to commands | Wrong chat ID or bot token | Double-check both values. Make sure you messaged the correct bot |
🔗Extensions
- MicroSD recording: Save every doorbell photo to the microSD card with a timestamp filename. The ESP32-CAM has a built-in microSD slot (uses GPIO 2, 4, 12, 13, 14, 15 -- some conflicts with our pin choices, so you would need to rearrange).
- Night vision: Replace the OV2640 with an OV2640 IR-CUT module and add IR LEDs for night-time visibility.
- Multiple recipients: Send photos to a Telegram group instead of a single user. Create a group, add the bot, and use the group's chat ID (it will be a negative number).
- Video clips: Capture a short burst of frames (5-10 JPEG frames) and send them as an animation/GIF.
- MQTT integration: Publish doorbell events to an MQTT broker for integration with Home Assistant or other automation platforms.
- Two-way audio: The ESP32 supports I2S audio. With an INMP441 microphone and a small speaker, you could add intercom functionality (this is an advanced extension).
- Face detection: The ESP32-CAM library includes a basic face detection function. You could trigger the doorbell capture only when a face is detected, reducing false alarms from the PIR sensor.