Every "smart" doorbell on the market wants the same thing from you: your video feed, uploaded to their servers, wrapped in an $8-a-month subscription just to keep more than a few hours of history. I got tired of that trade after my package went missing from the porch and the "free tier" of my old doorbell had already overwritten the clip. So I built my own — an ESP32-CAM that runs a real AI model on the chip itself, detects people and packages in real time, and sends me a Telegram alert the moment something shows up. No cloud AI, no subscription, no footage leaving my house unless I want it to.
This guide walks through the whole build: training a tiny object-detection model with Edge Impulse, flashing it to an ESP32-CAM, wiring the alert hardware, and getting notifications on your phone.
📑 Table of Contents
-
- Why On-Device AI (TinyML) Instead of Cloud AI
-
- Components & Cost Breakdown
-
- Circuit Wiring Diagram
-
- Training the AI Model with Edge Impulse
-
- Deploying the Model to the ESP32-CAM
-
- Complete Code
-
- Setting Up Telegram Alerts
-
- Calibration Tips
-
- Troubleshooting
-
- Taking It Further
1. Why On-Device AI (TinyML) Instead of Cloud AI
Most "smart" cameras ship your video to a server, run detection there, and send a result back. That works, but it means a monthly bill, a privacy trade-off, and a camera that stops being smart the moment your internet drops.
TinyML flips that: the neural network runs directly on the microcontroller, inches from the sensor. Edge Impulse has made this genuinely approachable for hobbyists over the last couple of years — you don't need a data science background, just a phone camera to collect training images and a browser to train the model.
⚡ Why this matters in 2026: ESP32-S3 and even the classic ESP32-CAM now have enough RAM and processing headroom to run lightweight object-detection models (like FOMO — Faster Objects, More Objects) at several frames per second. Two years ago this was research-lab territory. Now it's a weekend build.
2. Components & Cost Breakdown
| Component | Model | Purpose | Price |
|---|---|---|---|
| Camera Module | ESP32-CAM (AI-Thinker, OV2640) | Capture + run inference | $8 |
| Programmer | FTDI FT232RL USB-to-Serial | Flashing the ESP32-CAM | $3 |
| Alert | Active buzzer 5V | Audible alert on detection | $1 |
| Status LED | WS2812B addressable LED | Visual status indicator | $1 |
| Power | 5V 2A USB power adapter | Stable power for camera + WiFi | $4 |
| Misc | Jumper wires, mounting bracket | Assembly | $2 |
💰 Total Cost: ~$19 (vs. $130-250/year for commercial smart doorbells with subscriptions)
3. Circuit Wiring Diagram
The ESP32-CAM doesn't expose a lot of free GPIO pins because most are already committed to the camera interface, so wiring stays intentionally minimal.
FTDI Programmer (temporary, for flashing only):
- FTDI TX → ESP32-CAM U0R (RX)
- FTDI RX → ESP32-CAM U0T (TX)
- FTDI GND → ESP32-CAM GND
- FTDI 5V → ESP32-CAM 5V
- GPIO0 → GND (only while flashing — remove this jumper for normal operation)
Buzzer & LED (permanent):
- Buzzer + → GPIO12
- Buzzer − → GND
- WS2812B DIN → GPIO13
- WS2812B VCC → 5V
- WS2812B GND → GND
⚠️ Important: The ESP32-CAM's onboard voltage regulator is a common weak point — it struggles to supply both the camera and WiFi radio reliably from a USB port's 500mA limit. Power it from a proper 5V/2A wall adapter, not directly from a laptop USB port, or you'll get random brownout resets during WiFi transmission.
4. Training the AI Model with Edge Impulse
This is the part that used to require a GPU cluster and now takes an afternoon.
-
Create a free Edge Impulse account at edgeimpulse.com and start a new project.
-
Collect training data. Use your phone (or the ESP32-CAM itself once it's flashed with a basic camera-streaming sketch) to capture 100-150 images each of: a person at your door, a package on the porch, and an "empty porch" baseline. Vary lighting, angle, and distance — the model generalizes far better with messy, realistic data than with perfectly staged photos.
-
Label your images. Edge Impulse's built-in labeling tool lets you draw bounding boxes around "person" and "package" in each image directly in the browser.
-
Choose FOMO as your learning block. FOMO (Faster Objects, More Objects) is a detection architecture specifically designed by Edge Impulse for microcontrollers — it trades some accuracy for a massive drop in RAM and inference time compared to standard object detection models like YOLO, which is exactly the trade-off an ESP32 needs.
-
Train the model. Start with the default settings (96x96 grayscale input keeps things fast on this hardware) and let it run. Check the confusion matrix afterward — if "package" and "person" are getting confused with each other, add more varied training images for whichever class is weaker.
-
Test in the browser using the "Live Classification" tab before deploying anything to hardware. This saves you from flashing a model that isn't actually ready yet.
5. Deploying the Model to the ESP32-CAM
Once you're happy with the model's accuracy:
- Go to the Deployment tab in Edge Impulse.
- Select Arduino library as the deployment target.
- Click Build — this packages your trained model into a
.zipfile formatted as an Arduino library. - In the Arduino IDE: Sketch → Include Library → Add .ZIP Library, and select the file you just downloaded.
- Install the board support for ESP32 if you haven't already (Boards Manager → search "esp32" → install the espressif package).
6. Complete Code
#include <YOUR_PROJECT_NAME_inferencing.h>
#include "esp_camera.h"
#include <WiFi.h>
#include <FastLED.h>
// Camera pins for 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
#define BUZZER_PIN 12
#define LED_PIN 13
#define NUM_LEDS 1
CRGB leds[NUM_LEDS];
const char* ssid = "YOUR_WIFI";
const char* password = "YOUR_PASSWORD";
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_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_RGB565;
config.frame_size = FRAMESIZE_96X96;
config.fb_count = 1;
esp_camera_init(&config);
}
void alertDetection(const char* label) {
Serial.print("Detected: ");
Serial.println(label);
// Flash LED red and buzz
leds[0] = CRGB::Red;
FastLED.show();
digitalWrite(BUZZER_PIN, HIGH);
delay(300);
digitalWrite(BUZZER_PIN, LOW);
leds[0] = CRGB::Black;
FastLED.show();
}
void setup() {
Serial.begin(115200);
pinMode(BUZZER_PIN, OUTPUT);
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
setupCamera();
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected!");
}
void loop() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
return;
}
ei::signal_t signal;
signal.total_length = fb->len;
// (Conversion of fb->buf into the format expected by
// run_classifier() happens here — see the Edge Impulse
// "esp32 camera" example sketch bundled with your exported
// library for the full image-to-signal helper function.)
ei_impulse_result_t result = { 0 };
EI_IMPULSE_ERROR err = run_classifier(&signal, &result, false);
if (err == EI_IMPULSE_OK) {
for (size_t i = 0; i < result.bounding_boxes_count; i++) {
ei_impulse_result_bounding_box_t bb = result.bounding_boxes[i];
if (bb.value > 0.75) { // confidence threshold
alertDetection(bb.label);
}
}
}
esp_camera_fb_return(fb);
delay(2000); // check every 2 seconds
}
Note: The image-to-signal conversion step is intentionally left as a comment above — Edge Impulse's exported library includes a ready-made esp32_camera example sketch with this helper already written for you. Start from that example and drop in the alert/buzzer/LED logic shown here rather than writing the conversion function from scratch; it saves a lot of debugging.
7. Setting Up Telegram Alerts
A buzzer is great when you're home, useless when you're not. Add a phone notification with the UniversalTelegramBot library:
- Message @BotFather on Telegram and create a new bot to get an API token.
- Message @userinfobot to get your personal chat ID.
- Install the
UniversalTelegramBotandArduinoJsonlibraries via the Library Manager. - Inside
alertDetection(), add abot.sendMessage(CHAT_ID, message, "")call using your token and chat ID.
Now instead of (or alongside) the buzzer, you get a push notification with the detection label straight to your phone, anywhere you have signal.
8. Calibration Tips
- Confidence threshold: Start at 0.75 and adjust based on false positives. A shadow triggering a "person" alert at 3am gets old fast — raise the threshold if that happens.
- Lighting: FOMO models trained mostly on daytime images will struggle at night. If your porch has night-time traffic, collect and label some low-light training images too, or add an IR illuminator.
- Mounting angle: Keep the camera at a consistent height and angle to what you trained on — a model trained on chest-height shots performs worse on a camera mounted looking sharply downward.
9. Troubleshooting
Camera fails to initialize:
- Double-check the PSRAM is enabled in Arduino IDE (Tools → PSRAM → Enabled) — the AI-Thinker board requires it for camera buffering.
- Reseat the ribbon cable connecting the OV2640 sensor to the board; it's a common loose-connection point.
Board won't upload / gets stuck at "Connecting...":
- Confirm GPIO0 is jumpered to GND before pressing the reset button, and remove that jumper after flashing.
- Some FTDI adapters default to 5V logic — make sure yours is set to 3.3V, or you risk damaging the board.
Random resets during WiFi activity:
- This is almost always power-related. Swap to a proper 5V/2A supply and add a 470µF capacitor across the 5V and GND pins close to the board.
Model runs but never detects anything:
- Verify your confidence threshold isn't set unreasonably high.
- Re-check that the image resolution and color format in your sketch (96x96, RGB565) match what you selected when training in Edge Impulse — a mismatch here silently produces garbage predictions.
10. Taking It Further
Once the base build works reliably, a few upgrades are worth the extra weekend: adding a second detection class for specific delivery van logos, triggering a smart plug to turn on a porch light automatically when "person" is detected after dark, or logging detections with timestamps to an SD card for a simple local history — no cloud storage required.
Final Thoughts
This project is a good reminder that "AI" doesn't have to mean "someone else's server." Running the model directly on an $8 camera module, with zero recurring cost and zero footage leaving your house, is genuinely more private and more reliable than most commercial alternatives — and once you've trained one FOMO model, retraining it for a completely different detection task (a pet, a specific tool on a workbench, a parking spot) is just a matter of swapping the training images.
If you build this, share your confusion matrix and detection results — I'd love to see what accuracy other people are getting with different porch lighting setups.
Related Projects You'll Love

