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, 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, one passive piezo buzzer, and a small faceplate or enclosure. The camera pins are the ESP32-CAM's onboard wiring, so the only external wiring is the button on GPIO13 and the chime on GPIO14. The sketch uses the internal pull-up for the button and the onboard GPIO4 flash LED for feedback.

When the sketch starts, the ESP32-CAM prints its local IP address in Serial Monitor. Open that address from a browser on the same Wi-Fi network and you get the live view, ring count, and a test-chime button. 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.

Wiring diagram

Wiring diagram

Interactive wiring diagram

Components needed

ComponentTypeQtyBuy
ESP32-CAM Wi-Fi camera boardsensor1
Momentary doorbell buttoninput1€8.30
Passive piezo buzzeractuator1€4.75
ESP32-CAM USB programmer adapterother1
Doorbell enclosure or faceplateother1

Prices and availability are indicative and may have been updated by the supplier. 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