esp32 advanced 60 min

ESP32: build a solar-powered trail camera

ESP32-CAM with PIR motion sensor, SD card storage, and solar power. Captures images when motion is detected and stores them on the SD card.

Code available for: ESP32 Arduino
Published Aug 25, 2026

A trail camera: ESP32-CAM takes a photo when the PIR motion sensor fires, saves to SD card, and goes back to sleep. Solar + battery means it runs indefinitely in the field.

This is the project that ties the camera (built-in to ESP32-CAM), PIR motion, SD card, deep sleep, and solar+battery tutorials together.

What you need

  • ESP32-CAM module (about $10)
  • PIR motion sensor (HC-SR501)
  • microSD card (8-32 GB)
  • microSD card adapter for the ESP32-CAM (usually ships with the board)
  • 18650 + TP4056 + LDO (from the battery tutorial)
  • Small solar panel (1-2W, 6V)
  • Schottky diode (1N5817)
  • Weatherproof enclosure (IP65 or better)
  • FTDI USB-serial adapter (for programming the ESP32-CAM; it has no USB)

Wiring

ESP32-CAM 5V -- TP4056 OUT+ (via diode from solar panel)
ESP32-CAM GND -- TP4056 OUT- (common ground)
ESP32-CAM 3.3V -- (internal)

PIR VCC -- ESP32-CAM 5V
PIR GND -- ESP32-CAM GND
PIR OUT -- ESP32-CAM GPIO 13

microSD -- ESP32-CAM built-in slot

The ESP32-CAM has built-in microSD support. Insert a card formatted as FAT32.

Programming the ESP32-CAM

The ESP32-CAM does not have a USB port. You need an FTDI adapter:

FTDI 5V  -- ESP32-CAM 5V
FTDI GND -- ESP32-CAM GND
FTDI TX  -- ESP32-CAM RX (GPIO 3)
FTDI RX  -- ESP32-CAM TX (GPIO 1)

Set the FTDI to 3.3V mode. Connect GPIO 0 to GND during power-up to enter download mode. After upload, disconnect GPIO 0 from GND.

The code

#include "esp_camera.h"
#include "SD_MMC.h"
#include "esp_sleep.h"

// ESP32-CAM pin definitions (AI-Thinker model)
#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 PIR_PIN 13

void setup() {
  Serial.begin(115200);
  delay(1000);

  pinMode(PIR_PIN, INPUT);

  // Initialize camera
  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_UXGA;   // 1600x1200
  config.jpeg_quality = 10;
  config.fb_count = 1;

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed: 0x%x\n", err);
    ESP.restart();
  }

  // Initialize SD card
  if (!SD_MMC.begin()) {
    Serial.println("SD card init failed");
    ESP.restart();
  }

  Serial.println("Trail camera ready");
  Serial.println("Press the BOOT button to take a test photo");
}

void loop() {
  if (digitalRead(PIR_PIN) == HIGH) {
    Serial.println("Motion detected, capturing");
    takePhoto();
    delay(2000);   // debounce
  }

  // Sleep for 30 seconds if no motion
  esp_sleep_enable_ext0_wakeup(PIR_PIN, 1);
  esp_sleep_enable_timer_wakeup(30 * 1000000ULL);
  esp_deep_sleep_start();
}

void takePhoto() {
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    return;
  }

  // Generate filename: /sdcard/IMG_001.jpg
  static int photoCount = 0;
  photoCount++;
  char filename[32];
  snprintf(filename, sizeof(filename), "/sdcard/IMG_%03d.jpg", photoCount);

  File file = SD_MMC.open(filename, FILE_WRITE);
  if (!file) {
    Serial.printf("Failed to open %s\n", filename);
    esp_camera_fb_return(fb);
    return;
  }
  file.write(fb->buf, fb->len);
  file.close();
  esp_camera_fb_return(fb);

  Serial.printf("Saved %s (%d bytes)\n", filename, fb->len);
}

Upload. Press the BOOT button (or trigger the PIR). The ESP32-CAM takes a photo, saves it as IMG_001.jpg on the SD card, and goes back to sleep.

The SD card capacity

A 32 GB SD card holds about 20,000 JPEG photos at UXGA resolution (1600x1200). At 1 photo per hour (or per motion event), that is about 2 years of storage.

For higher resolution, use the OV2640’s full 5MP mode (FRAMESIZE_QSXGA). File size per photo doubles.

The battery math

The ESP32-CAM takes about 1 second to wake, capture, save, and sleep. During that 1 second, it draws about 200 mA. At rest, it draws about 20 mA in deep sleep.

For 10 photos per day:

  • Wake time: 10 * 1 = 10 seconds at 200 mA = 0.6 mAh
  • Sleep time: 86390 seconds at 20 mA = 480 mAh

Wait, that’s wrong. 20 mA in deep sleep is way too high. Let me redo the math:

  • Wake time: 10 * 1 second = 10 seconds. 200 mA peak.
  • Sleep time: 86390 seconds at 0.5 mA (ESP32-CAM deep sleep with PIR active).

Per day: (10 * 200 / 3600) + (86390 * 0.5 / 3600) = 0.55 + 12 = 12.5 mAh.

A 2500 mAh 18650 lasts about 200 days without solar. A 1W solar panel in 4 sun-hours provides 200 mAh per day, which is enough.

The weatherproof enclosure

The ESP32-CAM and PIR need weather protection:

  • IP65 or IP67 enclosure (the kind for outdoor electrical boxes)
  • Camera lens opening (use a clear window or the lens through a hole)
  • PIR sensor exposed (use a waterproof PIR with a clear lens cover)
  • Cable glands for any external wires

A standard outdoor junction box with a clear lid works. Mount the camera with the lens through a hole in the box, sealed with silicone.

What you learned

  • A complete solar-powered trail camera.
  • ESP32-CAM with camera, PIR, SD card, and deep sleep.
  • Solar + battery for perpetual operation.
  • 200+ days runtime on a single 18650 with solar.

What to build next

  • The PIR motion tutorial covers the sensor.
  • The 18650 + TP4056 tutorial covers the battery.
  • The solar + battery tutorial covers the perpetual power.
  • The book ESP32 Camera Projects covers image processing, motion detection in software, and remote viewing.