How to Build an ESP32 Video Doorbell

Local Wi-Fi live view with a real doorbell button, chime, and ring counter

ESP32SecurityIntermediate45 minutes5 components

Updated

How to Build an ESP32 Video Doorbell
For illustrative purposes only
On this page

What you'll build

This guide turns an ESP32-CAM into a small local video doorbell. The board hosts a browser page on your 2.4 GHz Wi-Fi network, refreshes the camera feed every second, and shows a ring counter so you can tell when the physical button was pressed. A passive buzzer plays a short two-note chime when the button is pressed or the test-chime button is clicked in the browser, and the onboard flash LED blinks as a quick visual confirmation.

The hardware stays deliberately simple: an AI Thinker-style ESP32-CAM board, its USB programmer adapter, one normally-open doorbell button on GPIO 13, and one passive piezo buzzer on GPIO 14. The sketch uses the internal pull-up for the button and the onboard GPIO 4 flash LED for feedback, so no resistors are needed beyond what the buzzer already has in its module.

Keep this as a private LAN prototype. It is a useful front-door, workshop, or room-alert build, but it is not an internet-exposed security product until you add authentication, HTTPS, weatherproofing, and a more permanent power plan. If you want the camera-only version first, start with the ESP32 Security Camera guide and then come back to add the doorbell interaction.

What you are building

The firmware has five jobs:

  1. initialise the OV2640 with the AI Thinker pin map,
  2. connect to your 2.4 GHz Wi-Fi network using YOUR_WIFI_SSID and YOUR_WIFI_PASSWORD,
  3. serve a browser page with a ring counter, a "last pressed" timestamp, a live camera feed, and a test-chime button,
  4. respond to the physical button on DOORBELL_BUTTON_PIN (GPIO 13) via interrupt, play the two-note chime on CHIME_PIN (GPIO 14), blink the flash LED on FLASH_LED_PIN (GPIO 4), and increment the ring count,
  5. serve JPEG frames at /capture and handle test-ring requests at /test-ring.

Cloud push, authentication, motion detection, and weatherproofing are outside this scope.

Upload and calibrate

Set the two Wi-Fi placeholders in the sketch before deploying:

const char* ssid     = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

After uploading and resetting, Serial Monitor at 115200 baud shows:

Video doorbell ready
Open http://192.168.x.x

Open that address from a browser on the same 2.4 GHz network. Press the physical button and confirm:

  • the ring counter increments,
  • the two-note chime plays (988 Hz then 1319 Hz),
  • the flash LED blinks briefly.

Use the "Test chime" button in the browser to verify audio without physically pressing the button. If the feed is slow, change FRAMESIZE_VGA to FRAMESIZE_QVGA in the setupCamera() function. The page also auto-reloads every 10 seconds to keep the ring counter current.

Troubleshooting

  • Camera init failed at boot. Wrong board profile selected, or the camera ribbon is not fully seated. Re-select the AI Thinker ESP32-CAM profile and reseat the ribbon.
  • Button press is not detected or fires randomly. Check the GPIO 13 connection to GND. The interrupt fires on a falling edge, so a floating pin can cause spurious triggers. A short wire directly from the button to GND is more reliable than a long run.
  • Chime sounds distorted or buzzer is silent. Confirm the passive buzzer signal is on GPIO 14. An active buzzer (with its own oscillator) will not play the tone() melody and may produce a single fixed pitch; swap it for a passive one.
  • ESP32-CAM resets during capture or Wi-Fi connection. Supply current is too low. The camera and Wi-Fi together can exceed what some USB hubs provide; use a dedicated 5 V supply at 1 A or more.
  • Browser ring counter does not update after a press. The page reloads every 10 seconds automatically, or click refresh manually. The /test-ring route also increments the counter if you want to test the full cycle.
  • Serial Monitor shows Wi-Fi connecting but never gets an IP. The network is probably 5 GHz-only. The ESP32-CAM supports 2.4 GHz only.

Going further

Once the doorbell is working locally, two natural extensions are push notifications and weatherproofing. For notifications, an MQTT publish on each ring event is straightforward; pair that with a Home Assistant automation or a Telegram bot and the ring reaches your phone without any cloud camera service.

For a more self-contained version that configures its own Wi-Fi credentials through a browser portal and can recover without reflashing, the ESP32-CAM Setup Portal Security Camera guide covers the Preferences-based credential storage pattern that slots cleanly into the doorbell code.

Wiring diagram

Loading diagram…
Interactive wiring diagram

Components needed

Supplier links, prices, and availability are shown as a guide and may change. Schematik may earn a commission from purchases made through affiliate links.

Assembly

1

Prepare the ESP32-CAM

Seat the ESP32-CAM on its USB programmer adapter, connect it to your computer, and select the AI Thinker ESP32-CAM style board profile.

2

Wire the doorbell button

Connect one side of the momentary button to GPIO13 and the other side to GND. The sketch uses the ESP32 internal pull-up, so no external resistor is needed for the basic build.

