
What you'll build
This guide turns the tiny ESP32-CAM board into a local Wi-Fi security camera. The build targets the AI Thinker ESP32-CAM specifically: the OV2640 camera is built into the module with its signal pins fixed on the PCB, so there is no camera breakout to wire. The only external hardware is the matching USB programmer adapter and a data cable.
Once the sketch is deployed, the board joins your 2.4 GHz Wi-Fi network and prints a local address in Serial Monitor. Open that address from any browser on the same network and you get a simple page that refreshes live captures from the camera every 700 milliseconds.
The stream stays local and the code stays simple. It is a practical starting point before adding authentication, motion detection, a battery, or a more permanent mount. This project came from Sam's Schematik Short, which you can watch on YouTube.
What you are building
This guide covers a local-network camera only. The firmware has four jobs:
- initialise the OV2640 with the AI Thinker pin map,
- connect to your 2.4 GHz Wi-Fi network using
YOUR_WIFI_SSIDandYOUR_WIFI_PASSWORD, - serve a root page that auto-refreshes captures at
/captureevery 700 ms, - return JPEG frames from the camera buffer on each request to
/capture.
Push notification, authentication, cloud storage, and motion detection are out of scope.
Upload and calibrate
Open the sketch in Schematik and set the two Wi-Fi placeholders before deploying:
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
Deploy from the Schematik project page, then open Serial Monitor at 115200 baud. After the board connects you will see:
Security camera ready
Open http://192.168.x.x
Open that address in a browser on the same 2.4 GHz network. The page loads a JPEG image that refreshes automatically. If the frame rate feels slow, change FRAMESIZE_VGA to FRAMESIZE_QVGA in the camera config for a faster but smaller image.
Do not commit or share the sketch with real Wi-Fi credentials in it.
Troubleshooting
- "Camera init failed" in Serial Monitor. The board type is probably wrong. Select the AI Thinker ESP32-CAM profile, not a generic ESP32 DevKit. Also check that the camera ribbon is fully clipped into the socket.
- No serial port appears after connecting USB. The cable is likely charge-only. Swap it for a cable that transfers data — the port should appear in the OS device list immediately.
- Board does not enter upload mode. Make sure IO0 is connected to GND before pressing upload, then remove the link before resetting.
- ESP32-CAM resets or drops connection during capture. The USB supply may not be stable under camera load. Try a powered hub or a dedicated 5 V adapter, keeping the ground shared.
- Browser page loads but image is black or corrupted. Power-cycle the board. The OV2640 occasionally needs a clean reset to initialise correctly after a flash.
- "Connection refused" when opening the IP address. The board has not finished connecting to Wi-Fi. Wait a few seconds and refresh; Serial Monitor will show the IP once the server starts.
Going further
Once the basic stream is reliable, the natural next step is to add a motion-detection pass in the capture loop: compare consecutive JPEG buffers and trigger an alert when enough pixels change. Pair that with an MQTT publish or a simple HTTP webhook call and you have a basic event-driven camera without any external cloud dependency.
For a version that sets itself up without editing credentials and supports recovery without reflashing, see the ESP32-CAM Setup Portal Security Camera guide, which adds a browser-based Wi-Fi configuration page and a power-cycle reset path.
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
Seat the ESP32-CAM on the USB adapter
Align the ESP32-CAM headers with the programmer adapter and press the board down evenly. Keep the camera ribbon seated and avoid touching the lens.
- The Short uses a tiny USB adapter, so you do not need loose jumper wires for this version.
- If your adapter has boot/upload buttons, leave them accessible while flashing.
- Unplug USB before reseating the ESP32-CAM on the adapter. Offset headers can short 5V into the wrong pin.
Connect USB and choose the ESP32-CAM board
Connect the adapter to your computer with a USB data cable. In Schematik or the Arduino tooling, use the AI Thinker ESP32-CAM style pinout unless your module says otherwise.
- Open the serial monitor at 115200 baud so you can read the local IP address after upload.
- If no port appears, try another USB cable before changing code.
- The camera signal pins are built into the ESP32-CAM module. Do not add jumper wires for the camera itself.
- Some charge-only USB cables power the board but cannot upload sketches.
Add Wi-Fi details and deploy
Replace YOUR_WIFI_SSID and YOUR_WIFI_PASSWORD in the sketch, then deploy it to the ESP32-CAM. The code starts a small web server on your local network.
- Use a 2.4 GHz Wi-Fi network. Most ESP32-CAM modules cannot join 5 GHz-only networks.
- Keep the board close to the router for the first test.
- Do not commit or share a version of the sketch with real Wi-Fi credentials in it.
Open the local camera page
After the board restarts, read the IP address printed in serial output and open it in a browser on the same Wi-Fi network. The page refreshes camera captures to give you a simple live view.
- If the page loads slowly, lower the frame size from FRAMESIZE_VGA to FRAMESIZE_QVGA.
- Use this as a local prototype, not an internet-exposed security system.
- Keep the camera stream on your private LAN unless you add authentication and HTTPS.
Pin assignments
| Pin | Connection | Type |
|---|---|---|
| GPIO 0 | esp32-cam-camera-1 XCLK | DIGITAL |
| GPIO 26 | esp32-cam-camera-1 SIOD | I2C |
| GPIO 27 | esp32-cam-camera-1 SIOC | I2C |
| GPIO 5 | esp32-cam-camera-1 Y2 | DATA |
| GPIO 18 | esp32-cam-camera-1 Y3 | DATA |
| GPIO 19 | esp32-cam-camera-1 Y4 | DATA |
| GPIO 21 | esp32-cam-camera-1 Y5 | DATA |
| GPIO 36 | esp32-cam-camera-1 Y6 | DATA |
| GPIO 39 | esp32-cam-camera-1 Y7 | DATA |
| GPIO 34 | esp32-cam-camera-1 Y8 | DATA |
| GPIO 35 | esp32-cam-camera-1 Y9 | DATA |
| GPIO 25 | esp32-cam-camera-1 VSYNC | DATA |
| GPIO 23 | esp32-cam-camera-1 HREF | DATA |
| GPIO 22 | esp32-cam-camera-1 PCLK | DATA |
| GPIO 32 | esp32-cam-camera-1 PWDN | DIGITAL |
| 3V3 | esp32-cam-camera-1 3V3 | POWER |
| GND | esp32-cam-camera-1 GND | GROUND |
| 5V | esp32-cam-usb-programmer-1 5V | POWER |
| GND | esp32-cam-usb-programmer-1 GND | GROUND |
| GPIO 3 | esp32-cam-usb-programmer-1 TX | UART |
| GPIO 1 | esp32-cam-usb-programmer-1 RX | UART |
| GPIO 0 | esp32-cam-usb-programmer-1 IO0_FLASH_LINK | DIGITAL |
Code
#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
WebServer server(80);
void handleRoot() {
server.send(200, "text/html",
"<!doctype html><html><head><meta name='viewport' content='width=device-width, initial-scale=1'>"
"<title>ESP32 Security Camera</title>"
"<style>body{font-family:sans-serif;background:#111;color:#fff;text-align:center;margin:0;padding:2rem}"
"img{width:100%;max-width:640px;border-radius:16px}</style></head>"
"<body><h1>ESP32 Security Camera</h1><img src='/capture' id='cam'>"
"<script>setInterval(()=>{document.getElementById('cam').src='/capture?t='+Date.now()},700)</script>"
"</body></html>"
);
}
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 setup() {
Serial.begin(115200);
Serial.setDebugOutput(false);
delay(300);
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 the board type and camera ribbon.");
return;
}
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.begin();
Serial.println();
Serial.println("Security camera ready");
Serial.print("Open http://");
Serial.println(WiFi.localIP());
}
void loop() {
server.handleClient();
}
// Run this and build other cool things at schematik.ioReady to build this?
Open this project in Schematik to get the full wiring diagram, pin assignments, and deployable code for the ESP32 Security Camera.
Open in Schematik →