目標:讓 ESP32-CAM 不只是「會亮畫面」,而是會自動拍、會排程、會存證、不卡關。
亮點:新增 timelapse 排程、事件快照、檔案保留(自動清空舊檔),再給你低延遲調校清單與實測小工具。
檔案結構
/sdcard/DCIM/
├─ 2025-10-06/
│ ├─ 00-15-00.jpg
│ ├─ 00-30-00.jpg
│ └─ event_00-42-12.jpg
└─ 2025-10-07/
└─ ...
Arduino(ESP32)範例:時間戳+當日資料夾
#include "esp_camera.h"
#include "FS.h"
#include "SD_MMC.h"
#include <WiFi.h>
#include <time.h>
const char* ssid="YOUR_SSID";
const char* pass="YOUR_PASS";
const char* ntp = "pool.ntp.org";
const long gmtOffset = 8 * 3600; // 依你時區調整
const int daylightOffset = 0;
void ensureDir(const String& dir){
if(!SD_MMC.exists(dir)) SD_MMC.mkdir(dir);
}
String nowDate(){ // yyyy-mm-dd
time_t t; time(&t); struct tm *tm = localtime(&t);
char buf[16]; strftime(buf, sizeof(buf), "%Y-%m-%d", tm);
return String(buf);
}
String nowTime(){ // HH-MM-SS
time_t t; time(&t); struct tm *tm = localtime(&t);
char buf[16]; strftime(buf, sizeof(buf), "%H-%M-%S", tm);
return String(buf);
}
void saveShot(const String& prefix=""){
camera_fb_t *fb = esp_camera_fb_get();
if(!fb) return;
String dir = "/DCIM/" + nowDate();
ensureDir(dir);
String name = prefix.length() ? (prefix + "_" + nowTime()) : nowTime();
String path = dir + "/" + name + ".jpg";
File f = SD_MMC.open(path, FILE_WRITE);
if(f){ f.write(fb->buf, fb->len); f.close(); }
esp_camera_fb_return(fb);
}
void setup(){
// 1) init camera_config_t + esp_camera_init(...) 略
// 2) SD: 1-bit 模式較穩定
SD_MMC.begin("/sdcard", true);
// 3) Wi-Fi + NTP 校時
WiFi.begin(ssid, pass); while(WiFi.status()!=WL_CONNECTED) delay(200);
configTime(gmtOffset, daylightOffset, ntp);
}
void loop(){
// demo:每 5 分鐘拍一張
static unsigned long t0 = 0;
if(millis() - t0 > 300000UL){
saveShot(); // 00-15-00.jpg 這種檔名
t0 = millis();
}
delay(100);
}
情境
事件快照 + 清理舊檔(擴充上段程式)
#define PIR_PIN 14 // 或門鈴/按鈕 GPIO
const int KEEP_DAYS = 7;
bool olderThanNDays(const String& yyyy_mm_dd, int days){
struct tm tm_now, tm_dir;
time_t tnow; time(&tnow); localtime_r(&tnow, &tm_now);
// 解析 yyyy-mm-dd
tm_dir = tm_now; // copy timezone
int y,m,d; sscanf(yyyy_mm_dd.c_str(), "%d-%d-%d", &y,&m,&d);
tm_dir.tm_year = y-1900; tm_dir.tm_mon = m-1; tm_dir.tm_mday = d;
tm_dir.tm_hour = 0; tm_dir.tm_min = 0; tm_dir.tm_sec = 0;
time_t tdir = mktime(&tm_dir);
return difftime(tnow, tdir) > days*86400L;
}
void cleanOld(){
File root = SD_MMC.open("/DCIM");
File f; while((f = root.openNextFile())){
if(!f.isDirectory()) continue;
String name = String(f.name()); // /DCIM/2025-10-06
String date = name.substring(6); // 2025-10-06
if(olderThanNDays(date, KEEP_DAYS)){
// 刪整個資料夾(遞迴簡化:先刪裡面,再刪它)
File sub = SD_MMC.open(name);
File ff; while((ff = sub.openNextFile())){ SD_MMC.remove(ff.path()); }
SD_MMC.rmdir(name);
}
}
}
void loop(){
// timelapse
static unsigned long t0=0, t1=0;
if(millis()-t0>300000UL){ saveShot(); t0=millis(); } // 每 5 分鐘
if(digitalRead(PIR_PIN)==HIGH){ // 事件快照
saveShot("event"); delay(200);
saveShot("event"); delay(200);
saveShot("event"); delay(500);
}
if(millis()-t1>3600000UL){ cleanOld(); t1=millis(); } // 每小時清理一次
delay(50);
}
最小可用 API(整合到你的 setup()
與 loop()
)
#include <WebServer.h>
WebServer srv(80);
void handleShot(){
camera_fb_t * fb = esp_camera_fb_get();
if(!fb){ srv.send(500,"text/plain","fail"); return; }
srv.sendHeader("Content-Type","image/jpeg");
srv.send_P(200,"image/jpeg",(const char*)fb->buf, fb->len);
esp_camera_fb_return(fb);
}
void handleBurst(){ // 連拍 3 張到 SD
saveShot("burst"); delay(120);
saveShot("burst"); delay(120);
saveShot("burst");
srv.send(200,"text/plain","ok");
}
void setup(){
// ... camera + WiFi + SD + NTP ...
srv.on("/shot", handleShot);
srv.on("/burst", handleBurst);
srv.begin();
}
void loop(){ srv.handleClient(); /* + 你的 timelapse/清理邏輯 */ }
安全(超簡化):把 ESP32-CAM 只放在內網;必要時加一個 query token:/shot?tk=SECRET
。
1) 解析度 / 畫質
2) 網路
3) 電源
Camera init failed
」十之八九是供電。改 5V/1–2A + 短線。4) SD 卡
解析度 vs 延遲(概念)
解析度 端到端延遲(越少越順)
QVGA (320×240) ■■■□□□□ ≈120ms
VGA (640×480) ■■■■□□ ≈200ms
SVGA (800×600) ■■■■■□ ≈320ms
XGA (1024×768) ■■■■■■ ≈480ms
估算帶寬(概念)
解析度 平均 JPEG 大小 10 fps 估計帶寬
QVGA ~25KB ≈ 2.0 Mbps
VGA ~45KB ≈ 3.6 Mbps
SVGA ~70KB ≈ 5.6 Mbps
(依場景與壓縮變動)
Python(OpenCV)量 FPS / 掉幀率
import cv2, time, collections
url = "http://<ESP32-IP>/stream" # 或 /:81/stream
cap = cv2.VideoCapture(url)
win = collections.deque(maxlen=100)
last = time.time(); frames=0
while True:
ok, frame = cap.read()
if not ok: break
frames += 1; win.append(time.time())
if time.time()-last >= 2:
fps = len(win)/(win[-1]-win[0]) if len(win)>1 else 0
print(f"FPS≈{fps:.1f} Total:{frames}")
last = time.time()
cv2.imshow("ESP32-CAM", frame)
if cv2.waitKey(1)==27: break
cap.release(); cv2.destroyAllWindows()
延遲「感覺測」小撇步
在畫面前晃動手指→看螢幕延後感;降解析度 / 亮一點 / 靠 AP,延遲很有感地下降。
想要「畫面有大變化就連拍」?可以做個超輕量判斷:JPEG 檔大小變化 or 連續兩張灰階差值。
下例採「JPEG 大小差」作為入門範例(不精準,但便宜好用)。
size_t last_len = 0;
void loop(){
camera_fb_t *fb = esp_camera_fb_get();
if(!fb) return;
size_t L = fb->len;
// 若大小變化超過 12% 視為有大動作(門口感應等)
bool moving = (last_len>0) && (abs((int)L - (int)last_len) > last_len*0.12);
last_len = L;
if(moving){
// 事件快照:多拍 3 張到 SD
esp_camera_fb_return(fb); // 先釋放,再用 saveShot 真正寫入
saveShot("event"); delay(150);
saveShot("event"); delay(150);
saveShot("event");
}else{
esp_camera_fb_return(fb);
}
delay(100);
}
正統的「畫面差分」要做灰階差 / 背景建模;建議放在 Pi 5(Day 5 會示範),ESP32-CAM 只負責拍拿圖。
在 Pi/PC 上:
# 把當日資料夾打包成影片
cd /path/to/DCIM/2025-10-06
ffmpeg -framerate 12 -pattern_type glob -i "*.jpg" -vf "scale=1280:-1" -y timelapse_2025-10-06.mp4
自動化小心法
cron
每晚 23:55 自動合成;