Whether you’re building a wildlife camera trap, a time-lapse project, or just want to explore the possibilities of IoT photography using your ESP32-CAM, one thing is clear—you’ll need a simple way to capture and save photos to a microSD card. That’s exactly what we’ll do in this tutorial.
Let’s get started!
Project Overview
In this project, we will create a compact, low-power camera system using the ESP32-CAM. The board will stay in a deep sleep mode most of the time to save power. When you press the RESET button, the board will wake up, take a photo, save it to the microSD card you’ve inserted into the board, and then go right back to deep sleep.

To keep track of your photos, the board will store the picture number in its internal flash memory. This way, even after a reset, it won’t forget which photo comes next. Each time you restart the board and take a new photo, the number will go up by one, making it easy to organize and find your pictures later.
Things You Will Need
For this project, you will need the following items:
- ESP32-CAM module: This will serve as the brains of your project.
- A micro USB cable: For powering and programming the ESP32-CAM.
- FTDI Adapter or ESP32-CAM-MB Adapter (optional): If your ESP32-CAM doesn’t have a built-in USB-to-Serial converter, you’ll need this to upload code.
- Computer with Arduino IDE: You’ll use this to write and upload the code to your ESP32.
When choosing your ESP32-CAM board for this project, it’s important to pick the right type. You’ll want one that can be easily programmed from your computer and powered up afterwards.
The ESP32-CAM with the ESP32-CAM-MB daughterboard or the newer ESP32-CAM-CH340 are good choices, as they both come with a USB port for programming and power. It’s best to avoid the bare ESP32-CAM board, as it needs a USB-to-Serial converter that you might not have on hand.
Preparing the microSD card
Before using your microSD card in a project, it’s important to make sure it’s properly formatted with the correct file system—FAT16 or FAT32. This helps the ESP32 write files without any issues.
If you’re using a brand new SD card, it probably came already formatted with a FAT file system. However, this factory formatting might not be perfect, and you could run into problems. If you’re using an older card that’s been used before, it will definitely need to be reformatted. Either way, it’s a good idea to format the card before using it in your project.
There are two methods to format your microSD card:
Method 1
First, insert your microSD card into your computer. Then, find your SD card’s drive, and right-click on it. Choose Format from the menu. A window will pop up—select FAT32 as the file system, then click Start to begin formatting. Follow the instructions on the screen until it’s done.

Method 2
For better results and fewer errors, it’s highly recommended to use the official SD card formatter utility made by the SD Association. This tool is more reliable than the basic formatter that comes with your computer. You can download it from the SD Association’s website. Once installed, run the program, choose your SD card from the list of drives, and click the Format button. This special tool helps avoid common problems caused by bad or incomplete formatting, which can save you a lot of troubleshooting later on.

Connecting the ESP32-CAM to the Computer
The bare ESP32-CAM module, though powerful, lacks a built-in USB interface for programming and communication with your computer. This means you’ll need a little help to get it up and running. There are three main options you can choose from:
Option 1: Bare ESP32-CAM + FTDI Adapter
If you have a bare ESP32-CAM module, you can use a USB-to-serial adapter (an FTDI adapter) to connect it to your computer like this:

Many FTDI adapters have a jumper that lets you choose between 3.3V and 5V. As we are powering the ESP32-CAM with 5V, make sure the jumper is set to 5V.
Please note that the GPIO 0 pin is connected to Ground. This connection is only necessary while programming the ESP32-CAM. Once you have finished programming the module, you must disconnect this connection.
Remember! You’ll have to make this connection every time you want to upload a new sketch.
Option 2: Bare ESP32-CAM + ESP32-CAM-MB Adapter
Using the FTDI Adapter to program the ESP32-CAM is a bit of a hassle. This is why many vendors now sell the ESP32-CAM board along with a small add-on daughterboard called the ESP32-CAM-MB.
You stack the ESP32-CAM on the daughterboard, attach a micro USB cable, and click the Upload button to program your board. It’s that simple.