3

Add the local chime

Connect the passive buzzer signal pin to GPIO14 and its ground pin to GND. The onboard flash LED on GPIO4 is used as a short visual blink during each ring.

4

Upload Wi-Fi details and test the feed

Replace the Wi-Fi placeholders, upload the sketch, then open the IP address printed in Serial Monitor from a browser on the same 2.4 GHz network. Press the button and confirm the ring count, chime, LED blink, and camera refresh all work.

5

Mount the prototype

Place the ESP32-CAM behind a faceplate or small enclosure so the lens has a clear view and the button is easy to press. Leave USB accessible while you are still tuning the sketch.

Pin assignments

PinConnectionType
GPIO 13doorbell-button-1 SIGNALDIGITAL INPUT
GNDdoorbell-button-1 GNDGROUND
GPIO 14doorbell-chime-1 SIGTONE OUTPUT
GNDdoorbell-chime-1 GNDGROUND

Code

Arduino C++
#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

#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 DOORBELL_BUTTON_PIN 13
#define CHIME_PIN 14
#define FLASH_LED_PIN 4

WebServer server(80);
volatile bool buttonPressed = false;
unsigned long lastRingMs = 0;
unsigned long ringCount = 0;

void IRAM_ATTR onDoorbellPress() {
  buttonPressed = true;
}

String htmlPage() {
  String ringState = ringCount == 0 ? "Waiting for the first press" : "Last press " + String((millis() - lastRingMs) / 1000) + " seconds ago";
  String page = "<!doctype html><html><head><meta name='viewport' content='width=device-width, initial-scale=1'>";
  page += "<title>ESP32 Video Doorbell</title><style>body{font-family:sans-serif;background:#111;color:#fff;margin:0;padding:24px;text-align:center}";
  page += "img{width:100%;max-width:640px;border-radius:18px;border:1px solid #333}.card{max-width:680px;margin:auto}.badge{display:inline-block;background:#0f766e;padding:8px 12px;border-radius:999px;margin:12px}</style></head>";
  page += "<body><div class='card'><h1>ESP32 Video Doorbell</h1><div class='badge'>Rings: " + String(ringCount) + "</div><p>" + ringState + "</p>";
  page += "<img src='/capture' id='cam'><p><button onclick=\"fetch('/test-ring')\">Test chime</button></p>";
  page += "<script>setInterval(()=>{document.getElementById('cam').src='/capture?t='+Date.now()},800);setInterval(()=>location.reload(),10000)</script>";
  page += "</div></body></html>";
  return page;
}

void playChime() {
  digitalWrite(FLASH_LED_PIN, HIGH);
  tone(CHIME_PIN, 988, 120);
  delay(160);
  tone(CHIME_PIN, 1319, 180);
  delay(220);
  noTone(CHIME_PIN);
  digitalWrite(FLASH_LED_PIN, LOW);
}

void handleRoot() {
  server.send(200, "text/html", htmlPage());
}

void handleCapture() {
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    server.send(500, "text/plain", "Camera capture failed");
    return;
  }
  server.sendHeader("Cache-Control", "no-store");
  server.send_P(200, "image/jpeg", (const char *)fb->buf, fb->len);
  esp_camera_fb_return(fb);
}

void handleTestRing() {
  lastRingMs = millis();
  ringCount++;
  playChime();
  server.send(200, "text/plain", "Doorbell test ring recorded");
}

void setupCamera() {
  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_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_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;
  config.frame_size = FRAMESIZE_VGA;
  config.jpeg_quality = 12;
  config.fb_count = 1;

  if (esp_camera_init(&config) != ESP_OK) {
    Serial.println("Camera init failed. Check board type and ribbon cable.");
    while (true) delay(1000);
  }
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(false);
  delay(300);

  pinMode(DOORBELL_BUTTON_PIN, INPUT_PULLUP);
  pinMode(CHIME_PIN, OUTPUT);
  pinMode(FLASH_LED_PIN, OUTPUT);
  digitalWrite(FLASH_LED_PIN, LOW);
  attachInterrupt(digitalPinToInterrupt(DOORBELL_BUTTON_PIN), onDoorbellPress, FALLING);

  setupCamera();

  WiFi.begin(ssid, password);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  server.on("/", handleRoot);
  server.on("/capture", handleCapture);
  server.on("/test-ring", handleTestRing);
  server.begin();

  Serial.println();
  Serial.println("Video doorbell ready");
  Serial.print("Open http://");
  Serial.println(WiFi.localIP());
}

void loop() {
  server.handleClient();

  if (buttonPressed && millis() - lastRingMs > 500) {
    buttonPressed = false;
    lastRingMs = millis();
    ringCount++;
    Serial.print("Doorbell pressed. Ring count: ");
    Serial.println(ringCount);
    playChime();
  }
}

// Run this and build other cool things at schematik.io
Libraries: esp32-camera