在前一篇的 Generic 韌體測試後得到一個結論 : 放棄 TF 卡吧! 反正 ESP32-S3 CAM 是 N16R8, 有巨量的 Flash 儲存空間, 所以還是回到又新又好用的 cnadler86 的 2026 年 Freenove 專版韌體探索其它應用. 本篇旨在紀錄燒回此版韌體後, 修改串流伺服器程式, 增加一個 "拍照存檔" 按鈕後的測試結果, 其實就是前面筆記 (五) 的後續修正版.
本系列之前的測試文章參考 :
燒錄韌體方法參考前面筆記 (五), 此處不再贅述, 其實就是洗掉重燒兩個指令而已 (此處 UART 端口之 COM 埠號要自行修改) :
esptool --chip esp32s3 --port COM4 erase_flash
esptool --chip esp32s3 --port COM4 write_flash -z 0 firmware.bin
串流伺服器程式修改如下, 主要是增加了一個拍照存檔按鈕, 並且網頁文字改為繁體中文 :
import network
import socket
import camera
import time
import machine
import gc
import os
import errno
# ==========================================
# 0. 全域變數
# ==========================================
cam = None # 用來存放相機物件
# ==========================================
# 1. 設定 Wi-Fi
# ==========================================
SSID = "ASUS-RT-AX3000"
PASSWORD = "a5572056"
print("========================================")
print("📸 Vibe Cam 2026 - with Save Function")
print("========================================")
# ==========================================
# 2. 連線 Wi-Fi
# ==========================================
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print(f"🔗 正在連接 Wi-Fi: {SSID}...", end="")
wlan.connect(SSID, PASSWORD)
retry = 0
while not wlan.isconnected():
time.sleep(1)
print(".", end="")
retry += 1
if retry > 20:
print("\n❌ Wi-Fi 連線逾時!")
return None
ip = wlan.ifconfig()[0]
print(f"\n✅ Wi-Fi 已連線! IP: http://{ip}")
return ip
# ==========================================
# 3. 相機初始化 (保持您原本的寫法)
# ==========================================
def init_camera():
global cam
gc.collect()
try:
# 1. 建立相機物件
cam = camera.Camera()
# Freenove 韌體通常使用無參數 init
try:
cam.init()
except:
cam.init(0)
# 2. 設定參數 (JPEG, VGA)
time.sleep(0.5)
# 嘗試設定參數 (相容不同版本的 Freenove 韌體)
try:
cam.reconfigure(pixel_format=camera.PixelFormat.JPEG,
frame_size=camera.FrameSize.VGA)
except:
# 如果 reconfigure 失敗,嘗試舊版寫法
try:
cam.framesize(camera.FrameSize.VGA)
cam.pixformat(camera.PixelFormat.JPEG)
except: pass
print("📷 相機初始化成功")
return True
except Exception as e:
print(f"⚠️ 相機初始化失敗: {e}")
return False
# ==========================================
# 4. 網頁 HTML (新增存檔按鈕)
# ==========================================
html = """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Vibe Cam 2026</title>
<style>
body { font-family: sans-serif; background: #222; color: #fff; text-align: center; padding: 20px; }
.btn { padding: 15px 30px; border: none; border-radius: 5px; font-size: 1.2rem; cursor: pointer; margin: 10px; color: white; }
.btn-blue { background: #007bff; }
.btn-green { background: #28a745; }
img { width: 100%; max-width: 640px; border: 2px solid #555; background: #000; min-height: 240px; }
</style>
</head>
<body>
<h1>🏠 Vibe Cam 2026</h1>
<img id="stream" src="" alt="等待影像...">
<br>
<button class="btn btn-blue" onclick="toggle()">啟動/停止 串流</button>
<button class="btn btn-green" onclick="savePhoto()">📸 拍照存檔</button>
<div id="status" style="color:#aaa; margin-top:10px;">準備好了</div>
<script>
var active = false;
var img = document.getElementById("stream");
var stat = document.getElementById("status");
function toggle() {
active = !active;
if(active) {
stat.innerText = "串流進行中 ...";
load();
} else {
stat.innerText = "串流已停止";
}
}
// 新增:拍照存檔函式
function savePhoto() {
stat.innerText = "照片儲存到 Flash...";
fetch('/save').then(response => response.text()).then(text => {
alert(text);
stat.innerText = text;
}).catch(error => {
alert("Save Failed!");
stat.innerText = "Error";
});
}
function load() {
if(!active) return;
img.src = "/capture?t=" + new Date().getTime();
img.onload = () => { if(active) load(); };
img.onerror = () => {
stat.innerText = "Error (Retrying...)";
if(active) setTimeout(load, 500);
};
}
</script>
</body>
</html>
"""
# ==========================================
# 5. 主伺服器 (新增 /save 路由)
# ==========================================
def start_server():
global cam
# 先連網
ip = connect_wifi()
if not ip: return
# 再開相機
if not init_camera():
print("❌ 無法啟動伺服器,因為相機失敗")
return
# 建立 Socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', 80))
s.listen(5)
s.settimeout(0.5)
print("✅ 伺服器就緒。請打開瀏覽器,並觀察 Shell 的輸出。")
try:
while True:
try:
conn, addr = s.accept()
conn.settimeout(None)
request = conn.recv(1024).decode()
# --- 路由 1: 取得影像 ---
if "GET /capture" in request:
buf = cam.capture()
if buf and len(buf) > 0:
conn.send(b'HTTP/1.1 200 OK\r\nContent-Type: image/jpeg\r\n')
conn.send(f'Content-Length: {len(buf)}\r\n\r\n'.encode())
conn.send(buf)
else:
conn.send(b'HTTP/1.1 500 Error\r\n\r\n')
# --- 路由 2: 拍照存檔 (新增功能) ---
elif "GET /save" in request:
print("📸 正在存檔...")
buf = cam.capture()
if buf and len(buf) > 0:
# 產生檔名 (使用時間戳記)
filename = f"photo_{time.ticks_ms()}.jpg"
with open(filename, "wb") as f:
f.write(buf)
msg = f"Saved: {filename} ({len(buf)//1024} KB)"
print(f"💾 {msg}")
conn.send(b'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n' + msg.encode())
else:
conn.send(b'HTTP/1.1 500 Error\r\n\r\nCamera Failed')
# --- 路由 3: 自毀程式 (保留您的功能) ---
elif "GET /nuke" in request:
print("☢️ DELETE main.py...")
try: os.remove('main.py')
except: pass
conn.send(b'HTTP/1.1 200 OK\r\n\r\nRebooting...')
time.sleep(1)
machine.reset()
# --- 路由 4: 首頁 ---
else:
conn.send(b'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n')
conn.send(html.encode())
conn.close()
gc.collect()
except OSError as e:
if e.args[0] == 110 or e.args[0] == 116: continue
except KeyboardInterrupt:
print("\n🛑 停止中...")
s.close()
if cam:
try: cam.deinit()
except: pass
print("相機已釋放")
# 啟動!
start_server()
輸出結果如下 :
========================================
📸 Vibe Cam 2026 - with Save Function
========================================
True
🔗 正在連接 Wi-Fi: ASUS-RT-AX3000.....
✅ Wi-Fi 已連線! IP: http://192.168.50.111
📷 相機初始化成功
✅ 伺服器就緒。請打開瀏覽器,並觀察 Shell 的輸出。
44
2216
44
2216
43
25
18212
43
.....
注意, 此程式設有自毀裝置, 當想要刪除串流程式 main.py 時, 可輸入網址後面加 /nuke 即可刪除 main.py, 因為此程式為一個 while 迴圈, 執行中可能無法在 Thonny 的左下角視窗刪除 main.py (會一直 Busy).





















