How to Build an ESP32-CAM Setup Portal Security Camera
A tiny camera that sets up its own Wi-Fi portal and can recover without rewriting code
Updated

What you'll build
This guide turns an ESP32-CAM into a small security camera that behaves more like a finished gadget than a loose development board. On first boot it creates its own setup Wi-Fi network, serves a browser page for your home Wi-Fi details, then joins that network so you can open the camera locally. If you need to move it to a new network, the sketch includes a recovery path that returns it to setup mode without editing the firmware.
The build uses the AI Thinker ESP32-CAM, its onboard OV2640 camera, and the built-in flash LED. There is very little external wiring once the board is programmed, but the guide is careful about the annoying ESP32-CAM bits: stable 5V power, upload mode, serial monitor, and why the flash LED should stay off on boot.
The result is a useful workshop or desk camera you can flash from Schematik, configure from your phone, and keep improving later with motion alerts, a printed enclosure, or a better mounting bracket.
Wiring diagram
Wiring diagram
Components needed
| Component | Type | Qty | Buy |
|---|---|---|---|
| ESP32-CAM board with OV2640 camera | board | 1 | |
| USB serial programmer | other | 1 | |
| 5V power supply | other | 1 | |
| Small camera enclosure or mount | other | 1 |
Assembly
Prepare the ESP32-CAM
Fit the OV2640 camera ribbon firmly into the ESP32-CAM socket and mount the board so the antenna is not boxed in metal.
Connect the USB serial programmer
Wire programmer 5V to ESP32-CAM 5V, GND to GND, U0R to programmer TX, and U0T to programmer RX. Link IO0 to GND only while flashing.
Flash and open Serial Monitor
Upload the sketch, remove the IO0 to GND link, press reset, and watch Serial Monitor at 115200 baud.
Join the setup portal
On first boot, connect to the camera setup Wi-Fi network, open http://192.168.4.1, and save your 2.4 GHz Wi-Fi name and password.
Power and place the camera
Move to a stable 5V supply. Use the printed local IP address from Serial Monitor or the setup page to open the private camera stream.
Code
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include "esp_camera.h"
#define FLASH_LED_PIN 4
#define RESET_HOLD_SECONDS 8
// AI Thinker ESP32-CAM pin map
#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
Preferences prefs;
WebServer server(80);
String ssid;
String pass;
void startSetupPortal();
void startCameraServer();
bool configureCamera() {
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_JPEG;
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 12;
config.fb_count = psramFound() ? 2 : 1;
config.fb_location = CAMERA_FB_IN_PSRAM;
return esp_camera_init(&config) == ESP_OK;
}
void handleRoot() {
server.send(200, "text/html", "<h1>ESP32-CAM</h1><p><a href='/capture'>Capture frame</a></p><p><a href='/reset'>Reset Wi-Fi setup</a></p>");
}
void handleCapture() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
server.send(503, "text/plain", "Camera capture failed");
return;
}
server.sendHeader("Content-Disposition", "inline; filename=capture.jpg");
server.send_P(200, "image/jpeg", (const char *)fb->buf, fb->len);
esp_camera_fb_return(fb);
}
void setup() {
pinMode(FLASH_LED_PIN, OUTPUT);
digitalWrite(FLASH_LED_PIN, LOW);
Serial.begin(115200);
delay(200);
prefs.begin("cam-portal", false);
ssid = prefs.getString("ssid", "");
pass = prefs.getString("pass", "");
if (ssid.length() == 0) {
startSetupPortal();
return;
}
WiFi.mode(WIFI_STA);
WiFi.begin(ssid.c_str(), pass.c_str());
Serial.print("Joining Wi-Fi");
for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) {
delay(250);
Serial.print('.');
}
Serial.println();
if (WiFi.status() != WL_CONNECTED || !configureCamera()) {
startSetupPortal();
return;
}
startCameraServer();
}
void startSetupPortal() {
WiFi.mode(WIFI_AP);
WiFi.softAP("ESP32-CAM-Setup");
server.on("/", HTTP_GET, []() {
server.send(200, "text/html", "<form method='post' action='/save'><label>Wi-Fi name <input name='s'></label><br><label>Password <input name='p' type='password'></label><br><button>Save</button></form>");
});
server.on("/save", HTTP_POST, []() {
prefs.putString("ssid", server.arg("s"));
prefs.putString("pass", server.arg("p"));
server.send(200, "text/plain", "Saved. Rebooting into camera mode.");
delay(500);
ESP.restart();
});
server.begin();
Serial.println("Setup portal: http://192.168.4.1");
}
void startCameraServer() {
server.on("/", handleRoot);
server.on("/capture", handleCapture);
server.on("/reset", []() {
prefs.clear();
server.send(200, "text/plain", "Wi-Fi settings cleared. Rebooting.");
delay(500);
ESP.restart();
});
server.begin();
Serial.print("Camera page: 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-CAM Setup Portal Security Camera.
Open in Schematik →