Option 3: ESP32-CAM-CH340
If you have an ESP32-CAM-CH340 module, you’re in luck! This variant comes with a CH340 USB-to-serial chip onboard, eliminating the need for additional hardware. Simply connect it to your computer via a micro USB cable and you’re ready to go.

Keep in mind that you’ll still need to connect the GPIO0 pin to GND during programming, just like with the FTDI adapter.
Setting Up the Arduino IDE
No matter which option you choose, the setup within the Arduino IDE is the same.
Installing the ESP32 Board
To use the ESP32-CAM, or any ESP32, with the Arduino IDE, you must first install the ESP32 board (also known as the ESP32 Arduino Core) via the Arduino Board Manager.
If you haven’t already, follow this tutorial to install the ESP32 board:
Selecting the Board and Port
After installing the ESP32 Arduino Core, restart your Arduino IDE and click on “Select other board and port…” on the top drop-down menu.

A new window will pop up. Search for the specific type of ESP32 board you are using (in our case, it’s the AI Thinker ESP32-CAM).
Next, select the port that corresponds to your ESP32-CAM board. It’s usually labeled something like “/dev/ttyUSB0” (on Linux or macOS) or “COM6” (on Windows).

That’s it; the Arduino IDE is now set up for the ESP32-CAM!
Uploading the Code
Below is the code you’ll need to upload to your ESP32-CAM.
#include "esp_camera.h"
#include "Arduino.h"
#include "FS.h" // SD Card ESP32
#include "SD_MMC.h" // SD Card ESP32
#include "soc/soc.h" // Disable brownout problems
#include "soc/rtc_cntl_reg.h" // Disable brownout problems
#include "driver/rtc_io.h"
#include <EEPROM.h> // read and write from flash memory
// Define the number of bytes you want to access
#define EEPROM_SIZE 1
// Pin definition for CAMERA_MODEL_AI_THINKER
#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
// Flash LED pin
#define FLASH_LED_PIN 4
int pictureNumber = 0;
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // Disable brownout detector
Serial.begin(115200);
Serial.println("ESP32-CAM Photo Capture Starting...");
// Initialize EEPROM first to get picture number
EEPROM.begin(EEPROM_SIZE);
pictureNumber = EEPROM.read(0) + 1;
// Handle overflow (reset to 1 if it exceeds 255)
if (pictureNumber > 255) {
pictureNumber = 1;
}
Serial.printf("Taking picture number: %d\n", pictureNumber);
// Initialize SD Card BEFORE camera to reduce memory pressure
Serial.println("Starting SD Card");
if (!SD_MMC.begin()) {
Serial.println("SD Card Mount Failed");
goToSleep();
return;
}
uint8_t cardType = SD_MMC.cardType();
if (cardType == CARD_NONE) {
Serial.println("No SD Card attached");
goToSleep();
return;
}
Serial.printf("SD Card Type: ");
if (cardType == CARD_MMC) {
Serial.println("MMC");
} else if (cardType == CARD_SD) {
Serial.println("SDSC");
} else if (cardType == CARD_SDHC) {
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
// Camera configuration - REDUCED settings to prevent stack overflow
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;
// CRITICAL: Reduced frame size and quality to prevent stack overflow
if (psramFound()) {
Serial.println("PSRAM found");
config.frame_size = FRAMESIZE_XGA; // Reduced from UXGA to XGA
config.jpeg_quality = 12; // Increased from 10 to 12 (lower quality, smaller file)
config.fb_count = 1; // Reduced from 2 to 1 to save memory
} else {
Serial.println("PSRAM not found");
config.frame_size = FRAMESIZE_VGA; // Reduced from SVGA to VGA
config.jpeg_quality = 15; // Lower quality for boards without PSRAM
config.fb_count = 1;
}
// Additional memory-saving configurations
config.grab_mode = CAMERA_GRAB_LATEST; // Changed from WHEN_EMPTY to LATEST
// Initialize Camera
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x\n", err);
goToSleep();
return;
}
Serial.println("Camera initialized successfully");
// Additional sensor settings to optimize for memory usage
sensor_t * s = esp_camera_sensor_get();
if (s != NULL) {
// Lower the resolution if needed
// s->set_framesize(s, FRAMESIZE_VGA);
// Optimize other settings
s->set_brightness(s, 0); // -2 to 2
s->set_contrast(s, 0); // -2 to 2
s->set_saturation(s, 0); // -2 to 2
s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect)
s->set_whitebal(s, 1); // 0 = disable , 1 = enable
s->set_awb_gain(s, 1); // 0 = disable , 1 = enable
s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled
s->set_exposure_ctrl(s, 1); // 0 = disable , 1 = enable
s->set_aec2(s, 0); // 0 = disable , 1 = enable
s->set_ae_level(s, 0); // -2 to 2
s->set_aec_value(s, 300); // 0 to 1200
s->set_gain_ctrl(s, 1); // 0 = disable , 1 = enable
s->set_agc_gain(s, 0); // 0 to 30
s->set_gainceiling(s, (gainceiling_t)0); // 0 to 6
s->set_bpc(s, 0); // 0 = disable , 1 = enable
s->set_wpc(s, 1); // 0 = disable , 1 = enable
s->set_raw_gma(s, 1); // 0 = disable , 1 = enable
s->set_lenc(s, 1); // 0 = disable , 1 = enable
s->set_hmirror(s, 0); // 0 = disable , 1 = enable
s->set_vflip(s, 0); // 0 = disable , 1 = enable
s->set_dcw(s, 1); // 0 = disable , 1 = enable
s->set_colorbar(s, 0); // 0 = disable , 1 = enable
}
// Wait a moment for camera to stabilize
delay(1000);
// Take Picture with Camera
Serial.println("Taking picture...");
camera_fb_t *fb = NULL;
// Try to take picture with retries
for (int retry = 0; retry < 3; retry++) {
fb = esp_camera_fb_get();
if (fb) break;
Serial.printf("Camera capture failed, retry %d\n", retry + 1);
delay(500);
}
if (!fb) {
Serial.println("Camera capture failed after retries");
goToSleep();
return;
}
Serial.printf("Picture taken! Size: %zu bytes\n", fb->len);
// Create file path
String path = "/picture" + String(pictureNumber) + ".jpg";
// Save picture to SD card
fs::FS &fs = SD_MMC;
Serial.printf("Picture file name: %s\n", path.c_str());
File file = fs.open(path.c_str(), FILE_WRITE);
if (!file) {
Serial.println("Failed to open file in writing mode");
esp_camera_fb_return(fb);
goToSleep();
return;
}
// Write file in chunks to prevent memory issues
size_t totalBytes = fb->len;
size_t bytesWritten = 0;
const size_t chunkSize = 1024; // Write in 1KB chunks
while (bytesWritten < totalBytes) {
size_t toWrite = min(chunkSize, totalBytes - bytesWritten);
size_t written = file.write(fb->buf + bytesWritten, toWrite);
if (written != toWrite) {
Serial.println("Write error occurred");
break;
}
bytesWritten += written;
}
file.close();
if (bytesWritten == totalBytes) {
Serial.printf("Saved file to path: %s (%zu bytes)\n", path.c_str(), bytesWritten);
// Update picture number in EEPROM only after successful save
EEPROM.write(0, pictureNumber);
EEPROM.commit();
Serial.println("Picture saved successfully!");
} else {
Serial.printf("File write incomplete: %zu/%zu bytes\n", bytesWritten, totalBytes);
}
// Return the frame buffer immediately after use
esp_camera_fb_return(fb);
// Brief delay before sleep
delay(500);
goToSleep();
}
void goToSleep() {
// Deinitialize camera to free memory before sleep
esp_camera_deinit();
// Turn off the ESP32-CAM white on-board LED (flash) connected to GPIO 4
pinMode(FLASH_LED_PIN, OUTPUT);
digitalWrite(FLASH_LED_PIN, LOW);
rtc_gpio_hold_en(GPIO_NUM_4);
Serial.println("Going to sleep now");
Serial.flush();
delay(100);
esp_deep_sleep_start();
}
void loop() {
// Empty loop - everything happens in setup()
}Demonstration
Once you’re ready, click the “Upload” button in the Arduino IDE to transfer the code to your ESP32-CAM. After the upload completes, if you’re using an FTDI adapter or an ESP32-CAM-CH340 module, make sure to disconnect the GPIO 0 pin from GND. This is important for the ESP32-CAM to function correctly in normal operation.
Next, open the Serial Monitor within the Arduino IDE and set the baud rate to 115200. To ensure the ESP32-CAM starts running your freshly uploaded code, give the RST button a quick press. The camera will take a photo. When it takes the photo, you’ll see the flash (which is connected to GPIO 4) briefly turn on.
Check the Serial Monitor for messages. You should see the filename of the photo, along with a message that confirms the image has been saved to the microSD card.

