ESP32 S3 Camera - Smart Video Doorbell with a Button

A shop doorbell with a camera costs a lot of money and needs a paid account. This one costs a few euros and needs nothing but your WiFi.

A visitor presses the button. The ESP32 S3 takes a photo and sends it to your phone with Telegram, together with a link to the live video. You look at the photo at once, and you tap the link when you want to see what is happening right now. The small LED on the board answers the visitor, so they know the bell worked.

ESP32 S3 Camera - Smart Video Doorbell with a Button

What you'll build:

  1. A doorbell button that takes a photo of the visitor
  2. A Telegram message with the photo and a link to the live video
  3. A live video page that anybody with the link can open on a phone or a computer
  4. An LED that answers the visitor, so they know the bell worked
  5. A quiet time, so a child pressing the button ten times does not send ten photos

Hardware Preparation

1×ESP32 S3 N16R8 OV5640 Camera
1×Alternatively, ESP32 S3 N16R8 OV2640 Camera
1×USB Cable Type-A to Type-C (for USB-A PC)
1×USB Cable Type-C to Type-C (for USB-C PC)
1×Breadboard-mount Button with Cap
1×Breadboard-mount Button Kit
1×Panel-mount Push Button
1×Push Button Module
1×Breadboard
1×Jumper Wires
1×Recommended: Screw Terminal Expansion Board for ESP32 S3
1×Recommended: Breakout Expansion Board for ESP32 S3
1×Recommended: Power Splitter for ESP32 S3

Or you can buy the following kits:

1×DIYables Sensor Kit (18 sensors/displays)
Disclosure: Some of the links provided in this section are Amazon affiliate links. We may receive a commission for any purchases made through these links at no additional cost to you.
Additionally, some of these links are for products from our own brand, DIYables .

※ NOTE THAT:

The ESP32 S3 camera board has 40 pins, but the screw terminal block and the breakout board have 44 pins. They still work together. Put the camera board in the center of the 44-pin board, so 2 pins stay free on the left side and 2 pins stay free on the right side.

The screw terminal block lets you connect wires with a screwdriver, with no soldering. The breakout board gives you easy pin headers for a breadboard.

How It Works

The whole project is six steps:

StepWhat happens
1A visitor presses the button
2The LED goes on, so the visitor sees that the bell works
3The camera takes a photo
4The ESP32 S3 sends the photo AND the live video link to your phone with Telegram
5The LED blinks two times. The visitor knows the message is on its way.
6You tap the link when you want to watch the door live

The message on your phone looks like this:

Telegram
Telegram 12:45
Welcome to Telegram!
ArduinoBot 10:19
Chatting with Arduino...
telegram-botfather
BotFather Yesterday
Your bot has been created.

ArduinoBot

bot
Today
Somebody is at the door! Live video: http\://192.168.0.2/stream
10:15 AM

Why a Photo AND a Link?

A photo and a live video are good at different things:

Photo in the messageLive video link
Arrives by itselfYesNo, you must tap it
Works from anywhereYes, alwaysOnly at home, unless you do port forwarding
Shows one momentYesNo, it shows now
Shows what the visitor does nextNoYes
Costs battery and WiFiA few secondsWhile you watch

So the photo tells you that somebody is there, and the link lets you see what they are doing now. That is how a real video doorbell works.

※ NOTE THAT:

The live video stops by itself after 60 seconds. This is on purpose. While somebody watches the video, the board is busy and cannot see the button. The time limit makes sure the doorbell always comes back to life. To change it, change the line #define STREAM_SECONDS 60.

How the ESP32 S3 Video Doorbell Works

The LED Talks to the Visitor

A doorbell with no answer is a bad doorbell. Nobody knows whether it worked, so people press it again and again. The small colour LED that is already on the board gives an answer for every situation:

What the LED doesWhat it means
3 blinks when you switch the board onThe doorbell is ready
Stays onThe photo is being taken and sent. Please wait.
2 slow blinksThe photo was sent. Somebody will come.
5 fast blinksSomething went wrong. No WiFi, or Telegram refused.
1 long blinkYou pressed too soon after the last ring.

※ NOTE THAT:

This is the part that makes the project feel finished. The code is only a few lines, but a visitor who gets an answer will not press the button twenty times.

Why a Button Needs Help

