ESP32-CAM and Raspberry Pi Camera Snapshots
This tutorial covers two approaches to capturing and uploading camera snapshots to SiliconWit.IO:
- Part 1: ESP32-CAM (Arduino/C++) - a self-contained WiFi camera module
- Part 2: Raspberry Pi with Python - using a Pi Camera Module or USB webcam
Both follow the same pattern: capture an image, upload it via HTTP, then publish the returned URL as MQTT telemetry.
What You Will Learn
Section titled “What You Will Learn”- How to capture images from an ESP32-CAM or Raspberry Pi camera
- How to upload snapshots to the SiliconWit.IO snapshot API
- How to publish telemetry with snapshot URLs over MQTT
- How to view snapshots as thumbnails on the dashboard
- How to run a Python camera script as a systemd service
Part 1: ESP32-CAM (Arduino)
Section titled “Part 1: ESP32-CAM (Arduino)”The ESP32-CAM AI-Thinker is a low-cost module with a built-in OV2640 camera and WiFi. It captures JPEG images and uploads them directly to SiliconWit.IO.
┌─────────────────┐ HTTPS POST ┌──────────────────┐│ ESP32-CAM │ 1. Upload JPEG │ SiliconWit.IO ││ AI-Thinker │ ────────────────────────────►│ Snapshot API ││ │ 2. Receive URL │ ││ OV2640 Camera │ ◄────────────────────────────│ (Cloud Storage) ││ + WiFi │ │ ││ │ 3. MQTT telemetry │ Dashboard ││ │ with snapshot_url │ (thumbnail) ││ │ ────────────────────────────►│ │└─────────────────┘ └──────────────────┘Prerequisites
Section titled “Prerequisites”Hardware
Section titled “Hardware”| Component | Description |
|---|---|
| ESP32-CAM AI-Thinker | Module with OV2640 camera (most common variant) |
| FTDI USB-to-Serial adapter | 3.3V logic level, for programming (ESP32-CAM has no built-in USB) |
| Jumper wires | Female-to-female, for connecting FTDI to ESP32-CAM |
| 5V power supply or USB cable | ESP32-CAM needs 5V (not 3.3V) for stable operation |
Software
Section titled “Software”| Software | Purpose |
|---|---|
| Arduino IDE 2.x | With ESP32 board support installed (setup guide) |
| PubSubClient library | MQTT client (install via Library Manager) |
| ArduinoJson library | JSON serialization (install via Library Manager) |
SiliconWit.IO Setup
Section titled “SiliconWit.IO Setup”- Go to Dashboard > Devices > Add Device
- Set Name to something like “Office Camera”
- Set Type to Sensor
- Click Create Device and copy the Device ID and Access Token
Wiring (Programming Mode)
Section titled “Wiring (Programming Mode)”The ESP32-CAM has no USB port. Use an FTDI adapter to program it:
| FTDI Pin | ESP32-CAM Pin | Notes |
|---|---|---|
| GND | GND | Common ground |
| VCC (5V) | 5V | Power the module |
| TX | U0R | FTDI TX to ESP32-CAM RX |
| RX | U0T | FTDI RX to ESP32-CAM TX |
| - | IO0 to GND | Connect IO0 to GND for flashing mode |
Full Arduino Code
Section titled “Full Arduino Code”#include <WiFi.h>#include <WiFiClientSecure.h>#include <HTTPClient.h>#include <PubSubClient.h>#include <ArduinoJson.h>#include "esp_camera.h"
// ─── Configuration ───────────────────────────────const char* WIFI_SSID = "YOUR_WIFI_SSID";const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_BROKER = "mqtt.siliconwit.io";const int MQTT_PORT = 8883;const char* DEVICE_ID = "YOUR_DEVICE_ID";const char* ACCESS_TOKEN = "YOUR_ACCESS_TOKEN";
const char* SNAPSHOT_URL = "https://siliconwit.io/api/devices/YOUR_DEVICE_ID/snapshot";
const unsigned long CAPTURE_INTERVAL = 60000; // 60 seconds between snapshots
// ─── Camera pins (AI-Thinker ESP32-CAM) ─────────#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
// ─── Globals ─────────────────────────────────────WiFiClientSecure secureClient;PubSubClient mqtt(secureClient);String telemetryTopic;unsigned long lastCapture = 0;
// ─── Camera init ─────────────────────────────────bool 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_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.grab_mode = CAMERA_GRAB_LATEST;
// Use lower resolution to stay under 500KB limit config.frame_size = FRAMESIZE_VGA; // 640x480 config.jpeg_quality = 12; // 0-63, lower = better quality 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); return false; } Serial.println("Camera initialized"); return true;}
// ─── WiFi ────────────────────────────────────────void setupWifi() { Serial.print("Connecting to WiFi"); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.print("\nConnected - IP: "); Serial.println(WiFi.localIP());}
// ─── MQTT connect ────────────────────────────────void connectMqtt() { while (!mqtt.connected()) { Serial.print("Connecting to MQTT..."); if (mqtt.connect(DEVICE_ID, DEVICE_ID, ACCESS_TOKEN)) { Serial.println("connected"); } else { Serial.print("failed (rc="); Serial.print(mqtt.state()); Serial.println(") retrying in 5s"); delay(5000); } }}
// ─── Upload snapshot and return URL ──────────────String uploadSnapshot(camera_fb_t* fb) { HTTPClient http; WiFiClientSecure uploadClient; uploadClient.setInsecure(); // Use setCACert() in production
http.begin(uploadClient, SNAPSHOT_URL); http.addHeader("Authorization", String("Bearer ") + ACCESS_TOKEN); http.addHeader("Content-Type", "image/jpeg");
int httpCode = http.POST(fb->buf, fb->len); String snapshotUrl = "";
if (httpCode == 200) { String response = http.getString(); JsonDocument doc; DeserializationError err = deserializeJson(doc, response); if (!err && doc["success"] == true) { snapshotUrl = doc["url"].as<String>(); Serial.print("Snapshot uploaded: "); Serial.println(snapshotUrl); } } else { Serial.print("Upload failed, HTTP "); Serial.println(httpCode); if (httpCode > 0) { Serial.println(http.getString()); } }
http.end(); return snapshotUrl;}
// ─── Setup ───────────────────────────────────────void setup() { Serial.begin(115200); Serial.println("\n=== ESP32-CAM Snapshot Demo ===");
if (!initCamera()) { Serial.println("Camera init failed. Halting."); while (true) delay(1000); }
// Discard first few frames (auto-exposure warm-up) for (int i = 0; i < 3; i++) { camera_fb_t* fb = esp_camera_fb_get(); if (fb) esp_camera_fb_return(fb); delay(100); }
setupWifi();
secureClient.setInsecure(); // Use setCACert() in production mqtt.setServer(MQTT_BROKER, MQTT_PORT); mqtt.setBufferSize(512); telemetryTopic = "d/" + String(DEVICE_ID) + "/t";}
// ─── Loop ────────────────────────────────────────void loop() { if (!mqtt.connected()) { connectMqtt(); } mqtt.loop();
if (millis() - lastCapture >= CAPTURE_INTERVAL) { lastCapture = millis();
// Capture image camera_fb_t* fb = esp_camera_fb_get(); if (!fb) { Serial.println("Camera capture failed"); return; }
Serial.printf("Captured %u bytes\n", fb->len);
// Check size before uploading if (fb->len > 500 * 1024) { Serial.println("Image too large, skipping upload"); esp_camera_fb_return(fb); return; }
// Upload to SiliconWit.IO String snapshotUrl = uploadSnapshot(fb); esp_camera_fb_return(fb);
// Publish telemetry with snapshot URL if (snapshotUrl.length() > 0) { JsonDocument doc; doc["snapshot_url"] = snapshotUrl;
char buf[384]; serializeJson(doc, buf); mqtt.publish(telemetryTopic.c_str(), buf);
Serial.print("Telemetry sent: "); Serial.println(buf); } }}Upload and Test
Section titled “Upload and Test”- In Arduino IDE, select Board: AI Thinker ESP32-CAM
- Connect IO0 to GND, then plug in the FTDI adapter
- Click Upload
- After upload completes, disconnect IO0 from GND and press the RST button
- Open Serial Monitor at 115200 baud
Expected output:
=== ESP32-CAM Snapshot Demo ===Camera initializedConnecting to WiFi....Connected - IP: 192.168.1.42Connecting to MQTT...connectedCaptured 24576 bytesSnapshot uploaded: https://cdn.siliconwit.io/snapshots/.../abc123.jpgTelemetry sent: {"snapshot_url":"https://cdn.siliconwit.io/snapshots/.../abc123.jpg"}Viewing on the Dashboard
Section titled “Viewing on the Dashboard”Open your device page on the SiliconWit.IO dashboard. You will see:
- Latest Reading shows a thumbnail of the most recent snapshot
- Data Table shows each telemetry entry with a clickable thumbnail
- Click any thumbnail to open the full-size image
Part 2: Python / Raspberry Pi
Section titled “Part 2: Python / Raspberry Pi”Use a Raspberry Pi with a camera module or USB webcam to capture and upload snapshots using Python.
┌─────────────────┐ HTTPS POST ┌──────────────────┐│ Raspberry Pi │ 1. Upload JPEG │ SiliconWit.IO ││ + Pi Camera │ ────────────────────────────►│ Snapshot API ││ or USB cam │ 2. Receive URL │ ││ │ ◄────────────────────────────│ (Cloud Storage) ││ Python script │ │ ││ (picamera2 / │ 3. MQTT telemetry │ Dashboard ││ OpenCV) │ with snapshot_url │ (thumbnail) ││ │ ────────────────────────────►│ │└─────────────────┘ └──────────────────┘Prerequisites
Section titled “Prerequisites”Hardware
Section titled “Hardware”| Component | Description |
|---|---|
| Raspberry Pi | Any model with camera connector or USB port (Pi 3, Pi 4, Pi 5, Pi Zero 2 W) |
| Camera Module | Pi Camera Module v2/v3 (CSI connector) or any USB webcam |
| MicroSD Card | With Raspberry Pi OS installed |
| Power Supply | Appropriate for your Pi model |
| WiFi or Ethernet | Internet connection |
Software
Section titled “Software”| Software | Purpose |
|---|---|
| Raspberry Pi OS | Bookworm or later |
| Python 3.8+ | Pre-installed on Raspberry Pi OS |
| SiliconWit IO Account | Free IoT platform account |
Install Dependencies
Section titled “Install Dependencies”mkdir ~/siliconwit-cameracd ~/siliconwit-camerapython3 -m venv venvsource venv/bin/activateFor Pi Camera Module (CSI):
pip install requests paho-mqtt picamera2For USB webcam:
pip install requests paho-mqtt opencv-python-headless| Library | Purpose |
|---|---|
requests | HTTP client for uploading snapshots |
paho-mqtt | MQTT client for telemetry |
picamera2 | Raspberry Pi camera interface |
opencv-python-headless | USB webcam capture (alternative to picamera2) |
Full Python Script (Pi Camera Module)
Section titled “Full Python Script (Pi Camera Module)”Create a file called camera_snapshot.py:
import ioimport sslimport jsonimport timeimport requestsimport paho.mqtt.client as mqtt
from picamera2 import Picamera2
# ========== CONFIGURATION ==========DEVICE_ID = "YOUR_DEVICE_ID"ACCESS_TOKEN = "YOUR_ACCESS_TOKEN"
SNAPSHOT_API = f"https://siliconwit.io/api/devices/{DEVICE_ID}/snapshot"
MQTT_BROKER = "mqtt.siliconwit.io"MQTT_PORT = 8883PUBLISH_TOPIC = f"d/{DEVICE_ID}/t"
CAPTURE_INTERVAL = 60 # seconds between snapshotsIMAGE_WIDTH = 640IMAGE_HEIGHT = 480JPEG_QUALITY = 80
# ========== CAMERA SETUP ==========def init_camera(): cam = Picamera2() config = cam.create_still_configuration( main={"size": (IMAGE_WIDTH, IMAGE_HEIGHT), "format": "RGB888"} ) cam.configure(config) cam.start() # Let auto-exposure settle time.sleep(2) return cam
# ========== SNAPSHOT UPLOAD ==========def capture_and_upload(cam): """Capture a JPEG image and upload it to SiliconWit.IO.""" # Capture to in-memory buffer stream = io.BytesIO() cam.capture_file(stream, format="jpeg") image_bytes = stream.getvalue() stream.close()
print(f"[CAMERA] Captured {len(image_bytes)} bytes")
if len(image_bytes) > 500 * 1024: print("[CAMERA] Image too large, skipping upload") return None
# Upload to snapshot API try: resp = requests.post( SNAPSHOT_API, headers={ "Authorization": f"Bearer {ACCESS_TOKEN}", "Content-Type": "image/jpeg", }, data=image_bytes, timeout=15, )
if resp.status_code == 200: data = resp.json() if data.get("success"): print(f"[UPLOAD] OK: {data['url']}") return data["url"] else: print(f"[UPLOAD] Failed: HTTP {resp.status_code} - {resp.text}")
except requests.RequestException as e: print(f"[UPLOAD] Error: {e}")
return None
# ========== MQTT CALLBACKS ==========def on_connect(client, userdata, flags, reason_code, properties): if reason_code == 0: print("[MQTT] Connected to SiliconWit IO") else: print(f"[MQTT] Connection failed: {reason_code}")
def on_disconnect(client, userdata, flags, reason_code, properties): print(f"[MQTT] Disconnected (code: {reason_code}). Reconnecting...")
# ========== MAIN ==========def main(): print("=== Raspberry Pi Camera -> SiliconWit IO ===\n")
# Initialize camera cam = init_camera() print("[CAMERA] Initialized")
# Create MQTT client client = mqtt.Client( callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=DEVICE_ID, protocol=mqtt.MQTTv311, ) client.username_pw_set(DEVICE_ID, ACCESS_TOKEN) client.tls_set(tls_version=ssl.PROTOCOL_TLS_CLIENT) client.on_connect = on_connect client.on_disconnect = on_disconnect client.reconnect_delay_set(min_delay=1, max_delay=60)
print(f"[MQTT] Connecting to {MQTT_BROKER}:{MQTT_PORT}...") try: client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60) except Exception as e: print(f"[ERROR] MQTT connection failed: {e}") cam.stop() return
client.loop_start()
try: while True: if client.is_connected(): snapshot_url = capture_and_upload(cam) if snapshot_url: payload = json.dumps({"snapshot_url": snapshot_url}) client.publish(PUBLISH_TOPIC, payload, qos=1) print(f"[TELEMETRY] Sent: {payload}") else: print("[MQTT] Waiting for connection...")
time.sleep(CAPTURE_INTERVAL)
except KeyboardInterrupt: print("\n[INFO] Shutting down...") finally: cam.stop() client.loop_stop() client.disconnect() print("[INFO] Camera stopped. Disconnected. Goodbye.")
if __name__ == "__main__": main()Run the script:
python camera_snapshot.pyExpected output:
=== Raspberry Pi Camera -> SiliconWit IO ===
[CAMERA] Initialized[MQTT] Connecting to mqtt.siliconwit.io:8883...[MQTT] Connected to SiliconWit IO[CAMERA] Captured 31245 bytes[UPLOAD] OK: https://cdn.siliconwit.io/snapshots/.../abc123.jpg[TELEMETRY] Sent: {"snapshot_url":"https://cdn.siliconwit.io/snapshots/.../abc123.jpg"}Alternative: USB Webcam with OpenCV
Section titled “Alternative: USB Webcam with OpenCV”If you are using a USB webcam instead of the Pi Camera Module, replace the camera functions:
import cv2
# ========== CAMERA SETUP (USB WEBCAM) ==========def init_camera(): cam = cv2.VideoCapture(0) cam.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) if not cam.isOpened(): raise RuntimeError("Could not open USB webcam") # Warm-up frames for _ in range(5): cam.read() return cam
# ========== SNAPSHOT UPLOAD (USB WEBCAM) ==========def capture_and_upload(cam): """Capture a JPEG from USB webcam and upload it.""" ret, frame = cam.read() if not ret: print("[CAMERA] Capture failed") return None
# Encode as JPEG encode_params = [cv2.IMWRITE_JPEG_QUALITY, 80] success, image_bytes = cv2.imencode(".jpg", frame, encode_params) if not success: print("[CAMERA] JPEG encoding failed") return None
data = image_bytes.tobytes() print(f"[CAMERA] Captured {len(data)} bytes")
if len(data) > 500 * 1024: print("[CAMERA] Image too large, skipping upload") return None
try: resp = requests.post( SNAPSHOT_API, headers={ "Authorization": f"Bearer {ACCESS_TOKEN}", "Content-Type": "image/jpeg", }, data=data, timeout=15, )
if resp.status_code == 200: result = resp.json() if result.get("success"): print(f"[UPLOAD] OK: {result['url']}") return result["url"] else: print(f"[UPLOAD] Failed: HTTP {resp.status_code} - {resp.text}")
except requests.RequestException as e: print(f"[UPLOAD] Error: {e}")
return NoneReplace init_camera() and capture_and_upload() in the main script. The MQTT and main loop code stays the same. Update the finally block to call cam.release() instead of cam.stop().
Running as a systemd Service
Section titled “Running as a systemd Service”Create a service file to run the script on boot:
sudo nano /etc/systemd/system/siliconwit-camera.service[Unit]Description=SiliconWit IO Camera SnapshotsAfter=network-online.targetWants=network-online.target
[Service]Type=simpleUser=piWorkingDirectory=/home/pi/siliconwit-cameraExecStart=/home/pi/siliconwit-camera/venv/bin/python camera_snapshot.pyRestart=alwaysRestartSec=10
[Install]WantedBy=multi-user.targetEnable and start:
sudo systemctl daemon-reloadsudo systemctl enable siliconwit-camerasudo systemctl start siliconwit-cameraCheck status and logs:
sudo systemctl status siliconwit-camerajournalctl -u siliconwit-camera -fTroubleshooting
Section titled “Troubleshooting”| Problem | Cause | Solution |
|---|---|---|
| HTTP 413 Payload Too Large | Image exceeds 500 KB | Lower resolution or JPEG quality. VGA (640x480) at quality 80 is usually under 100 KB. |
| HTTP 429 Too Many Requests | Daily snapshot limit reached | Increase capture interval or upgrade your plan. Free plan allows 10/day. |
| HTTP 401 Unauthorized | Invalid access token | Verify DEVICE_ID and ACCESS_TOKEN from your dashboard. |
| HTTP 403 Forbidden | Device is paused | Resume the device from your dashboard. |
| HTTP 503 Service Unavailable | Storage backend issue | Retry after a few seconds. This is temporary. |
| Blurry images (ESP32-CAM) | Auto-exposure not settled | Discard the first 3-5 frames after camera init before capturing. |
| Blurry images (Pi Camera) | Camera not focused | Adjust the lens focus ring. Add a time.sleep(2) after cam.start(). |
| ESP32-CAM brownout/reboot | Insufficient power | Use a 5V 2A power supply. Do not power from the FTDI 3.3V pin. |
esp_camera_init failed: 0x105 | Camera not detected | Check the ribbon cable is fully seated. Try pressing the camera connector. |
Python ModuleNotFoundError | Missing library | Activate your venv: source venv/bin/activate, then pip install the missing package. |
- Use JPEG, not PNG. JPEG photos are 5-10x smaller. PNG is only better for screenshots or diagrams with flat colors.
- Resize before uploading. 640x480 is plenty for event monitoring and keeps files well under 500 KB.
- Add motion detection. On the ESP32-CAM, compare consecutive frames. On the Pi, use OpenCV’s
absdiff(). Only upload when something changes to conserve your daily quota. - Include sensor data. Add temperature, motion score, or other readings alongside
snapshot_urlin your telemetry payload. This makes each event searchable and alertable. - Adjust capture interval. 60 seconds is a good default. For security cameras, consider event-triggered capture instead of fixed intervals.
Next Steps
Section titled “Next Steps”- Camera Snapshots Reference - API details, plan limits, and data retention
- Topics & Payloads - Full MQTT message format reference
- Dashboard Alerts - Trigger alerts when telemetry values cross thresholds
- Raspberry Pi DHT22 MQTT - Combine camera snapshots with sensor readings