ESP32-CAM Smart Doorbell Advanced

Build a smart doorbell that captures photos and sends Telegram notifications

🔗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:

FeatureESP32 DevKitESP32-CAM
CameraNoOV2640 (2MP)
MicroSD slotNoYes
USB portYes (built-in)No -- needs FTDI adapter
GPIO pins available~25 usable~5 usable (most taken by camera)
Flash LEDNoBuilt-in bright white LED
PSRAMUsually no4MB PSRAM (for image buffers)
Price~5 USD~6-8 USD
Form factorBreadboard-friendlyCompact, 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

ComponentQtyNotes
ESP32-CAM module (AI-Thinker)1With OV2640 camera
FTDI USB-to-serial adapter13.3V/5V selectable -- use 5V for power, 3.3V for logic
Push button1Momentary, normally open
PIR sensor (HC-SR501)1Optional -- for motion-triggered capture
Piezo buzzer (active)13.3V compatible
LED (any color)1Status indicator
220 ohm resistor1For the LED
10K ohm resistor1Pull-up for the button
Breadboard1
Jumper wires~12Male-to-female mostly
5V power supply1At least 500mA (USB or wall adapter)

🔗Software Requirements

In the Arduino IDE:

  1. Add ESP32 board support (if not already done): File > Preferences > Additional Board URLs, add https://dl.espressif.com/dl/package_esp32_index.json
  2. Select board: AI Thinker ESP32-CAM
  3. Install the UniversalTelegramBot library by Brian Lough via Library Manager
  4. 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:

  1. Open Telegram and search for @BotFather
  2. Send /newbot and follow the prompts to name your bot
  3. BotFather gives you a bot token (looks like 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw). Save this.
  4. Search for @userinfobot (or @myidbot) in Telegram and send /start. It replies with your chat ID (a number like 123456789). 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:

GPIOAvailable?Notes
GPIO 0LimitedMust be LOW during upload. Can use as input after boot
GPIO 1NoTX (serial output)
GPIO 2LimitedConnected to microSD and onboard LED
GPIO 3NoRX (serial input)
GPIO 4LimitedConnected to flash LED. Can use if flash not needed
GPIO 12CautionUsed by microSD. Free if SD not used
GPIO 13YesBest general-purpose GPIO
GPIO 14CautionUsed by microSD. Free if SD not used
GPIO 15CautionUsed by microSD. Free if SD not used
GPIO 16YesAvailable
GPIO 33YesOnboard 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 PinESP32-CAM Pin
5V5V
GNDGND
TXU0R (GPIO 3)
RXU0T (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 --- CG

Upload 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:

ComponentPinESP32-CAM Pin
Push buttonLeg 1GPIO 13
Push buttonLeg 2GND (through 10K pull-down) and 3.3V
PIR sensor (HC-SR501)VCC5V
PIR sensorGNDGND
PIR sensorOUTGPIO 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:

SettingValue
BoardAI Thinker ESP32-CAM
Partition SchemeHuge APP (3MB No OTA/1MB SPIFFS)
Upload Speed115200
PortYour 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:

  1. Wire the FTDI adapter as shown in Step 1 (including GPIO 0 to GND)
  2. In Arduino IDE, select the correct COM port for your FTDI adapter
  3. Press the RST button on the ESP32-CAM board
  4. Click Upload in the Arduino IDE
  5. Wait for "Connecting........" to appear, then for the upload to complete
  6. Disconnect GPIO 0 from GND (this is the most commonly forgotten step)
  7. Press RST again to boot the program
  8. 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

  1. Serial Monitor: Confirm WiFi connects and the IP address is shown
  2. Web stream: Open http://<IP>/stream in a browser on the same network. You should see a live video feed.
  3. Telegram: Send /photo to your bot. It should reply with a photo within a few seconds.
  4. Doorbell button: Press the button. You should hear a beep and receive a photo on Telegram.
  5. 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 SizeResolutionTypical JPEG SizeBest For
QQVGA160x120~5 KBFastest, minimal bandwidth
QVGA320x240~15 KBFast streaming
VGA640x480~40 KBGood streaming
SVGA800x600~60 KBBalanced
XGA1024x768~80 KBGood still photos
SXGA1280x1024~120 KBHigh quality photos
UXGA1600x1200~180 KBMaximum 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:

StateCurrent 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

ProblemCauseFix
"Camera init failed"Camera ribbon cable looseReseat the ribbon cable firmly. The connector has a small latch that lifts up
Brownout reset (constant rebooting)Insufficient power supplyUse 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 pressedEnsure GPIO 0 is connected to GND, press RST, then upload
Photo is darkFlash LED not firing, or camera settingsIncrease FLASH_LED_PIN on-time. Adjust brightness/exposure in camera settings
Photo is overexposedFlash LED too bright at close rangeReduce flash duration or use s->set_exposure_ctrl(s, 1) for auto exposure
Telegram "Failed to connect"WiFi not connected, or time not syncedCheck WiFi status. setInsecure() should bypass cert issues
Stream is very slowResolution too highReduce to VGA or QVGA for smoother streaming
PIR triggers constantlyPIR sensitivity too high, or in direct sunlightReduce PIR sensitivity potentiometer. Avoid pointing at heat sources or windows
GPIO 12 boot failureGPIO 12 affects flash voltage on bootIf 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 resolutionReduce frame size. Ensure PSRAM is detected (psramFound() returns true)
Bot doesn't respond to commandsWrong chat ID or bot tokenDouble-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.