Story
I’ve always been fascinated by spy gadgets and mini electronics. I always wanted to create a tiny camera that could fit in my pocket, capable of capturing moments discreetly. With the advancements in technology and the availability of powerful microcontrollers like the Xiao ESP32S3 Sense, I finally have the opportunity to bring this dream to life.
It is also normally a challenge collecting image data for tinyML projects using the device that you are actually going to deploy the model in. So this camera is also a useful device in remote image data collection.
This project, The Smallest DIY Spy Cam, is a compact camera that you can build yourself. It’s simple, affordable, and a great way to dive into the world of embedded electronics.
We're using the Xiao ESP32S3 Sense for its compact size, low power consumption, expansion camera module (sense) and powerful capabilities, making it perfect for creating the smallest DIY spy cam.
Features:
- Tiny Form Factor: Built with the Xiao ESP32S3 Sense, this camera is incredibly small and can easily be concealed.
- Image Capture with a Button Press: A dedicated button allows you to take pictures instantly.
- Automatic Image Naming: Each image is saved with a sequential filename (image1, image2, image3, etc.), so you never lose track of your captures.
- SD Card Storage: Images are saved directly to an SD card, making it easy to transfer them to your computer.
- Power-Saving Mode: A long press of the button puts the device into deep sleep, where it consumes minimal power. It wakes up and turns on the LED when a long press is detected again.
Components:
- Xiao ESP32S3 Sense: The brain of the operation, providing processing power and connectivity.
- XiaoCamera Module: Captures high-quality images.
- Micro SD Card Module: Stores captured images.
- Push Button: Used for image capture and power control.
- LiPo Battery: Provides power for the camera.
- Wires Essential for wiring the components together.
Step-by-Step Guide:
1. Hardware Assembly:
- Start by connecting the camera module to the Xiao ESP32S3 Sense.
- Next, connect the SD card module. Ensure that the wiring is correct to avoid any issues during data storage.
- Attach the capture button to GPIO pin D0 on the Xiao.
- Solder the LiPo battery to the Xiao battery pads for portable power.
The soldering is not that neat because I am using a cheap soldering iron but that doesn't matter.
You'll have to 3D print the casing for the Camera. I have attached the STL files for printing.
Here is the fully assembled camera in comparison to a 20 KSH, Probably the size of a penny.
2. Software Setup:
- Download the Arduino IDE if you don’t have it already.
- Clone the GitHub repository for this project.
- Open the project in the Arduino IDE and select the Xiao ESP32S3 board from the board manager.
Here: Download the code
This camera.ino code simply captures an image when the button is pressed and saves the image in the SD card in asequential order.
//camera.ino
#include "esp_camera.h"
#include "FS.h"
#include "SD.h"
#include "SPI.h"
// CAMERA_MODEL_XIAO_ESP32S3 // Has PSRAM
// CAMERA PINS
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13
#define LED_GPIO_NUM 21
#define capturePin D0
unsigned long lastCaptureTime = 0; // Last shooting time
int imageCount = 1; // File Counter
bool camera_sign = false; // Check camera status
bool sd_sign = false; // Check sd status
bool captureFlag = false;
// Save pictures to SD card
void photo_save(const char * fileName) {
// Take a photo
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Failed to get camera frame buffer");
return;
}
// Save photo to file
writeFile(SD, fileName, fb->buf, fb->len);
// Release image buffer
esp_camera_fb_return(fb);
Serial.println("Photo saved to file");
}
// SD card write file
void writeFile(fs::FS &fs, const char * path, uint8_t * data, size_t len){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.write(data, len) == len){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
void setup() {
Serial.begin(115200);
pinMode(capturePin, INPUT_PULLUP);
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.frame_size = FRAMESIZE_UXGA;
config.pixel_format = PIXFORMAT_JPEG; // for streaming
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
// if PSRAM IC present, init with UXGA resolution and higher JPEG quality
// for larger pre-allocated frame buffer.
if(config.pixel_format == PIXFORMAT_JPEG){
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// Limit the frame size when PSRAM is not available
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}
} else {
// Best option for face detection/recognition
config.frame_size = FRAMESIZE_240X240;
#if CONFIG_IDF_TARGET_ESP32S3
config.fb_count = 2;
#endif
}
// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
camera_sign = true; // Camera initialization check passes
// Initialize SD card
if(!SD.begin(21)){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
// Determine if the type of SD card is available
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("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");
}
sd_sign = true; // sd initialization check passes
Serial.println("*** XIAO ESP32S3 Spy Camera ***");
Serial.println("Press button to capture and save an image\n");
}
void loop() {
take_pic();
}
void take_pic()
{
if(camera_sign && sd_sign){
if (digitalRead(capturePin) == 0) { // checks when the button is pressed
delay(200); //delay for debouncing
Serial.println("\nImage Captured");
char filename[32];
sprintf(filename, "/image%d.jpg", imageCount);
photo_save(filename);
Serial.printf("Saved picture:%s\n", filename);
Serial.println("");
imageCount++;
}
}
}
Capturing Images with Power EfficiencyIn this enhanced version of code:
- Single press: The device captures an image and saves it to the SD card. Each image is named sequentially and avoids overwriting previous images when the camera is turned on (woken up from deep-sleep mode). to learn more about deep-sleep on th Xiao ESP32S3 Sense use this Link
- Long press: Puts the device into deep sleep, effectively conserving power by shutting down unnecessary processes. Another long press will wake up the device, allowing it to continue capturing images.
This feature makes the device particularly useful for long-term use or image data collection in remote areas where it is impractical to maintain continuous power. This could be especially valuable for TinyML image data collection, enabling the device to remain dormant until needed, significantly extending its battery life in the field.
The code with DeepSleep mode
Download it here
#include "esp_camera.h"
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <Preferences.h>
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13
#define LED_GPIO_NUM 21
#define capturePin D0
#define LEDAnode D5
#define statusLED D6
#define captureLED D4
Preferences preferences; // Preferences object to store non-volatile values
unsigned long lastPressTime = 0; // Last button press start time
unsigned long pressDuration = 0; // Duration of the button press
int imageCount = 1; // File Counter
bool camera_sign = false; // Check camera status
bool sd_sign = false; // Check SD card status
void photo_save(const char * fileName) {
// Take a photo
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Failed to get camera frame buffer");
return;
}
// Save photo to file
writeFile(SD, fileName, fb->buf, fb->len);
// Release image buffer
esp_camera_fb_return(fb);
Serial.println("Photo saved to file");
}
void writeFile(fs::FS &fs, const char * path, uint8_t * data, size_t len){
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.write(data, len) == len){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
void setup() {
Serial.begin(115200);
pinMode(capturePin, INPUT_PULLUP);
pinMode(statusLED, OUTPUT); // Initialize LED pin as an output
pinMode(LEDAnode, OUTPUT);
pinMode(captureLED, OUTPUT);
digitalWrite(LEDAnode, HIGH); //I am using a common anode RGB LED
digitalWrite(captureLED, HIGH);
// Initialize Preferences
preferences.begin("camera", false);
// Retrieve the stored image count from non-volatile memory
imageCount = preferences.getInt("imageCount", 1); // Default to 1 if not set
// 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_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.frame_size = FRAMESIZE_UXGA;
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
if(config.pixel_format == PIXFORMAT_JPEG){
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}
} else {
config.frame_size = FRAMESIZE_240X240;
}
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
camera_sign = true;
// Initialize SD card
if(!SD.begin(21)){
Serial.println("Card Mount Failed");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
sd_sign = true;
Serial.println("*** XIAO ESP32S3 Spy Camera ***");
Serial.println("Press and hold the button to enter deep sleep.");
Serial.println("Press the button briefly to capture an image.");
esp_sleep_enable_ext0_wakeup(static_cast<gpio_num_t>(capturePin), 0);
digitalWrite(statusLED, LOW);
digitalWrite(captureLED, HIGH);
}
void loop() {
handleButtonPress();
}
void handleButtonPress() {
int buttonState = digitalRead(capturePin);
if (buttonState == LOW) {
if (lastPressTime == 0) {
lastPressTime = millis(); // Record the press start time
}
pressDuration = millis() - lastPressTime;
if (pressDuration > 2000) { // Long press detection (>2 seconds)
Serial.println("Long press detected: Going to deep sleep");
digitalWrite(statusLED, HIGH);
delay(1000); // Delay to debounce before deep sleep
goToDeepSleep(); // Enter deep sleep
}
} else {
if (lastPressTime > 0 && pressDuration > 100 && pressDuration < 1000) { // Short press detection
Serial.println("Short press detected: Taking picture");
digitalWrite(captureLED, LOW);
captureImage();
digitalWrite(captureLED, HIGH);
}
lastPressTime = 0; // Reset timing when the button is released
pressDuration = 0;
}
}
void captureImage() {
char filename[32];
sprintf(filename, "/image%d.jpg", imageCount);
photo_save(filename); // Capture the image and save it
Serial.printf("Saved picture: %s\n", filename);
imageCount++;
// Store the new imageCount in non-volatile memory
preferences.putInt("imageCount", imageCount);
}
void goToDeepSleep() {
// Store image count before going to sleep
preferences.putInt("imageCount", imageCount);
preferences.end(); // Close preferences
digitalWrite(statusLED, HIGH);
esp_deep_sleep_start(); // Enter deep sleep
}
- Upload the code to your Xiao ESP32S3 Sense.
- Initialization: The camera, SD card, and LED are initialized during setup. The image count is stored and retrieved from non-volatile memory, ensuring that images are sequentially saved without overwriting existing files.
- Image Capture: A simple press of the button captures an image and saves it to the SD card.
- Deep Sleep and Wake: The long press is detected using a timing mechanism. If the button is held for over 2 seconds, the device goes into deep sleep. When the device wakes up, the LED turns on, signaling that it's ready to capture images again.
Deep Sleep Function
// Deep Sleep Function
void goToDeepSleep() {
// Save image count and other data
preferences.putInt("imageCount", imageCount);
preferences.end(); // Close preferences
// Turn off LED before going to sleep
digitalWrite(LED_GPIO_NUM, LOW);
esp_deep_sleep_start(); // Enter deep sleep
}
- Handling Button Presses: The button logic checks for short presses (to capture an image) and long presses (to enter deep sleep).
cpp
Copy code
void handleButtonPress() {
if (isButtonPressed()) {
if (lastPressTime == 0) {
lastPressTime = millis(); // Record the press start time
}
pressDuration = millis() - lastPressTime;
if (pressDuration > 2000) { // Long press detection (>2 seconds)
Serial.println("Long press detected: Going to deep sleep");
delay(500); // Delay to debounce before deep sleep
// Turn off LED before going to sleep
digitalWrite(LED_GPIO_NUM, LOW);
goToDeepSleep(); // Enter deep sleep
}
} else {
if (lastPressTime > 0 && pressDuration > 200 && pressDuration < 1000) { // Short press detection
Serial.println("Short press detected: Taking picture");
captureImage();
}
lastPressTime = 0; // Reset timing when the button is released
pressDuration = 0;
}
}
3. Operation:
- Once the code is uploaded, and everything is connected, turn on the camera using the power button.
- Press the capture button to take a picture.
- The image will be saved on the SD card with a unique, sequential filename.
Here is the output from the serial monitor when the camera in in operation
The Images will be saved like this;
Gallery:
Video Demo:
Images taken by the Camera:
Future Improvements:
While this project is fully functional, there’s always room for improvement. Here are some ideas for future versions:
- Video Recording: Expand the functionality to capture short video clips.
- Wireless Transfer: Integrate a feature to send images wirelessly to a smartphone or computer.
- Motion Detection: Add motion detection to automatically capture images when movement is detected.
If you enjoyed this project and would like to see more innovative creations like this, I would greatly appreciate your support. Feel free to reach out. Your sponsorship and assistance enable me to continue developing exciting projects and sharing them with the community. Whether through contributions, feedback, or any other form of support, your help makes a real difference.
And if you're reading this and you're from Seeed Studio, I need more XIAOs to keep building such compact projects :( ...help me out :)
Conclusion
This project is a fun and educational way to explore the capabilities of the Xiao ESP32S3 Sense. Whether you’re a beginner or an experienced maker, you’ll find this project both challenging and rewarding. I’m excited to see how others will build upon and customize their versions of The Smallest DIY Spy Cam!
Credits:
A big thank you to ScottyDoesKnow for the original casing that helped me create mine to fit my camera, the open-source community and everyone who provided guidance and inspiration for this project.
License:
This project is shared under the MIT License, encouraging others to remix, adapt, and build upon it. Be sure to give credit where it’s due!
Comments