You can continue pressing the RESET button to take more photos. Each new photo will have a filename with an increased number. When you’re finished taking photos, power off the ESP32-CAM and remove the microSD card. Put the card into your computer, and you should find all your saved photos there.

Code Explanation
The stretch starts by including several libraries.
- The
esp_camera.hlibrary lets us control the ESP32-CAM’s camera. It includes all the tools we need to take photos, adjust camera settings, and work with image data. - The
Arduino.his a standard Arduino library that includes basic functions like delay(), Serial.print(), and other things we use in almost every Arduino program. - The
FS.hlibrary gives us commands to read from and write to files on the SD card. - The
SD_MMC.hlibrary helps us work with SD cards using the SD_MMC interface, which is how the ESP32-CAM talks to its microSD card slot. - The
soc.handrtc_cntl_reg.hlibraries allow us to disable the brownout detector, which can cause unwanted resets when the board’s voltage drops a bit. - The
rtc_io.hlibrary helps us control RTC (Real Time Clock) pins, which is useful when we want to hold GPIOs during deep sleep—like keeping the flash LED off. - The
EEPROM.hlibrary lets us save data (like the picture number) into the ESP32’s internal flash memory, so it doesn’t get erased when the board resets. That way, each new picture gets a new filename instead of starting over from 0.
#include "esp_camera.h"
#include "Arduino.h"
#include "FS.h" // SD Card ESP32
#include "SD_MMC.h" // SD Card ESP32
#include "soc/soc.h" // Disable brownout problems
#include "soc/rtc_cntl_reg.h" // Disable brownout problems
#include "driver/rtc_io.h"
#include <EEPROM.h> // read and write from flash memoryWe also define how many bytes of EEPROM we want to use—in this case, just 1 byte is enough to store numbers up to 255.
// Define the number of bytes you want to access
#define EEPROM_SIZE 1Then, we define all the pins the ESP32-CAM board uses to talk to the camera and control other features. These pin numbers are specific to the AI-Thinker model, so we make sure to match them properly. We also define the pin for the flash LED, which helps take photos in low light.
// Pin definition for CAMERA_MODEL_AI_THINKER
#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
// Flash LED pin
#define FLASH_LED_PIN 4We also set up a variable called pictureNumber which we’ll use to keep track of how many photos we’ve taken.
int pictureNumber = 0;In the setup() function, we start by disabling the brownout detector, which is a feature that resets the board if the voltage drops too low. This can sometimes cause problems during normal use, so we turn it off for smoother operation.
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // Disable brownout detectorNext, we start the Serial Monitor so we can see messages from the board in the Arduino IDE. We also print a welcome message so we know the program has started.
Serial.begin(115200);
Serial.println("ESP32-CAM Photo Capture Starting...");We then initialize the EEPROM to read the last saved photo number. We add 1 to that number, so the next photo gets a new name. If that number ever gets too big (more than 255), we reset it back to 1 so we don’t run into errors.
// Initialize EEPROM first to get picture number
EEPROM.begin(EEPROM_SIZE);
pictureNumber = EEPROM.read(0) + 1;
// Handle overflow (reset to 1 if it exceeds 255)
if (pictureNumber > 255) {
pictureNumber = 1;
}
Serial.printf("Taking picture number: %d\n", pictureNumber);Next, we set up the SD card. This step is really important and must be done before initializing the camera, because the camera uses a lot of memory.
If the SD card doesn’t mount properly or there’s no card inserted, we print an error and send the board to deep sleep mode to save power.
// Initialize SD Card BEFORE camera to reduce memory pressure
Serial.println("Starting SD Card");
if (!SD_MMC.begin()) {
Serial.println("SD Card Mount Failed");
goToSleep();
return;
}Once the SD card is ready, we check its type and print what kind it is—this is just to help us troubleshoot if something goes wrong.
uint8_t cardType = SD_MMC.cardType();
if (cardType == CARD_NONE) {
Serial.println("No SD Card attached");
goToSleep();
return;
}
Serial.printf("SD Card Type: ");
if (cardType == CARD_MMC) {
Serial.println("MMC");
} else if (cardType == CARD_SD) {
Serial.println("SDSC");
} else if (cardType == CARD_SDHC) {
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}After that, we configure the camera settings. This section looks big, but it’s just setting each pin to match the hardware. These settings tell the ESP32-CAM how to connect with the camera module.
// Camera configuration - REDUCED settings to prevent stack overflow
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;We then check if PSRAM (extra memory) is available on the board. If it is, we use better settings like higher resolution and better JPEG quality. If not, we lower the settings to make sure the board doesn’t crash from using too much memory.
// CRITICAL: Reduced frame size and quality to prevent stack overflow
if (psramFound()) {
Serial.println("PSRAM found");
config.frame_size = FRAMESIZE_XGA; // Reduced from UXGA to XGA
config.jpeg_quality = 12; // Increased from 10 to 12 (lower quality, smaller file)
config.fb_count = 1; // Reduced from 2 to 1 to save memory
} else {
Serial.println("PSRAM not found");
config.frame_size = FRAMESIZE_VGA; // Reduced from SVGA to VGA
config.jpeg_quality = 15; // Lower quality for boards without PSRAM
config.fb_count = 1;
}We also change the way the camera grabs frames to use the latest image rather than waiting for memory to be empty—this helps it run more smoothly with low memory.
// Additional memory-saving configurations
config.grab_mode = CAMERA_GRAB_LATEST; // Changed from WHEN_EMPTY to LATESTNow we try to initialize the camera using all those settings. If something goes wrong, we print an error and go to sleep. But if it works, we print a success message.
// Initialize Camera
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x\n", err);
goToSleep();
return;
}
Serial.println("Camera initialized successfully");After that, we fine-tune some extra camera settings to make sure the image looks decent. These settings control things like brightness, contrast, white balance, and exposure. You can change these as per your needs to get better photos in different lighting conditions.
// Additional sensor settings to optimize for memory usage
sensor_t * s = esp_camera_sensor_get();
if (s != NULL) {
// Lower the resolution if needed
// s->set_framesize(s, FRAMESIZE_VGA);
// Optimize other settings
s->set_brightness(s, 0); // -2 to 2
s->set_contrast(s, 0); // -2 to 2
s->set_saturation(s, 0); // -2 to 2
s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect)
s->set_whitebal(s, 1); // 0 = disable , 1 = enable
s->set_awb_gain(s, 1); // 0 = disable , 1 = enable
s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled
s->set_exposure_ctrl(s, 1); // 0 = disable , 1 = enable
s->set_aec2(s, 0); // 0 = disable , 1 = enable
s->set_ae_level(s, 0); // -2 to 2
s->set_aec_value(s, 300); // 0 to 1200
s->set_gain_ctrl(s, 1); // 0 = disable , 1 = enable
s->set_agc_gain(s, 0); // 0 to 30
s->set_gainceiling(s, (gainceiling_t)0); // 0 to 6
s->set_bpc(s, 0); // 0 = disable , 1 = enable
s->set_wpc(s, 1); // 0 = disable , 1 = enable
s->set_raw_gma(s, 1); // 0 = disable , 1 = enable
s->set_lenc(s, 1); // 0 = disable , 1 = enable
s->set_hmirror(s, 0); // 0 = disable , 1 = enable
s->set_vflip(s, 0); // 0 = disable , 1 = enable
s->set_dcw(s, 1); // 0 = disable , 1 = enable
s->set_colorbar(s, 0); // 0 = disable , 1 = enable
}We wait a second to let the camera get stable, and then we take the picture.
// Wait a moment for camera to stabilize
delay(1000);We try up to three times to take a picture, in case it fails the first time. If it keeps failing, we go to sleep. But if it works, we print the size of the image we just took.
// Take Picture with Camera
Serial.println("Taking picture...");
camera_fb_t *fb = NULL;
// Try to take picture with retries
for (int retry = 0; retry < 3; retry++) {
fb = esp_camera_fb_get();
if (fb) break;
Serial.printf("Camera capture failed, retry %d\n", retry + 1);
delay(500);
}
if (!fb) {
Serial.println("Camera capture failed after retries");
goToSleep();
return;
}
Serial.printf("Picture taken! Size: %zu bytes\n", fb->len);Now we build a file path using the picture number, like “/picture3.jpg”, and open a new file on the SD card with that name.
// Create file path
String path = "/picture" + String(pictureNumber) + ".jpg";
// Save picture to SD card
fs::FS &fs = SD_MMC;
Serial.printf("Picture file name: %s\n", path.c_str());
File file = fs.open(path.c_str(), FILE_WRITE);
if (!file) {
Serial.println("Failed to open file in writing mode");
esp_camera_fb_return(fb);
goToSleep();
return;
}We then save the picture by writing it in small chunks of 1 kilobyte (1024 bytes) at a time. This helps prevent memory issues and keeps the board from crashing during large writes.
// Write file in chunks to prevent memory issues
size_t totalBytes = fb->len;
size_t bytesWritten = 0;
const size_t chunkSize = 1024; // Write in 1KB chunks
while (bytesWritten < totalBytes) {
size_t toWrite = min(chunkSize, totalBytes - bytesWritten);
size_t written = file.write(fb->buf + bytesWritten, toWrite);
if (written != toWrite) {
Serial.println("Write error occurred");
break;
}
bytesWritten += written;
}After writing, we close the file and check if everything was saved correctly. If yes, we update the picture number in EEPROM and save it, so next time we don’t overwrite the old image.
file.close();
if (bytesWritten == totalBytes) {
Serial.printf("Saved file to path: %s (%zu bytes)\n", path.c_str(), bytesWritten);
// Update picture number in EEPROM only after successful save
EEPROM.write(0, pictureNumber);
EEPROM.commit();
Serial.println("Picture saved successfully!");
} else {
Serial.printf("File write incomplete: %zu/%zu bytes\n", bytesWritten, totalBytes);
}We return the memory buffer we used for the photo, since we’re done with it. We then wait half a second and go to deep sleep mode to save power until the next time the board is reset.
// Return the frame buffer immediately after use
esp_camera_fb_return(fb);
// Brief delay before sleep
delay(500);
goToSleep();The goToSleep() function turns off the camera and flash LED, prints a message that we’re going to sleep, and then starts deep sleep mode. This means the board will stay off until the RESET button is pressed again.
void goToSleep() {
// Deinitialize camera to free memory before sleep
esp_camera_deinit();
// Turn off the ESP32-CAM white on-board LED (flash) connected to GPIO 4
pinMode(FLASH_LED_PIN, OUTPUT);
digitalWrite(FLASH_LED_PIN, LOW);
rtc_gpio_hold_en(GPIO_NUM_4);
Serial.println("Going to sleep now");
Serial.flush();
delay(100);
esp_deep_sleep_start();
}Finally, we have an empty loop() function because everything we need happens in the setup() part.
void loop() {
// Empty loop - everything happens in setup()
}
