Networking and Communication

ESP32 Networked Camera Node

A wireless camera node that automatically captures and saves images to a computer every 24 hours at random times. The ESP32 sleeps between captures to stay cool and save power.

Hardware

  • ESP32S3 with Sense camera module
  • OV5640 camera lens
  • Micro SD card slot (included on Sense module)

Setup Notes

  • Attach the antenna! The ESP32 needs it for WiFi
  • Disable VPN on your computer during setup (blocks local network communication)

Software Architecture

Two-part system:

  • ESP32 (Client) - Captures images and sends them over WiFi
  • Python Server (Computer) - Receives and saves images

Part 1: Computer Setup (Python Server)

Install Python & Flask

Mac/Linux:

# Create project folder
cd Desktop
mkdir esp32_server
cd esp32_server

# Create virtual environment
python3 -m venv venv
source venv/bin/activate

# Install Flask
pip install flask

Create the Server

Create a file called image_server.py:

from flask import Flask, request
import os
from datetime import datetime

app = Flask(__name__)
SAVE_FOLDER = "esp32_images"

if not os.path.exists(SAVE_FOLDER):
    os.makedirs(SAVE_FOLDER)

@app.route('/upload', methods=['POST'])
def upload_image():
    try:
        filename = request.headers.get('X-Filename')
        if not filename:
            filename = f"image_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
        
        filepath = os.path.join(SAVE_FOLDER, filename)
        with open(filepath, 'wb') as f:
            f.write(request.data)
        
        print(f"Image saved: {filename} ({len(request.data)} bytes)")
        return "Image received successfully", 200
    except Exception as e:
        print(f"Error: {e}")
        return "Error saving image", 500

if __name__ == '__main__':
    print("ESP32 Image Server")
    print(f"Saving to: {os.path.abspath(SAVE_FOLDER)}")
    app.run(host='0.0.0.0', port=8080, debug=False)

Run the Server

# Normal mode (terminal must stay open)
python3 image_server.py

# Background mode (can close terminal)
nohup python3 image_server.py > server.log 2>&1 &

Note your computer's IP address from the server output (e.g., 192.168.1.100). You'll need this for the ESP32 code!

Part 2: ESP32 Code

Arduino Libraries Needed

Complete Code

Make sure to update:

#include <WiFi.h>
#include <HTTPClient.h>
#include "esp_camera.h"
#include "esp_sleep.h"
#include <time.h>

// WiFi credentials
const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";

// Server URL - UPDATE WITH YOUR COMPUTER'S IP
const char* serverUrl = "http://192.168.1.100:8080/upload";

// XIAO ESP32S3 Sense 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

// Sleep settings (hours)
#define MIN_SLEEP_HOURS 20
#define MAX_SLEEP_HOURS 24
#define uS_TO_S_FACTOR 1000000ULL

bool cameraInitialized = false;

void initCamera() {
  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.pixel_format = PIXFORMAT_JPEG;
  
  if(psramFound()){
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_QVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }
  
  esp_err_t err = esp_camera_init(&config);
  cameraInitialized = (err == ESP_OK);
  if (cameraInitialized) {
    Serial.println("Camera initialized!");
  } else {
    Serial.println("Camera init failed");
  }
}

bool sendImageToComputer(camera_fb_t * fb) {
  HTTPClient http;
  http.begin(serverUrl);
  http.addHeader("Content-Type", "image/jpeg");
  
  time_t now = time(nullptr);
  struct tm* timeinfo = localtime(&now);
  char timestamp[32];
  strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", timeinfo);
  http.addHeader("X-Filename", String(timestamp) + ".jpg");
  
  int httpResponseCode = http.POST(fb->buf, fb->len);
  http.end();
  
  return (httpResponseCode > 0);
}

void captureAndSendImage() {
  // Take throwaway photo (camera sensor warm-up)
  camera_fb_t * fb_warmup = esp_camera_fb_get();
  if (fb_warmup) {
    esp_camera_fb_return(fb_warmup);
  }
  delay(1000);
  
  // Take real photo
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Capture failed");
    return;
  }
  
  Serial.print("Photo captured: ");
  Serial.print(fb->len);
  Serial.println(" bytes");
  
  bool success = sendImageToComputer(fb);
  esp_camera_fb_return(fb);
  
  Serial.println(success ? "✓ Saved to computer" : "✗ Failed to save");
}

void setup() {
  Serial.begin(115200);
  Serial.println("\n=== ESP32 Camera Node ===");
  
  // Connect WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected!");
  
  // Sync time
  configTime(0, 0, "pool.ntp.org");
  time_t now = time(nullptr);
  while (now < 8 * 3600 * 2) {
    delay(500);
    now = time(nullptr);
  }
  randomSeed(now);
  
  // Initialize camera
  initCamera();
  delay(2000);
  
  if (cameraInitialized) {
    captureAndSendImage();
  }
  
  // Sleep until next capture (random 20-24 hours)
  float randomHours = MIN_SLEEP_HOURS + (random(0, 1000) / 1000.0) * (MAX_SLEEP_HOURS - MIN_SLEEP_HOURS);
  uint64_t sleepSeconds = (uint64_t)(randomHours * 3600);
  
  Serial.print("Sleeping for ");
  Serial.print(randomHours);
  Serial.println(" hours");
  
  esp_sleep_enable_timer_wakeup(sleepSeconds * uS_TO_S_FACTOR);
  esp_deep_sleep_start();
}

void loop() {
  // Never runs - device sleeps and resets
}

Common Issues & Solutions

Problem Solution
ESP32 won't connect to WiFi Check antenna is attached, verify WiFi credentials
Computer can't be reached Disable VPN, use correct IP address
Port 5000 already in use Use port 8080 instead (AirPlay uses 5000 on Mac)
Pink/purple images Camera needs warm-up photo (code includes this fix)
ESP32 gets hot Use deep sleep mode (included in final code)
pip: command not found Use pip3 or python3 -m pip instead