Skip to content

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.

  • 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

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) │
│ │ ────────────────────────────►│ │
└─────────────────┘ └──────────────────┘
ComponentDescription
ESP32-CAM AI-ThinkerModule with OV2640 camera (most common variant)
FTDI USB-to-Serial adapter3.3V logic level, for programming (ESP32-CAM has no built-in USB)
Jumper wiresFemale-to-female, for connecting FTDI to ESP32-CAM
5V power supply or USB cableESP32-CAM needs 5V (not 3.3V) for stable operation
SoftwarePurpose
Arduino IDE 2.xWith ESP32 board support installed (setup guide)
PubSubClient libraryMQTT client (install via Library Manager)
ArduinoJson libraryJSON serialization (install via Library Manager)
  1. Go to Dashboard > Devices > Add Device
  2. Set Name to something like “Office Camera”
  3. Set Type to Sensor
  4. Click Create Device and copy the Device ID and Access Token

The ESP32-CAM has no USB port. Use an FTDI adapter to program it:

FTDI PinESP32-CAM PinNotes
GNDGNDCommon ground
VCC (5V)5VPower the module
TXU0RFTDI TX to ESP32-CAM RX
RXU0TFTDI RX to ESP32-CAM TX
-IO0 to GNDConnect IO0 to GND for flashing mode
#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);
}
}
}
  1. In Arduino IDE, select Board: AI Thinker ESP32-CAM
  2. Connect IO0 to GND, then plug in the FTDI adapter
  3. Click Upload
  4. After upload completes, disconnect IO0 from GND and press the RST button
  5. Open Serial Monitor at 115200 baud

Expected output:

=== ESP32-CAM Snapshot Demo ===
Camera initialized
Connecting to WiFi....
Connected - IP: 192.168.1.42
Connecting to MQTT...connected
Captured 24576 bytes
Snapshot uploaded: https://cdn.siliconwit.io/snapshots/.../abc123.jpg
Telemetry sent: {"snapshot_url":"https://cdn.siliconwit.io/snapshots/.../abc123.jpg"}

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

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) │
│ │ ────────────────────────────►│ │
└─────────────────┘ └──────────────────┘
ComponentDescription
Raspberry PiAny model with camera connector or USB port (Pi 3, Pi 4, Pi 5, Pi Zero 2 W)
Camera ModulePi Camera Module v2/v3 (CSI connector) or any USB webcam
MicroSD CardWith Raspberry Pi OS installed
Power SupplyAppropriate for your Pi model
WiFi or EthernetInternet connection
SoftwarePurpose
Raspberry Pi OSBookworm or later
Python 3.8+Pre-installed on Raspberry Pi OS
SiliconWit IO AccountFree IoT platform account
Terminal window
mkdir ~/siliconwit-camera
cd ~/siliconwit-camera
python3 -m venv venv
source venv/bin/activate

For Pi Camera Module (CSI):

Terminal window
pip install requests paho-mqtt picamera2

For USB webcam:

Terminal window
pip install requests paho-mqtt opencv-python-headless
LibraryPurpose
requestsHTTP client for uploading snapshots
paho-mqttMQTT client for telemetry
picamera2Raspberry Pi camera interface
opencv-python-headlessUSB webcam capture (alternative to picamera2)

Create a file called camera_snapshot.py:

import io
import ssl
import json
import time
import requests
import 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 = 8883
PUBLISH_TOPIC = f"d/{DEVICE_ID}/t"
CAPTURE_INTERVAL = 60 # seconds between snapshots
IMAGE_WIDTH = 640
IMAGE_HEIGHT = 480
JPEG_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:

Terminal window
python camera_snapshot.py

Expected 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"}

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 None

Replace 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().

Create a service file to run the script on boot:

Terminal window
sudo nano /etc/systemd/system/siliconwit-camera.service
[Unit]
Description=SiliconWit IO Camera Snapshots
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/siliconwit-camera
ExecStart=/home/pi/siliconwit-camera/venv/bin/python camera_snapshot.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

Enable and start:

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable siliconwit-camera
sudo systemctl start siliconwit-camera

Check status and logs:

Terminal window
sudo systemctl status siliconwit-camera
journalctl -u siliconwit-camera -f

ProblemCauseSolution
HTTP 413 Payload Too LargeImage exceeds 500 KBLower resolution or JPEG quality. VGA (640x480) at quality 80 is usually under 100 KB.
HTTP 429 Too Many RequestsDaily snapshot limit reachedIncrease capture interval or upgrade your plan. Free plan allows 10/day.
HTTP 401 UnauthorizedInvalid access tokenVerify DEVICE_ID and ACCESS_TOKEN from your dashboard.
HTTP 403 ForbiddenDevice is pausedResume the device from your dashboard.
HTTP 503 Service UnavailableStorage backend issueRetry after a few seconds. This is temporary.
Blurry images (ESP32-CAM)Auto-exposure not settledDiscard the first 3-5 frames after camera init before capturing.
Blurry images (Pi Camera)Camera not focusedAdjust the lens focus ring. Add a time.sleep(2) after cam.start().
ESP32-CAM brownout/rebootInsufficient powerUse a 5V 2A power supply. Do not power from the FTDI 3.3V pin.
esp_camera_init failed: 0x105Camera not detectedCheck the ribbon cable is fully seated. Try pressing the camera connector.
Python ModuleNotFoundErrorMissing libraryActivate 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_url in 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.