ESP32-S3 camera board and PIR motion sensor mounted near a doorway with phone notification

Intermediate

Build a Motion-Triggered Security Camera

SensorsIoT

Combine a camera board with a motion sensor and an SD card so it only captures and saves an image when something actually moves, a real, self-contained motion-triggered camera with no cloud account required.

3 real parts included

Total for this kit: 1,220.00 ฿

Doesn't include any order discount you may get at checkout.

What you'll need

Project Goal

Most camera projects just stream video constantly. This one is smarter: it sits idle, watching for motion, and only wakes the camera and writes a photo to storage when something genuinely triggers it, a real motion-activated security camera that needs no subscription and no cloud account.

What the Controller Does

It spends most of its time simply polling the motion sensor's output pin. The moment that pin goes high, it wakes the camera, captures a single frame, and writes it to the SD card with a timestamped filename before going back to watching.

What the Motion Sensor Does

The PIR sensor detects the infrared heat signature of a moving body passing through its field of view and reports it as a simple digital signal, the same detection technology behind most household motion-activated lights.

What the Camera Board Does

It stays idle until commanded, then captures a single still frame on demand and hands it to the controller as image data ready to be written straight to a file.

What the SD Card Module Does

It gives the project real, permanent local storage for every captured photo, something the controller's own onboard memory cannot hold at any useful scale, and something that survives a power cycle.

Software and Library Concepts

Get each part working alone first: confirm the motion sensor reliably reports a clean high/low signal, take one manual photo and save it to the SD card, then combine them so a motion event is what triggers the capture.

Testing and Troubleshooting

  • The motion sensor fires constantly with nothing moving, most PIR modules have an onboard sensitivity and delay trimmer, turn sensitivity down and give the sensor a minute to stabilize after power-on before testing.
  • Photos come out blank or corrupted, confirm the SD card is formatted FAT32 and that the file is being properly closed after each write.
  • The camera misses fast-moving subjects, add a short cooldown after each capture rather than trying to capture continuously, and accept that a single still frame will always trail the trigger slightly.

Optional Upgrade

Add the real-time clock from the SD Card Data Logger build so every saved photo gets an accurate timestamp in its filename even if the controller loses power between triggers.

Wiring

  • PIR VCC → camera board 5V
  • PIR GND → camera board GND
  • PIR OUT → camera board GPIO13 — pick any GPIO that your board's `camera_pins.h` does NOT list as a camera/PSRAM pin; GPIO13 is a common example, confirm against your specific board
  • SD module VCC → camera board 3.3V (or 5V if the module has its own onboard regulator — check the module's silkscreen)
  • SD module GND → camera board GND
  • SD module CS → GPIO1 — again, pick a free GPIO not claimed by the camera interface
  • SD module SCK/MOSI/MISO → the board's hardware SPI pins (used automatically by `SD.begin()`)
  • Freenove camera board → computer via USB-C — power + programming

Which GPIOs are actually free depends entirely on which pins Freenove's camera example claims in its `camera_pins.h` for your exact board — open that file first and avoid every pin it lists before wiring PIR/SD. Most PIR modules have onboard potentiometers for sensitivity and retrigger delay; if you get rapid-fire triggers, turn the delay pot up rather than debouncing purely in code.

Wiring Diagram

VCC → 5V GND OUT → GPIO13 VCC → 3.3V GND CS → GPIO1 SCK/MOSI/MISO Freenove ESP32-S3 PIR SD module the board's hardware SPI…

Starter Code

#include <SPI.h>
#include <SD.h>

// --- Pin definitions ---
// PIR and SD pins: pick any GPIOs that Freenove's camera_pins.h for your exact
// board does NOT list as in use by the camera/PSRAM interface. The pins below
// are a common example pattern - confirm against your board's pin definition
// header before wiring.
#define PIR_PIN     13   // PIR OUT (digital input, HIGH on motion)
#define SD_CS_PIN   1    // SD card module chip select
// SD_CS_PIN uses the board's default hardware SPI bus (SCK/MOSI/MISO) via SD.begin()

bool motionActive = false;

void setup() {
  Serial.begin(115200);
  pinMode(PIR_PIN, INPUT);

  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("SD card init failed - check wiring/CS pin");
  } else {
    Serial.println("SD card ready");
  }
}

void logMotionEvent(unsigned long timestampMs) {
  File logFile = SD.open("/motion_log.txt", FILE_APPEND);
  if (logFile) {
    logFile.print("Motion detected at t+");
    logFile.print(timestampMs);
    logFile.println("ms");
    logFile.close();
    Serial.println("Logged motion event to SD");
  } else {
    Serial.println("Failed to open log file on SD");
  }
}

void loop() {
  bool motionNow = digitalRead(PIR_PIN) == HIGH;

  if (motionNow && !motionActive) {
    motionActive = true;
    Serial.println("Motion started");
    logMotionEvent(millis());

    // --- Camera integration point (pseudocode) ---
    // This is where you'd call into the camera example's real capture flow.
    // The official CameraWebServer example (and esp_camera.h it's built on)
    // uses these real, genuine functions - wire this in once you've loaded
    // that example and confirmed your board's camera_pins.h:
    //
    //   camera_fb_t *fb = esp_camera_fb_get();
    //   if (fb) {
    //     File imgFile = SD.open("/capture.jpg", FILE_WRITE);
    //     if (imgFile) {
    //       imgFile.write(fb->buf, fb->len);
    //       imgFile.close();
    //     }
    //     esp_camera_fb_return(fb);
    //   }
    //
    // Don't copy pin numbers from anywhere else - camera_pins.h from your
    // loaded example already has them correct for your board.

  } else if (!motionNow && motionActive) {
    motionActive = false;
    Serial.println("Motion ended");
  }

  delay(200);
}

Not sure about a step? Ask us