A push button is two pieces of metal. When they touch, they bounce against each other many times in the first milliseconds. Without protection, one press looks like five presses, and your phone gets five photos.

You could write the waiting code yourself, but the ezButton library already does it. Three lines are the whole job:

ezButton button(BUTTON_PIN, EXTERNAL_PULLDOWN); // 1. make the button button.setDebounceTime(50); // 2. in setup() button.loop(); // 3. first line in loop()

Then you simply ask whether somebody pressed it:

if (button.isPressed()) { ringTheDoorbell(); }

※ NOTE THAT:

isPressed() is true only in the moment the button goes down, not while it is held. So one press does the work one time, even when a visitor keeps the button down for three seconds.

※ NOTE THAT:

button.loop() must be the first line inside your own loop(). It is the line that reads the pin and does the waiting. Forget it, and the button never works.

Overview of the Button Module

We use a button module, not a bare push button. A module is the same button, but it sits on a small board that already has the resistor on it, and it has three clean pins. You only push three wires on, and nothing can be wrong way round.

Push Button Module

The resistor on the module is a pull-down resistor. It holds the pin at GND while nobody touches the button:

ButtonPin reads
Nobody touches itLOW, the resistor holds it down
Somebody presses itHIGH, the button joins the pin to 3.3V

We tell the library about it with one word:

ezButton button(BUTTON_PIN, EXTERNAL_PULLDOWN);

※ NOTE THAT:

EXTERNAL_PULLDOWN means "the resistor is outside the chip, on the module, and it pulls the pin down". The library then knows that HIGH means pressed.

Do you have a bare push button instead of a module? Then the board must make the resistor itself. The code has both lines. Swap the // marks:

// A bare push button, with one leg on the pin and the other leg on GND. // The board uses its own resistor inside, and the pin reads LOW when pressed. ezButton button(BUTTON_PIN); // A button module, which carries its own resistor. // ezButton button(BUTTON_PIN, EXTERNAL_PULLDOWN);
What you haveLine to useWiring
Button moduleezButton button(BUTTON_PIN, EXTERNAL_PULLDOWN);VCC, GND and OUT
Bare push buttonezButton button(BUTTON_PIN);One leg to the pin, one leg to GND

※ NOTE THAT:

Never use both. With a bare button you need the resistor inside the board, and with a module you must not use it, because the module already has one.

Make Your Telegram Bot

You need a bot token and a chat ID. This takes three minutes.

Step 1: Make Your Bot

  1. Install Telegram on your phone, and make an account.
  2. In Telegram, search for @BotFather. It has a blue tick.
  3. Open the chat with BotFather and talk to it like this:
Telegram
Telegram 12:45
Welcome to Telegram!
ArduinoBot 10:19
Chatting with Arduino...
telegram-botfather
BotFather Yesterday
Your bot has been created.
BotFather

BotFather

bot
Today
/newbot
10:15 AM ✓✓
Alright, a new bot. How are we going to call it? Please choose a name for your bot.
10:16 AM
My Doorbell
10:17 AM ✓✓
Good. Now let's choose a username for your bot. It must end in bot.
10:18 AM
my_esp32_doorbell_bot
10:19 AM ✓✓
Done! Congratulations on your new bot. Use this token to access the HTTP API: 8012345678:AAHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
10:20 AM
  1. Copy the bot token from the last message and keep it safe.

Step 2: Find Your Chat ID

  1. In Telegram, search for the name of your new bot and open the chat.
  2. Send any message to your bot, for example hello. This step is necessary. Telegram does not let a bot write to you before you have written to it.
Telegram
Telegram 12:45
Welcome to Telegram!
ArduinoBot 10:19
Chatting with Arduino...
telegram-botfather
BotFather Yesterday
Your bot has been created.

ArduinoBot

bot
Today
hello
10:15 AM ✓✓
  1. Open this address in your web browser. Put your own bot token in the place of <YOUR_BOT_TOKEN>:
https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
  1. The browser shows a long text. Look for "chat":{"id":. The number after it is your chat ID.

※ NOTE THAT:

Does the browser show {"ok":true,"result":[]} with nothing inside? Then you did not send a message to your bot yet. Go back to step 2, send hello to your bot, then load the address again.

※ NOTE THAT:

Never show your bot token to anybody, and never put it on a website or on GitHub. If it leaks, send /revoke to BotFather and you get a new one.

Wiring Diagram between Button and ESP32 S3

The wiring diagram between ESP32 S3 Video Doorbell

This image is created using Fritzing. Click to enlarge image

Button Module PinESP32 S3 Pin
VCC (or +)3.3V
GND (or -)GND
OUT (or S, or SIG)GPIO47

That is the whole wiring. Three wires, and no resistor.

※ NOTE THAT:

You do not need an LED. The board already has a small colour LED on it, and the code uses that one:

#define LED_PIN LED_BUILTIN

※ NOTE THAT:

Has your board no LED on it? Then connect your own LED with a 220 ohm resistor to a free pin, for example GPIO21, and change the line to #define LED_PIN 21.

Safety Notes

Use 3.3V for the button module, not 5V. The ESP32 S3 pins are made for 3.3V, and 5V can damage them.

The button module needs no extra resistor, because the resistor is already on the module.

The camera uses GPIO4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17 and 18. Do not use these pins. GPIO47 is free, so we use it for the button.

※ NOTE THAT:

A real doorbell button has four legs, but only two of them are needed. If your button does nothing, turn it a quarter round on the breadboard and try again. Two legs that are already joined inside will never work.

Install the Libraries

This project needs two libraries.

DIYables ESP32 WebServer makes the live video page:

  1. Open the Arduino IDE.
  2. Click the Libraries icon on the left bar.
  3. Type DIYables ESP32 WebServer into the search box.
  4. Find the library by DIYables.io and click INSTALL.
  • Search for DIYables ESP32 WebServer created by DIYables.io and click the Install button.
∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Library Manager
Type:
All
Topic:
All
DIYables ESP32 WebServer by DIYables.io
DIYables ESP32 WebServer library designed for ESP32 boards. It includes multi-page web server capabilities and WebSocket support for real-time communication, perfect for IoT projects and DIYables ESP32 boards. More info
1.0.1
INSTALL
Newbiely.ino
···
1 void setup() {
Output
Serial Monitor
Ln 1, Col 1
ESP32S3 Dev Module on COM15
1

ezButton reads the button and takes care of the bouncing:

  1. Type ezButton into the same search box.
  2. Find the library by ArduinoGetStarted.com and click INSTALL.
  • Search for ezButton created by ArduinoGetStarted.com and click the Install button.
∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Library Manager
Type:
All
Topic:
All
ezButton by ArduinoGetStarted.com
Button library supports debounce, pressed/released events and the press counting. It is easy to use with multiple buttons. The library can be used for push-button, momentary switches, toggle switch, magnetic contact switch (door sensor)... It is designed for not only beginners but also experienced users. More info
1.0.6
INSTALL
Newbiely.ino
···
1 void setup() {
Output
Serial Monitor
Ln 1, Col 1
ESP32S3 Dev Module on COM15
1

Arduino IDE Settings for ESP32 S3 Camera

The camera does not work with the default settings of the Arduino IDE. You must change some items in the Tools menu before you upload the code.

Tools Menu ItemSelect This
BoardESP32S3 Dev Module
Flash Size16MB (128Mb)
PSRAMOPI PSRAM
Partition SchemeHuge APP (3MB No OTA/1MB SPIFFS)
CPU Frequency240MHz (WiFi)
USB ModeHardware CDC and JTAG
Upload ModeUART0 / Hardware CDC
Upload Speed921600
USB CDC On BootEnabled

ESP32 S3 Code

Change your WiFi, your bot token and your chat ID at the top of the code.

The settings you can change:

NameStart withWhat it does
PUBLIC_ADDRESSemptyThe link in the message. Empty means the address of the board.
USE_PASSWORDfalseSet it to true when you open the camera to the internet
QUIET_TIME10000Milliseconds to wait before the next ring
STREAM_SECONDS60The live video stops by itself after this many seconds
DEBOUNCE_TIME50Milliseconds to wait, because the button bounces
/* * This ESP32 S3 code was developed by newbiely.com * * This ESP32 S3 code is made available for public use without any restriction * * For comprehensive instructions and wiring diagrams, please visit: * https://newbiely.com/tutorials/esp32-s3/esp32-s3-camera-smart-video-doorbell-with-a-button */ #include <WiFi.h> #include <WiFiClientSecure.h> #include <DIYables_ESP32_WebServer.h> // DIYables ESP32 WebServer library #include <ezButton.h> // ezButton library #include "esp_camera.h" const char *WIFI_SSID = "YOUR_WIFI_SSID"; // CHANGE IT const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; // CHANGE IT const char *BOT_TOKEN = "1234567890:ABCDefGhIJKlmNoPQRsTUVwXYZ1234567890"; // CHANGE IT const char *CHAT_ID = "1234567890"; // CHANGE IT // The address of the live video that goes into the Telegram message. // // Leave it EMPTY to use the address of the board on your own WiFi. The link then // works only when your phone is at home, on the same WiFi. // // Did you make port forwarding on your router? Then write your own address here, // for example "http://my-house.duckdns.org:8080", and the link works from anywhere. const char *PUBLIC_ADDRESS = ""; // CHANGE IT, or leave it empty // A camera that anybody on the internet can watch is a bad idea. // Set this to true when you use PUBLIC_ADDRESS, and the page asks for a password. #define USE_PASSWORD false const char *WEB_USER = "admin"; // CHANGE IT const char *WEB_PASSWORD = "esp32s3"; // CHANGE IT #define BUTTON_PIN 47 // the OUT pin of the button module is connected to GPIO47 // The board already has a small colour LED on it, so you do not need // an extra LED and resistor. This LED answers the visitor. #define LED_PIN LED_BUILTIN // Has your board no LED on it? Then connect your own LED with a 220 ohm // resistor to a free pin, and use this line instead: // #define LED_PIN 21 #define DEBOUNCE_TIME 50 // wait 50 milliseconds, because the button bounces #define QUIET_TIME 10000 // wait 10 seconds before the next ring (10000 milliseconds) #define STREAM_SECONDS 60 // the live video stops by itself after 60 seconds // Camera pins of the ESP32-S3-N16R8 camera board. // The same pins work for the OV2640, OV3660 and OV5640 versions. #define PWDN_GPIO_NUM -1 #define RESET_GPIO_NUM -1 #define XCLK_GPIO_NUM 15 #define SIOD_GPIO_NUM 4 #define SIOC_GPIO_NUM 5 #define Y9_GPIO_NUM 16 #define Y8_GPIO_NUM 17 #define Y7_GPIO_NUM 18 #define Y6_GPIO_NUM 12 #define Y5_GPIO_NUM 10 #define Y4_GPIO_NUM 8 #define Y3_GPIO_NUM 9 #define Y2_GPIO_NUM 11 #define VSYNC_GPIO_NUM 6 #define HREF_GPIO_NUM 7 #define PCLK_GPIO_NUM 13 #define TELEGRAM_HOST "api.telegram.org" #define BOUNDARY "diyablesphoto" #define STREAM_MARK "diyablesframe" DIYables_ESP32_WebServer server(80); // The line below is for a BARE push button wired to GND. ezButton then // uses the internal pull-up resistor of the board, and the pin reads LOW // while the button is held down. // ezButton button(BUTTON_PIN); // If you use a BUTTON MODULE instead, the module carries its own resistor // on the board, so the internal one must not be used. Comment the line // above and use this line instead: ezButton button(BUTTON_PIN, EXTERNAL_PULLDOWN); unsigned long lastRingTime = 0; const char INDEX_HTML[] = R"rawliteral( <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ESP32 S3 Doorbell</title> <style> body { background:#111; color:#eee; font-family:sans-serif; text-align:center; margin:0; padding:16px; } img { width:100%; max-width:800px; border-radius:8px; } p { color:#aaa; font-size:14px; } </style> </head> <body> <h2>Who is at the door?</h2> <img id="live" src="/stream"> <p>The live video stops by itself after a minute, so the doorbell button works again.</p> <script> // When the video stops, load the page again and start a new one document.getElementById("live").onerror = function () { setTimeout(function () { location.reload(); }, 1000); }; </script> </body> </html> )rawliteral"; // Returns the name of the camera sensor that is installed on the board const char *getSensorName(int pid) { switch (pid) { case OV2640_PID: return "OV2640 (2 MP, max UXGA 1600x1200)"; case OV3660_PID: return "OV3660 (3 MP, max QXGA 2048x1536)"; case OV5640_PID: return "OV5640 (5 MP, max QSXGA 2592x1944)"; default: return "Unknown sensor"; } } bool 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_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_SVGA; // 800x600 - small, so it is sent quickly config.jpeg_quality = 12; // 0 to 63, a lower number means a better image config.fb_count = 1; config.fb_location = CAMERA_FB_IN_DRAM; config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; if (psramFound()) { config.fb_location = CAMERA_FB_IN_PSRAM; config.jpeg_quality = 10; config.fb_count = 2; config.grab_mode = CAMERA_GRAB_LATEST; } else { config.frame_size = FRAMESIZE_QVGA; Serial.println("WARNING: PSRAM not found. Please set PSRAM to OPI PSRAM in the Tools menu."); } esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x\n", err); return false; } sensor_t *s = esp_camera_sensor_get(); Serial.printf("Camera sensor: %s\n", getSensorName(s->id.PID)); // Each sensor needs slightly different settings switch (s->id.PID) { case OV2640_PID: s->set_vflip(s, 0); s->set_hmirror(s, 0); break; case OV3660_PID: s->set_vflip(s, 1); // the OV3660 image is upside down by default s->set_brightness(s, 1); // make the image a little brighter s->set_saturation(s, -2); // make the colors a little softer break; case OV5640_PID: s->set_vflip(s, 0); s->set_hmirror(s, 0); break; default: break; } return true; } // Builds the link to the live video that goes into the Telegram message String getLiveLink() { if (strlen(PUBLIC_ADDRESS) > 0) return String(PUBLIC_ADDRESS) + "/stream"; return "http://" + WiFi.localIP().toString() + "/stream"; } // Blinks the LED, so the visitor knows what is happening void blinkLed(int times, int speed) { for (int i = 0; i < times; i++) { digitalWrite(LED_PIN, HIGH); delay(speed); digitalWrite(LED_PIN, LOW); delay(speed); } } // Sends one photo and the live link to your Telegram chat bool sendPhotoToTelegram(camera_fb_t *fb) { WiFiClientSecure client; client.setInsecure(); // do not check the certificate of the Telegram server client.setTimeout(15); Serial.println("Connecting to Telegram..."); if (!client.connect(TELEGRAM_HOST, 443)) { Serial.println("Cannot connect to Telegram"); return false; } // The text under the photo, with the address of the live video String caption = "Somebody is at the door!\n"; caption += "Live video: " + getLiveLink(); // The first part of the message: who receives the photo, and the text String head = "--" BOUNDARY "\r\n"; head += "Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n"; head += String(CHAT_ID) + "\r\n"; head += "--" BOUNDARY "\r\n"; head += "Content-Disposition: form-data; name=\"caption\"\r\n\r\n"; head += caption + "\r\n"; head += "--" BOUNDARY "\r\n"; head += "Content-Disposition: form-data; name=\"photo\"; filename=\"doorbell.jpg\"\r\n"; head += "Content-Type: image/jpeg\r\n\r\n"; // The last part of the message String tail = "\r\n--" BOUNDARY "--\r\n"; size_t totalLength = head.length() + fb->len + tail.length(); // Send the HTTP header client.print("POST /bot" + String(BOT_TOKEN) + "/sendPhoto HTTP/1.1\r\n"); client.print("Host: " TELEGRAM_HOST "\r\n"); client.print("Content-Type: multipart/form-data; boundary=" BOUNDARY "\r\n"); client.print("Content-Length: " + String(totalLength) + "\r\n"); client.print("Connection: close\r\n\r\n"); // Send the message client.print(head); // Send the photo in small pieces of 1024 bytes. // We check every piece. If one fails, Telegram would wait for ever. size_t sent = 0; for (size_t i = 0; i < fb->len; i += 1024) { size_t piece = fb->len - i; if (piece > 1024) piece = 1024; size_t written = client.write(fb->buf + i, piece); if (written != piece) { Serial.printf("Sending stopped after %u of %u bytes\n", (unsigned)(sent + written), (unsigned)fb->len); client.stop(); return false; } sent += written; } client.print(tail); // Read the answer of Telegram. // Do NOT stop when the connection closes. Telegram answers and closes at once, // and the answer is often still waiting in the buffer at that moment. String answer = ""; unsigned long startTime = millis(); while (millis() - startTime < 15000) { while (client.available()) { answer += (char)client.read(); startTime = millis(); } if (answer.length() > 0 && !client.connected()) break; // the whole answer arrived and the server hung up delay(10); } client.stop(); if (answer.length() == 0) { Serial.println("Telegram did not answer at all."); Serial.println("Check your internet, and check that your network does not block Telegram."); return false; } if (answer.indexOf("\"ok\":true") > 0) { Serial.println("The photo and the live link were sent to Telegram"); return true; } Serial.println("Telegram did not accept the photo. The answer was:"); Serial.println(answer); return false; } // The whole doorbell: light on, photo, send, answer with the LED void ringTheDoorbell() { Serial.println("The doorbell was pressed!"); digitalWrite(LED_PIN, HIGH); // the light stays on while we work camera_fb_t *fb = esp_camera_fb_get(); if (!fb) { Serial.println("Photo capture failed"); digitalWrite(LED_PIN, LOW); blinkLed(5, 100); // five fast blinks mean "something went wrong" return; } Serial.printf("Photo taken: %u bytes\n", fb->len); bool sent = sendPhotoToTelegram(fb); esp_camera_fb_return(fb); // give the memory back to the camera digitalWrite(LED_PIN, LOW); if (sent) blinkLed(2, 200); // two slow blinks mean "the message is on its way" else blinkLed(5, 100); // five fast blinks mean "something went wrong" } // Shows the web page with the live video void handleHome(WiFiClient &client, const String &method, const String &request, const QueryParams &params, const String &jsonData) { server.sendResponse(client, INDEX_HTML, "text/html"); } // Sends the live video to the phone or the computer that opened the link void handleStream(WiFiClient &client, const String &method, const String &request, const QueryParams &params, const String &jsonData) { Serial.println("Somebody is watching the live video"); client.print("HTTP/1.1 200 OK\r\n"); client.print("Content-Type: multipart/x-mixed-replace; boundary=" STREAM_MARK "\r\n"); client.print("Access-Control-Allow-Origin: *\r\n"); client.print("Connection: close\r\n\r\n"); unsigned long startTime = millis(); while (client.connected()) { // Stop by ourselves after a while, so the doorbell button works again if (millis() - startTime >= (unsigned long)STREAM_SECONDS * 1000) { Serial.println("The live video stopped after the time limit"); break; } camera_fb_t *fb = esp_camera_fb_get(); if (!fb) { Serial.println("Frame capture failed"); break; } size_t len = fb->len; client.print("--" STREAM_MARK "\r\n"); client.print("Content-Type: image/jpeg\r\n"); client.printf("Content-Length: %u\r\n\r\n", len); size_t sent = client.write(fb->buf, len); client.print("\r\n"); esp_camera_fb_return(fb); if (sent != len) // the phone closed the page break; } Serial.println("The live video was closed"); } void setup() { Serial.begin(115200); delay(1000); // The library watches the button and handles the bouncing for us button.setDebounceTime(DEBOUNCE_TIME); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); if (!initCamera()) { Serial.println("Stopped"); blinkLed(10, 100); while (true) delay(1000); } WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); WiFi.setSleep(false); // keep WiFi fast for the live video if (USE_PASSWORD) server.enableAuthentication(WEB_USER, WEB_PASSWORD); server.addRoute("/", handleHome); server.addRoute("/stream", handleStream); server.begin(); Serial.print("Live video on your own WiFi: http://"); Serial.print(WiFi.localIP()); Serial.println("/stream"); Serial.println("The link inside the Telegram message is: " + getLiveLink()); blinkLed(3, 150); // three blinks mean "the doorbell is ready" Serial.println("Ready. Waiting for somebody to press the button..."); } void loop() { server.handleClient(); // answers the phone that wants to watch the live video button.loop(); // the library must do its work first, in every loop // isPressed() is true only in the moment the button goes down if (button.isPressed()) { if (millis() - lastRingTime >= QUIET_TIME) { ringTheDoorbell(); lastRingTime = millis(); } else { Serial.println("Too soon after the last ring. Nothing is sent."); blinkLed(1, 400); } } }

Detailed Instructions

  • New to ESP32 S3? Complete our Getting Started with ESP32 S3 guide first.
  • Make your Telegram bot and find your chat ID. See the steps above.
  • Install the DIYables ESP32 WebServer and ezButton libraries.
  • Unplug the USB cable, then wire the button module as in the table above.
  • Copy the above code and paste it into the Arduino IDE.
  • Change WIFI_SSID, WIFI_PASSWORD, BOT_TOKEN and CHAT_ID. Leave PUBLIC_ADDRESS empty for now.
  • Change the settings in the Tools menu. See the table above.
  • Compile and upload the code to the ESP32 S3 board by clicking the Upload button in Arduino IDE.
Arduino IDE Upload Code
  • Open the Serial Monitor in Arduino IDE.
How to open serial monitor on Arduino IDE
  • Press the RESET button one time. The LED blinks three times, so you know the doorbell is ready.
  • Point the camera at your face and press the doorbell button.
  • The LED stays on for a moment, then blinks two times.
  • Look at your phone. The photo is in your Telegram chat, with the link under it.
Telegram
Telegram 12:45
Welcome to Telegram!
ArduinoBot 10:19
Chatting with Arduino...
telegram-botfather
BotFather Yesterday
Your bot has been created.

ArduinoBot

bot
Today
Somebody is at the door! Live video: http\://192.168.0.2/stream
10:15 AM
  • Tap the link. Your phone opens the live video of your door.
  • Watch for a moment, then close the page. The doorbell is ready again.
  • Pro Tip: Do you want the link to work when you are not at home? Read The Live Video Link above, and do the port forwarding.
ESP32 S3 Doorbell Photo in Telegram
ESP32 S3 Doorbell Live Video

Serial Monitor

The following readings were captured in 2026:

∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'ESP32S3 Dev Module' on 'COM15')
New Line
9600 baud
[2026-09-28 17:00:01] Camera sensor: OV5640 (5 MP, max QSXGA 2592x1944) [2026-09-28 17:00:01] Connecting to WiFi... [2026-09-28 17:00:04] Live video on your own WiFi: http://192.168.0.2/stream [2026-09-28 17:00:04] The link inside the Telegram message is: http://192.168.0.2/stream [2026-09-28 17:00:05] Ready. Waiting for somebody to press the button... [2026-09-28 17:02:18] The doorbell was pressed! [2026-09-28 17:02:18] Photo taken: 51353 bytes [2026-09-28 17:02:18] Connecting to Telegram... [2026-09-28 17:02:21] The photo and the live link were sent to Telegram [2026-09-28 17:02:25] Too soon after the last ring. Nothing is sent. [2026-09-28 17:02:33] Somebody is watching the live video [2026-09-28 17:02:51] The live video was closed
Ln 11, Col 1
ESP32S3 Dev Module on COM15
2

※ NOTE THAT:

Too soon after the last ring is the quiet time doing its job. Somebody pressed the button again after 4 seconds, and the code did not send a second photo.

※ NOTE THAT:

Somebody is watching the live video appears when you tap the link. If you never see this line, the link did not reach the board. Read the troubleshooting below.

The two lines at the start tell you what went into the message. When you set PUBLIC_ADDRESS, the second line becomes your own address:

∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'ESP32S3 Dev Module' on 'COM15')
New Line
9600 baud
[2026-09-28 17:00:04] Live video on your own WiFi: http://192.168.0.2/stream [2026-09-28 17:00:04] The link inside the Telegram message is: http://my-house.duckdns.org:8080/stream
Ln 11, Col 1
ESP32S3 Dev Module on COM15
2

Troubleshooting

Nothing happens when I press the button.

  • Check the three wires. VCC to 3.3V, GND to GND, OUT to GPIO47.
  • Is button.loop() the first line inside loop()? Without it the library never reads the pin.
  • Does your module have a pull-up resistor instead of a pull-down? Then change the code to ezButton button(BUTTON_PIN, EXTERNAL_PULLUP);
  • Watch the Serial Monitor while you press. No line there means the wiring, not the code.

The doorbell rings by itself, with nobody there.

The OUT wire is loose, so the pin picks up noise. Push the wire in properly. A module with its own resistor should never do this when it is wired correctly.

One press sends two or three photos.

Increase DEBOUNCE_TIME from 50 to 150.

The LED blinks five times after every press.

Five fast blinks mean the photo could not be sent. Read the Serial Monitor:

  • Telegram did not answer at all means the photo went out but nothing came back. Check your internet, and try a phone hotspot, because some networks block Telegram.
  • Sending stopped after ... bytes means the WiFi broke in the middle. Move the board closer to the router.
  • Cannot connect to Telegram means no internet at all.
  • Unauthorized means your bot token is wrong.
  • chat not found means your chat ID is wrong.

The Serial Monitor shows Forbidden: bot can't initiate conversation with a user.

You never wrote to your bot. Open Telegram, find your bot, and send it any message. Then press RESET on the board.

The LED never lights up.

The code uses the LED that is on the board. Not every board has one. If yours has none, connect your own LED with a 220 ohm resistor to GPIO21, and change the line to #define LED_PIN 21.

The link in the message does not open anything.

  • Leave PUBLIC_ADDRESS empty and try again at home, on the same WiFi as the board. This must work first.
  • Your phone must be on the WiFi, not on mobile data. A private address like 192.168.0.2 does not exist outside your house.
  • Type http://, not https://. The board does not use https.

The link works at home but not outside.

This is normal with an empty PUBLIC_ADDRESS. To make it work from anywhere, you need port forwarding and a DDNS name. Read The Live Video Link above.

I did the port forwarding, and it still does not work.

  • Give the board a fixed address in your router, or the forwarding points at the wrong device after a power cut.
  • Some internet companies give you an address that is shared with other houses (CGNAT). Then port forwarding cannot work at all. Ask your company for a real public address.
  • Test from mobile data, not from your home WiFi. Many routers cannot open their own public address from inside.

The live video stops after one minute.

That is on purpose. While somebody watches, the board cannot see the button. Reload the page to watch again, or change #define STREAM_SECONDS 60.

The doorbell button does nothing while somebody watches the video.

The board can do one thing at a time. Close the video page, and the button works again. This is also why the video stops by itself after a minute.

The photo shows the visitor from below, or their head is cut off.

Move the camera. A doorbell camera should sit about 150 cm above the ground, so it looks at a face and not at a chest.

The photo is dark in the evening.

The camera cannot see in the dark. See the ESP32 S3 Camera - Add a Flash LED for Night Photos tutorial. A light at a door is also friendly for the visitor.

The photo takes a long time.

Use a smaller image size. FRAMESIZE_VGA (640 × 480) is about half of SVGA.

The Serial Monitor shows nothing after the boot messages.

If your USB cable is in the port named UART, set USB CDC On Boot to Disabled. If it is in the port named USB, set it to Enabled.

Applications

A button, a camera and a message. That small idea fits in many places. Here are practical projects you can build with this code:

  1. Video doorbell for your house: See the visitor on your phone, from anywhere.
  2. Doorbell for a deaf person: The photo and the phone alert replace the sound.
  3. Office reception bell: A visitor rings, and the right person sees who it is.
  4. Tutorial or garage bell: Know that somebody is at the back door while a machine is loud.
  5. Help button for an older person: One press sends a photo of the room to a family member.
  6. Hotel or guest room bell: The owner sees the guest without walking to the door.
  7. Warehouse delivery bell: The driver rings, and the office sees the lorry.
  8. Shop service button: A customer asks for help, and a photo shows where they stand.
  9. School or club entrance: Keep a photo record of every person who rang.
  10. Photo booth button: Press to take your own picture and get it on your phone.

Video Tutorial

Watch the step-by-step video walkthrough for this ESP32 S3 project below.

Challenges

  1. Beginner: Change the text Somebody is at the door! to your own message, for example the name of your house.
  2. Intermediate: Add a buzzer that makes a sound inside the house when the button is pressed, so you hear the bell as well as see it.
  3. Advanced: Also save every photo to the microSD card, so you keep a record of all your visitors even when the internet is down. See the ESP32 S3 Camera - Save Photo to SD Card When Door Opens tutorial.

※ OUR MESSAGES

  • As freelancers, We are AVAILABLE for HIRE. See how to outsource your project to us
  • Please feel free to share the link of this tutorial. However, Please do not use our content on any other websites. We invested a lot of effort and time to create the content, please respect our work!