2026年2月15日 星期日

MicroPython 學習筆記 : ESP32-S3 CAM 開發板測試 (八)

在前一篇的 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). 

沒有留言 :