ESP32-CAM OTA Firmware Updates and WiFiManager Setup Portal
Once an ESP32-CAM is mounted on a wall, in a bird box or on a robot, reconnecting the USB-TTL adapter and the IO0 jumper for every code change becomes a real nuisance. Hard-coding the Wi-Fi password is another problem: change the router and every camera needs reprogramming.
This tutorial solves both. WiFiManager lets you enter Wi-Fi details from a phone through a temporary access point, and ArduinoOTA lets the Arduino IDE upload new firmware over the network. You will also choose a partition scheme that leaves room for OTA, which is the step most people miss.
How OTA and WiFiManager Work
OTA (over-the-air) update needs two application slots in flash. The running firmware receives the new image over Wi-Fi and writes it to the other slot. Only when the image is complete and verified does the bootloader switch to it on the next reboot. If the upload fails half way, the old firmware keeps running. This is why OTA needs a partition table with two app partitions plus an otadata partition that records which slot is active.
WiFiManager tries to connect with the credentials saved in flash. If there are none, or the network cannot be reached, it starts its own access point with a captive portal. You connect your phone to that access point, a page opens, you choose your network and type the password, and the board saves it and restarts as a normal Wi-Fi client.
Choosing a Partition Scheme
The ESP32-CAM has 4 MB of flash. Camera sketches with Wi-Fi, TLS and web pages are large, so many tutorials select a scheme with one huge app partition, which leaves no second slot for OTA.
| Scheme (Arduino IDE name) | App slots | OTA possible |
|---|---|---|
| Huge APP (3MB No OTA/1MB SPIFFS) | 1 x 3 MB | No |
| Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS) | 2 x 1.2 MB | Yes, if the sketch fits |
| Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS) | 2 x 1.9 MB | Yes, recommended for camera projects |
If your board entry does not offer a partition menu, select ESP32 Dev Module, set PSRAM: Enabled and choose Minimal SPIFFS. The first upload with the new scheme must be made over serial, because it changes the partition table itself. After the compile, the IDE reports the sketch size as a percentage of the app partition; keep some margin for future features.
Complete Code
Install WiFiManager by tzapu from the Library Manager. ArduinoOTA and WebServer are part of the ESP32 core. Holding a button on GPIO 13 to GND during power-up erases the saved Wi-Fi details.
#include <WiFi.h>
#include <WiFiManager.h> // tzapu WiFiManager
#include <ArduinoOTA.h>
#include <WebServer.h>
#include "esp_camera.h"
#define RESET_BTN 13 // hold LOW at boot to clear Wi-Fi
#define RED_LED 33 // active LOW
const char *HOSTNAME = "esp32cam-garden";
const char *OTA_PASS = "choose-a-strong-password";
// 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
WebServer server(80);
bool initCamera() {
camera_config_t c = {};
c.ledc_channel = LEDC_CHANNEL_0;
c.ledc_timer = LEDC_TIMER_0;
c.pin_d0 = Y2_GPIO_NUM; c.pin_d1 = Y3_GPIO_NUM;
c.pin_d2 = Y4_GPIO_NUM; c.pin_d3 = Y5_GPIO_NUM;
c.pin_d4 = Y6_GPIO_NUM; c.pin_d5 = Y7_GPIO_NUM;
c.pin_d6 = Y8_GPIO_NUM; c.pin_d7 = Y9_GPIO_NUM;
c.pin_xclk = XCLK_GPIO_NUM; c.pin_pclk = PCLK_GPIO_NUM;
c.pin_vsync = VSYNC_GPIO_NUM; c.pin_href = HREF_GPIO_NUM;
c.pin_sccb_sda = SIOD_GPIO_NUM; c.pin_sccb_scl = SIOC_GPIO_NUM;
c.pin_pwdn = PWDN_GPIO_NUM; c.pin_reset = RESET_GPIO_NUM;
c.xclk_freq_hz = 20000000;
c.pixel_format = PIXFORMAT_JPEG;
c.frame_size = FRAMESIZE_SVGA;
c.jpeg_quality = 12;
c.fb_count = 1;
c.fb_location = CAMERA_FB_IN_PSRAM;
c.grab_mode = CAMERA_GRAB_LATEST;
return esp_camera_init(&c) == ESP_OK;
}
void handleJpg() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
server.send(500, "text/plain", "capture failed");
return;
}
server.setContentLength(fb->len);
server.send(200, "image/jpeg", "");
server.sendContent((const char *)fb->buf, fb->len);
esp_camera_fb_return(fb);
}
void setupOTA() {
ArduinoOTA.setHostname(HOSTNAME); // name shown in the IDE port list
ArduinoOTA.setPassword(OTA_PASS); // IDE asks for this on upload
ArduinoOTA.onStart([]() {
Serial.println("OTA start");
server.stop(); // stop serving during the update
esp_camera_deinit(); // free camera memory
});
ArduinoOTA.onProgress([](unsigned int done, unsigned int total) {
Serial.printf("OTA %u%%\r", done * 100 / total);
});
ArduinoOTA.onEnd([]() { Serial.println("\nOTA done, rebooting"); });
ArduinoOTA.onError([](ota_error_t e) {
Serial.printf("OTA error %u, restarting\n", e);
ESP.restart(); // camera was stopped, so reboot
});
ArduinoOTA.begin();
}
void setup() {
Serial.begin(115200);
pinMode(RESET_BTN, INPUT_PULLUP);
pinMode(RED_LED, OUTPUT);
digitalWrite(RED_LED, LOW); // LED on while setting up
WiFi.mode(WIFI_STA);
WiFiManager wm;
if (digitalRead(RESET_BTN) == LOW) {
Serial.println("Clearing saved Wi-Fi");
wm.resetSettings();
}
wm.setConfigPortalTimeout(180); // give up after 3 minutes
// Starts AP "ESP32CAM-Setup" if it cannot connect
if (!wm.autoConnect("ESP32CAM-Setup", "setup1234")) {
Serial.println("No Wi-Fi configured, restarting");
ESP.restart();
}
Serial.printf("Connected, IP %s\n", WiFi.localIP().toString().c_str());
if (!initCamera()) Serial.println("Camera init failed");
server.on("/jpg", handleJpg);
server.begin();
setupOTA();
digitalWrite(RED_LED, HIGH); // LED off: ready
}
void loop() {
ArduinoOTA.handle(); // must run often
server.handleClient();
}
Using the Portal and Uploading Over the Air
- Upload the sketch once over serial with the Minimal SPIFFS partition scheme.
- On first boot, connect your phone to the Wi-Fi network ESP32CAM-Setup with the password
setup1234. A setup page opens; if not, browse to 192.168.4.1. - Tap Configure WiFi, pick your router, enter the password and save. The board restarts and joins your network.
- In the Arduino IDE, the board appears under Tools > Port as a network port named
esp32cam-garden. Your computer must be on the same network. - Select that port and upload as usual. Enter the OTA password when asked. The Serial Monitor, if connected, shows the progress.
ArduinoOTA uploads from the IDE are convenient during development. For cameras installed at several sites, a pull model is more practical: the board periodically downloads a firmware file from your own web server with the HTTPUpdate library that ships with the ESP32 core, and installs it if the version number is newer. The partition scheme requirement is the same for both methods.
Troubleshooting
- Network port does not appear: mDNS discovery is blocked by the router or a firewall, or the PC is on a different subnet. Restart the IDE and allow it through the firewall.
- Error: sketch too big: the partition scheme still has no OTA slot, or the app is larger than 1.9 MB. Remove unused libraries or features.
- OTA starts but fails at a random percentage: weak Wi-Fi or a weak power supply. Move the camera closer to the router during updates.
- Portal never appears: saved credentials are still valid elsewhere; hold the GPIO 13 button during reset to clear them.
- Portal password rejected: WiFiManager access point passwords must be at least 8 characters.
Summary
WiFiManager removes hard-coded passwords by offering a captive setup portal, and ArduinoOTA lets you upload new firmware from the IDE over Wi-Fi with a password. Both rely on a partition scheme with two app slots, such as Minimal SPIFFS, set with one final serial upload. Stop the camera and web server when an update starts, keep OTA in every version, and the serial adapter can stay in the drawer.