今天的主題是「會通報、會上雲、別亂刷通知」。
我們把 ESP32-CAM 做成一台有 API的相機,接上 MQTT 當事件匯流排,再用 LINE Notify(或 Telegram)把「有人靠近」的瞬間送到你手機。
全文可直接貼部落格(純 Markdown),附 ASCII 圖/表,在任何平台都能顯示。
/shot
、/burst
、/status
、/led?on=1
esp32cam/<id>/event
、.../health
、.../snapshot
)setup()
/ loop()
)#include <WiFi.h>
#include <WebServer.h>
#include "esp_camera.h"
WebServer srv(80);
void handleShot(){ // 回傳 JPEG
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
for (int i=0;i<3;i++){ /* 呼叫你 Day3 的 saveShot("burst") */ delay(120); }
srv.send(200, "text/plain", "ok");
}
void handleStatus(){ // 回 JSON
String json = "{\"model\":\"ESP32-CAM\",\"uptime_ms\":"+String(millis())+"}";
srv.send(200, "application/json", json);
}
void handleLed(){ // /led?on=1
bool on = (srv.hasArg("on") && srv.arg("on")=="1");
digitalWrite(4, on ? HIGH : LOW); // 多數板子 LED=GPIO4
srv.send(200, "text/plain", on?"LED ON":"LED OFF");
}
void setup(){
// Wi-Fi 與 camera_config_t 初始化(略)
pinMode(4, OUTPUT); // LED
srv.on("/shot", handleShot);
srv.on("/burst", handleBurst);
srv.on("/status", handleStatus);
srv.on("/led", handleLed);
srv.begin();
}
void loop(){ srv.handleClient(); }
curl -o test.jpg http://<ESP32-IP>/shot
curl "http://<ESP32-IP>/led?on=1"
curl http://<ESP32-IP>/status
API 封裝建議(命名 & 回應)
路徑 | 方法 | 回應 | 用途 |
---|---|---|---|
/shot |
GET | image/jpeg |
拍一張回傳 |
/burst |
POST | text/plain |
連拍數張到 SD/雲端 |
/status |
GET | application/json |
心跳、韌體版本、PSRAM 狀態 |
/led?on=1 |
GET | text/plain |
簡易光源控制 |
[ESP32-CAM] -- publish --> [MQTT Broker( mosquitto )] <-- subscribe -- [Raspberry Pi 5]
| ↑ ├─ LINE/Telegram 推播
└─ HTTP /shot <--- Pi 5 -----┘ └─ 雲端/硬碟 存證
# /srv/mqtt/docker-compose.yml
services:
mosquitto:
image: eclipse-mosquitto:2
container_name: mqtt
restart: unless-stopped
ports:
- "1883:1883"
volumes:
- ./conf:/mosquitto/config
- ./data:/mosquitto/data
- ./log:/mosquitto/log
./conf/mosquitto.conf
(最簡)
listener 1883
allow_anonymous true
persistence true
之後可改帳密、TLS;內網先跑起來最重要。
#include <PubSubClient.h>
#include <WiFi.h>
WiFiClient wifi;
PubSubClient mqtt(wifi);
const char* BROKER="192.168.1.50";
const char* ID="cam-porch";
void mqttSend(const char* topic, const String& payload, bool retain=false){
mqtt.publish(topic, payload.c_str(), retain);
}
void setup(){
// Wi-Fi init...
mqtt.setServer(BROKER, 1883);
mqtt.connect(ID);
mqttSend("esp32cam/cam-porch/health", "{\"online\":true}", true); // retained
}
void loop(){
if(!mqtt.connected()) mqtt.connect(ID);
mqtt.loop();
// 例:每 60 秒送一次心跳
static unsigned long t0=0;
if(millis()-t0>60000){
mqttSend("esp32cam/cam-porch/health", "{\"online\":true,\"ts\":"+String(millis())+"}", true);
t0=millis();
}
// 偵測到事件就送通知(Day3 的觸發條件)
// mqttSend("esp32cam/cam-porch/event", "{\"type\":\"motion\"}", false);
}
sudo apt install -y python3-pip
pip3 install paho-mqtt requests opencv-python
# save as notifier.py
import cv2, requests, time, io
import paho.mqtt.client as mqtt
ESP_SHOT = "http://<ESP32-IP>/shot"
LINE_TOKEN = "你的_LINE_TOKEN"
BROKER = "192.168.1.50"
TOPIC_EVENT = "esp32cam/cam-porch/event"
def line_notify(msg, img_bytes=None):
h={"Authorization":f"Bearer {LINE_TOKEN}"}
d={"message":msg}
f={"imageFile":("shot.jpg", img_bytes, "image/jpeg")} if img_bytes else None
requests.post("https://notify-api.line.me/api/notify", headers=h, data=d, files=f)
def grab_jpeg():
# 方式1:requests 直接抓(快速)
r = requests.get(ESP_SHOT, timeout=6)
r.raise_for_status()
return r.content
cooldown=20
last_sent=0
def on_msg(client, userdata, msg):
global last_sent
if time.time() - last_sent < cooldown: return
try:
jpg = grab_jpeg()
line_notify("門口可能有人(來自 ESP32-CAM)", jpg)
last_sent = time.time()
except Exception as e:
print("notify failed:", e)
mqttc = mqtt.Client()
mqttc.on_message = on_msg
mqttc.connect(BROKER, 1883, 60)
mqttc.subscribe(TOPIC_EVENT, qos=1)
mqttc.loop_forever()
冷卻/夜間模式(反垃圾通知)
ACTIVE_HOURS=(23,6)
def in_active_hours(h0,h1):
now=time.localtime().tm_hour
return (now>=h0) or (now<=h1)
# 在 on_msg 裡
if not in_active_hours(*ACTIVE_HOURS): return
1) 事件通知流程
[ESP32-CAM 偵測] → publish {motion} → [MQTT]
→ [Pi 5] → GET /shot → LINE 通知 + 存證
2) 健康檢查(LWT/retained)
[ESP32-CAM 上線] → publish retained "online:true"
[ESP32-CAM 斷線] → Broker 發 LWT "online:false" → Pi 5 接到 → Slack/LINE 報警
可靠度
.../health
(retained),Pi 5 訂閱做儀表板安全
/shot?tk=...
(至少擋掉隨手亂掃)事件次數(每小時)
時段 | 次數 | 迷你圖
00-03 | 1 | ▁
03-06 | 0 |
06-09 | 4 | ▂▂▂▂
09-12 | 7 | ▃▃▃▃▃▃▃
12-15 | 12 | ████████████
15-18 | 9 | █████████
18-21 | 15 | ███████████████
21-24 | 5 | █████
管道成功率(近 24h)
抓圖(/shot)成功率:96%
LINE API 成功率:98%
MQTT 重連次數:1
平均冷卻間隔:20 s
/shot
加基本認證、TLS。esp32cam/frontdoor
、.../backyard
、.../garage
,Pi 5 同時訂閱與分流存檔。