2026年2月12日 星期四

LG Gram 筆電書籤備份 20260212

LG Gram 筆電的瀏覽器頁籤不知不覺又超過 50 個, 耗掉蠻多記憶體的, 備份存參 :

Chrome : 
Edge :

蝦皮購買 ESP32-S3 CAM 開發板 x 2

因為之前買的三片 ESP32-S3 CAM 板子有兩片有問題 (一個照片不全, 一個 OTG 埠無反應), 所以在蝦皮找到另一個賣家環島科技買兩片來測試, 他們的板子似乎較長且布局與 Freenove 的不同, 價錢也較貴一些 :





明華店自取 860 元. 

MicroPython 學習筆記 : ESP32-S3 CAM 開發板鏡頭慢速測試程式

之前買的兩片 ESP32-S3 CAM 開發板的其中一片拍的照片, 前面看來正常, 但後面卻糊掉或變成馬賽克 (燒錄的韌體與使用的拍照的程式均相同), 甚至將解析度降至 QVGA 狀況依舊. 本來懷疑是鏡頭模組問題, 只要將兩片開發板的 ov2640 交換測試即知, 但鏡頭的排線很密很脆弱, 過度曲折可能造成內部電線斷裂, 連接座的間隙也是每拆卸排線一次就變大, 鏡頭裝上去沒必要就盡量不要拆.  






我把以上不良照片上傳給 Gemini 分析, 它認為鏡頭與感光元件 (CMOS) 都沒有壞, 問題可能出在排線 (Ribbon Cable) 或連接座, 因為如果鏡頭破了或感光元件燒了, 畫面會是全黑, 全白, 或是充滿雜訊點, 絕對不會只壞下面. 相機的排線非常脆弱, 輕微的震動都可能導致 D0-D7 數據線接觸不良, 建議將 ov2640 排線拆下來用橡皮擦清潔接點, 清完後用下列 "龜速診斷模式" 測試 : 

import camera
import time
import gc

# 1. 強制回收記憶體
gc.collect()

print("========================================")
print("🚀 Vibe Cam - 龜速診斷模式")
print("========================================")

try:
    # 2. 關閉舊的相機實體
    try:
        camera.deinit()
    except:
        pass
    time.sleep(1.0) # 讓電容放電

    # 3. 初始化:關鍵參數全開
    print("📷 初始化相機 (設定最保守參數)...")
    
    # 嘗試 Freenove 的標準初始化,但強制覆寫頻率
    try:
        # xclk_freq=10000000 (10MHz) -> 讓時脈變慢一半
        camera.init(0, format=camera.JPEG, framesize=camera.FRAME_QVGA, xclk_freq=10000000)
        print("   -> ✅ 頻率已降至 10MHz")
    except:
        print("   -> ⚠️ 韌體不支援降頻參數,使用預設值")
        camera.init()

    # 4. 畫質設定:最差畫質 (High Compression)
    # quality=63 (這是 OV2640 的極限,畫質最爛,但數據量最小)
    try:
        camera.quality(63)
        print("   -> 📉 畫質設定為 63 (最高壓縮/最小檔案)")
    except:
        pass

    # 5. 暖身 (給它多跳幾下)
    print("🔥 暖身中 (連拍 5 張)...")
    time.sleep(2.0)
    for i in range(5):
        try:
            camera.capture()
            time.sleep(0.2)
        except:
            pass

    # 6. 正式拍照
    print("📸 拍攝診斷照片...")
    gc.collect()
    
    buf = camera.capture()

    if buf and len(buf) > 0:
        print(f"📦 取得影像: {len(buf)} bytes")
        
        # 檢查檔案完整性
        valid = False
        if len(buf) > 2 and buf[0] == 0xFF and buf[1] == 0xD8 and buf[-2] == 0xFF and buf[-1] == 0xD9:
            valid = True
            print("   -> ✅ 標頭尾完整 (傳輸成功)")
        else:
            print(f"   -> ❌ 標頭尾殘缺 (傳輸中斷, 結尾: {hex(buf[-1])})")

        filename = "vibe_slow.jpg"
        with open(filename, "wb") as f:
            f.write(buf)
            
        print(f"💾 已儲存: {filename}")
        print("👉 請打開這張圖。如果連這張 QVGA+畫質最爛的照片都還有撕裂,")
        print("   那就代表鏡頭排線真的斷了。")
    else:
        print("❌ 拍照失敗 (無數據)")

    camera.deinit()

except Exception as e:
    print(f"❌ 錯誤: {e}")

但即使以 10MHz 龜速與 QVGA 超低畫質設定去拍, 問題還是一樣 : 




 
我的零件箱應該還有很久以前採購的 ov2640 模組, 等找出來後再測試看看. 上面 Gemini 提供的慢速測試程式留著備用. 

MicroPython 學習筆記 : ESP32-S3 CAM 開發板 OTG 與 UART 槽的差別

ESP32-S3 CAM 開發板有兩個 Type C USB 槽, 一個是 UART, 一個是 OTG, 最近幾天測試發現測試發現燒錄時要用 UART, 燒錄完需斷電重開機, 然後改插 OTG 槽, 這樣按 Thonny 的 "執行/重新啟動" 才會順利進入 MicroPython 環境, 如果用 UART 會一直顯示 "Wainting for Bootloader", 我問 Gemini 原因, 回覆整理如下 :
  • UART 槽 (COM Port / USB-to-TTL) : 
    它是透過一顆中介晶片 (通常是 CH343/CH340/CP210x) 來連接電腦與 ESP32 :
    電腦 USB訊號 <-> 轉換晶片 <-> ESP32 的 TX/RX 腳位
    此槽特點是硬體強制力強, 連接著 ESP32 的重置電路 (EN pin) 啟動模式腳位 (IO0), 它能透過電路訊號 (DTR/RTS) 強制把 ESP32 踢進下載模式 (Download Mode), 所以燒錄韌體時用它是最穩定的.
  • OTG 槽 (Native USB / USB Serial JTAG) : 
    它是直通 ESP32-S3 內部的 USB 控制器 :
    電腦 USB訊號 <-> ESP32-S3 (沒有中間人)
    它需要等 ESP32 內部的韌體  (如 MicroPython) 跑起來之後才能被電腦辨識, ESP32-S3 MicroPython 韌體預設會把 REPL (互動介面) 和模擬磁碟機指定給這個 OTG 端口. 
當燒錄好支援 ESP32-S3 的 MicroPython 韌體後, 韌體啟動時會把 Python 的輸出視窗 (REPL) 導向到 Native USB (OTG) 端口而不是 UART 端口. 所以插 OTG 的話 Thonny 就能直接透過 USB 協定跟 MicroPython 對話. 如果插 UART 端口, 當在 Thonny 按下 "停止/重啟" 時, Thonny 會試圖透過 USB 送出一個訊號重置開發板, 並透過那顆 USB 中介晶片去控制硬體的 DTR 和 RTS 腳位, 使開發板進入 "等待燒錄模式" 而卡在 "Waiting for Bootloader"; 而在 OTG 槽, 這個重置是 "軟體重置 (Soft Reboot)", 效果與按 Ctrl+D 一樣而進入 MicroPython 互動執行環境. 

2026年2月11日 星期三

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

在前一篇測試中使用 cnadler86 提供的 ESP32-S3 CAM 韌體一直無法拍攝到正常完整的照片, 今天在 GitHub 上找到 shariltumin 的 repo, 發現裡面有 Gemini 強烈建議下載的 2022 年版韌體, 雖然是較舊的 MicroPython 1.20 版, 但能用才是王道, 於是下載來試試. 

本系列之前的測試文章參考 :


雖然 repo 的標題是 2022, 但 2023 年有更新, 我是下載 20230717 的韌體 firmware.bin : 





我將第一片燒錄到 GENERIC 那片板子拿來洗掉重燒 :

D:\ESP32-S3-CAM>esptool --chip esp32s3 --port COM4 erase_flash   
esptool.py v4.6.2
Serial port COM4
Connecting...................
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:dd:d4
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 4.6s
Hard resetting via RTS pin...

燒錄新版韌體 : 

D:\ESP32-S3-CAM>esptool --chip esp32s3 --port COM4 write_flash -z 0 firmware.bin   
esptool.py v4.6.2
Serial port COM4
Connecting........................
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:dd:d4
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash will be erased from 0x00000000 to 0x00151fff...
Compressed 1383856 bytes to 913906...
Wrote 1383856 bytes (913906 compressed) at 0x00000000 in 79.8 seconds (effective 138.7 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

注意, 以上指令都需在下達出現 .... 時按住 BOOT 鍵才會生效. 燒錄完需拔掉 USB 斷電重開, 這樣 在 Thonny 按重新執行才能進入 Bootloader, 首先檢查 Flash 記憶體 : 

MicroPython v1.20.0-206-g33b403dfb-kaki5 on 2023-07-10; ESP32S3-FREENOVE OV2640 w/BLE (KAKI5) with ESP32-S3

>>> import esp   
>>> print(f"Flash 大小: {esp.flash_size() / 1024 / 1024:.2f} MB")    
Flash 大小: 8.00 MB

N16R8 應該有 16MB 才對, 卻只能用到 8MB, 因為作者在製作這個韌體時為了相容性, 將分割表 (Partition Table) 設定為 8MB 之故. 8MB 足夠存幾千張照片或寫幾萬行程式, 剩下的 8MB 雖然看得到吃不到, 但也完全不影響效能. 

接下來檢視 RAM 資訊 : 

>>> import micropython   
>>> micropython.mem_info()   
stack: 736 out of 15360
GC: total: 6408320, used: 12560, free: 6395760
 No. of 1-blocks: 282, 2-blocks: 36, max blk sz: 24, max free sz: 399722

GC 欄顯示有 6.4MB 的巨量 RAM, 與前一篇測試使用之韌體的 64K RAM 相比, 提升了 100 倍! 這對拍照與處理影像資料有如神助, 之前的綠色雜訊, 畫面撕裂, 與緩衝區溢位應該都會消失了. 

先執行下列程式檢視 Flash 記憶體與 camera 模組 :

import gc   
import camera
import micropython

# 分配記憶體與整理
gc.collect()

print("="*30)
print("🚀 N16R8 Vibe Check")
print("="*30)

# 1. 檢查 RAM (關鍵指標)
# N16R8 成功驅動後,這裡應該要顯示 4MB ~ 8MB 之間的數值
free_ram_mb = gc.mem_free() / 1024 / 1024
print(f"✅ 剩餘 RAM: {free_ram_mb:.2f} MB")

if free_ram_mb > 4:
    print("   -> 狀態:完美!Octal PSRAM 已啟用。")
else:
    print("   -> 警告:RAM 過少,可能燒錄成 Non-Octal 版本。")

# 2. 檢查 Camera 模組
try:
    print(f"✅ Camera 模組版本: {camera}")
    print("   -> 驅動載入成功。")
except Exception as e:
    print(f"❌ Camera 模組載入失敗: {e}")

print("="*30)

輸出結果顯示有 camera 模組且 8MB PSRAM 有啟動 :

==============================
🚀 N16R8 Vibe Check
==============================
✅ 剩餘 RAM: 6.11 MB
   -> 狀態:完美!Octal PSRAM 已啟用。
✅ Camera 模組版本: <module 'camera'>
   -> 驅動載入成功。
==============================

接下來執行下列程式拍照 :

import camera
import time
import gc

# 1. 啟動與設定 (使用 Freenove 預設值)
gc.collect()
camera.init() 
time.sleep(2.0) # 給相機一點時間適應光線

# 2. 拍照
buf = camera.capture() # 使用標準指令即可

# 3. 存檔
if buf:
    with open("photo.jpg", "wb") as f:
        f.write(buf)
    print("拍照成功!")

# 4. 關閉
camera.deinit()

執行結果 :

True
28679
拍照成功!
True

這時重新整理 Thonny 左側的開發板資料夾, 會出現一個 photo.jpg 檔, 將其下載到 PC, 點擊開啟圖檔檢視軟體, 已成功拍攝了一張 800X600 的 JPG 照片 : 





可見燒對韌體很重要, 雖然是 2023 年的版本, 且 MicroPython 是較舊的 v1.20, 但可以正常拍照就行. 上面的程式固定將拍攝的照片存成 photo.jpg 會蓋掉原檔案, 下面修正版則會自動編號為 photo_x.jpg :

import camera
import time
import os
import gc

# 1. 記憶體大掃除 (確保拍照順暢)
gc.collect()
print("========================================")
print("🚀 Vibe Cam - 22.0 純淨拍照版")
print(f"🧠 可用記憶體: {gc.mem_free() / 1024:.2f} KB")
print("========================================")

try:
    # 2. 初始化相機
    # Freenove 專用韌體不需要參數,它會自動載入正確設定
    print("📷 初始化相機...")
    camera.init()
    
    # 【關鍵技巧】等待白平衡與自動曝光穩定
    # 剛啟動的前幾秒畫面顏色可能會偏綠或偏暗,給它 2 秒適應光線
    print("🔥 暖身中 (等待光線穩定)...")
    time.sleep(2.0)

    # 3. 拍照
    print("📸 拍攝中...")
    # 使用標準指令,Sharil 韌體會自動處理 JPEG 編碼
    buf = camera.capture()

    if buf and len(buf) > 0:
        print(f"📦 取得影像: {len(buf)} bytes")
        
        # 4. 智慧存檔 (存入內部 Flash)
        # 自動尋找沒用過的檔名,避免覆蓋
        i = 0
        while True:
            filename = f"photo_{i}.jpg"
            try:
                # 嘗試取得檔案資訊,如果檔案存在就不報錯,繼續下一號
                os.stat(filename)
                i += 1
            except OSError:
                # 檔案不存在 (報錯 ENOENT),代表這個名字可以用!
                break
        
        # 寫入檔案
        with open(filename, "wb") as f:
            f.write(buf)
            
        print(f"💾 已儲存至內部空間: {filename}")
        print("🎉 成功!請在 Thonny 左側檔案欄按右鍵 -> Refresh (重新整理) 查看。")

    else:
        print("❌ 拍照失敗 (數據為空)")

    # 5. 釋放資源
    # 養成好習慣,拍完就釋放,避免下次啟動衝突
    camera.deinit()
    print("✅ 相機已釋放")

except Exception as e:
    print(f"❌ 發生錯誤: {e}")
    # 嘗試強制釋放
    try:
        camera.deinit()
    except:
        pass

print("========================================")

輸出結果 :

========================================
🚀 Vibe Cam - 22.0 純淨拍照版
🧠 可用記憶體: 6225.03 KB
========================================
📷 初始化相機...
True
🔥 暖身中 (等待光線穩定)...
📸 拍攝中...
📦 取得影像: 51852 bytes
51852
💾 已儲存至內部空間: photo_0.jpg
🎉 成功!請在 Thonny 左側檔案欄按右鍵 -> Refresh (重新整理) 查看。
True
✅ 相機已釋放
========================================



市圖還書 2 本 : 慢富 & 外匯交易圖表分析入門

本周市圖還書 2 本 :
這兩本書都被預約需還, No.1 河堤有書, 周二去借; No.2 排隊預約. 

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

前一篇測試使用了 cnadler86 的 Generic 的韌體, 裡面沒有 camera 模組無法拍照, 本篇改下載燒錄檔名有 CAM 的韌體 : 





同樣解壓縮 zip 檔後用 esptool 燒錄到 ESP32-S3 CAM 中 (最好先卸除鏡頭) :

D:\ESP32>esptool --port COM5 flash_id   
esptool.py v4.6.2
Serial port COM5
Connecting..................................
Detecting chip type... ESP32-S3
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:e0:38
Uploading stub...
Running stub...
Stub running...
Manufacturer: 68
Device: 4018
Detected flash size: 16MB
Flash type set in eFuse: quad (4 data lines)
Hard resetting via RTS pin...

D:\ESP32>esptool --chip esp32s3 --port COM5 erase_flash   
esptool.py v4.6.2
Serial port COM5
Connecting...................
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:e0:38
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 3.5s
Hard resetting via RTS pin...

D:\ESP32>esptool --chip esp32s3 --port COM5 write_flash -z 0 firmware.bin
esptool.py v4.6.2
Serial port COM5
Connecting..............
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:e0:38
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash will be erased from 0x00000000 to 0x001dbfff...
Compressed 1948208 bytes to 1238683...
Wrote 1948208 bytes (1238683 compressed) at 0x00000000 in 108.5 seconds (effective 143.7 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

燒錄完需拔掉 USB 斷電重插, 這樣 Thonny 的 "執行/重新啟動" 才會 Boot 進去 : 

MicroPython v1.27.0-dirty on 2026-01-12; Generic ESP32S3 module with Octal-SPIRAM with ESP32S3
Type "help()" for more information.
>>> import esp   
>>> print(f"Flash 大小: {esp.flash_size() / 1024 / 1024:.2f} MB")   
Flash 大小: 16.00 MB  

用下列程式檢查記憶體與 camera 模組 : 

import gc   
import camera
import micropython

# 分配記憶體與整理
gc.collect()

print("="*30)
print("🚀 N16R8 Vibe Check")
print("="*30)

# 1. 檢查 RAM (關鍵指標)
# N16R8 成功驅動後,這裡應該要顯示 4MB ~ 8MB 之間的數值
free_ram_mb = gc.mem_free() / 1024 / 1024
print(f"✅ 剩餘 RAM: {free_ram_mb:.2f} MB")

if free_ram_mb > 4:
    print("   -> 狀態:完美!Octal PSRAM 已啟用。")
else:
    print("   -> 警告:RAM 過少,可能燒錄成 Non-Octal 版本。")

# 2. 檢查 Camera 模組
try:
    print(f"✅ Camera 模組版本: {camera}")
    print("   -> 驅動載入成功。")
except Exception as e:
    print(f"❌ Camera 模組載入失敗: {e}")

print("="*30)
==============================
🚀 N16R8 Vibe Check
==============================
✅ 剩餘 RAM: 7.93 MB
   -> 狀態:完美!Octal PSRAM 已啟用。
✅ Camera 模組版本: <module 'camera'>
   -> 驅動載入成功。
==============================

可見此韌體有 camera 模組, 且 8MB 的 PSRAM 可用, 這巨量的 RAM 讓拍照不會 Out of memory. 

但用下列 Gemini 提供的程式拍照卻有馬賽克 :

import camera
import time
import os
import gc

gc.collect()

print("========================================")
print("🚀 Vibe Cam - 最終勝利存檔版")
print("========================================")

cam = None
try:
    # 1. 啟動
    cam = camera.Camera()
    cam.init()
    time.sleep(0.5)

    # 2. 設定 (使用剛剛驗證成功的 Keyword 方式)
    try:
        cam.reconfigure(pixel_format=camera.PixelFormat.JPEG, frame_size=camera.FrameSize.VGA)
        print("✅ 格式設定成功 (JPEG/VGA)")
    except:
        pass # 已經設過了或失敗都沒關係,反正數據看起來是對的

    time.sleep(1.5) # 等待緩衝區穩定

    # 3. 拍照
    print("📸 拍攝中...")
    # 為了清空舊緩衝區,建議空拍一張
    try:
        cam.capture()
        time.sleep(0.1)
    except:
        pass
        
    buf = cam.capture()
    print(f"📦 取得數據: {len(buf)} bytes")

    # 4. 【關鍵】不做任何檢查,直接暴力存檔!
    filename = "vibe_victory.jpg"
    with open(filename, "wb") as f:
        f.write(buf)
        
    print(f"💾 已強制儲存至: {filename}")
    print("🎉 請立刻用 Thonny 下載這張照片,它絕對是好的!")
    
    # 5. 釋放
    cam.deinit()

except Exception as e:
    print(f"❌ 錯誤: {e}")

print("========================================")

輸出結果如下 :

========================================
🚀 Vibe Cam - 最終勝利存檔版
========================================
✅ 格式設定成功 (JPEG/VGA)
📸 拍攝中...
<memoryview>
📦 取得數據: 28189 bytes
28189
💾 已強制儲存至: vibe_victory.jpg
🎉 請立刻用 Thonny 下載這張照片,它絕對是好的!
========================================

按 Thonny 左邊開發板資料夾框右上角的三條槓按鈕, 點選 "重新整理" 就會出現拍攝的照片 JPG 檔, 點選 "下載到 D:\ESP32-S3-CAM" 即可複製此 JPG 到 PC 瀏覽 :





來回試了好幾次都無法得到完整的照片只好放棄了. 


2026-02-12 補充 :

這幾天測試結果顯示, 照片不全可能不是 cnadler86 的韌體問題, 而是鏡頭模組排線問題. 尚待找到庫存的 ov2640 模組進一步測試後才能確認. 

2026年2月10日 星期二

momo 買書 1 本 (AI 時代的 Side Project 全攻略) + 威剛 64GB TF 卡


上次婷婷表妹幫我買的 3 支小米監視器還剩一支待安裝, 需要一片 TF 卡, 上 momo 看到威剛 64GB 特價 260 元, 但僅限購一片, 這樣得付運費, 所以加買一本上回放進購物車的書, 最近有打算退休後接案, 此書剛好合用 : 





合計 805 元, 用掉 momo 幣 133 元, 實付 672 元. 

好站 : codemee 的部落格

今天在測試 ESP32-S3 CAM 開發板時搜尋到 codemee 的部落格 :


作者是旗標的工程師, 其文章具有高度技術性, 所涉獵範圍與我頗多重疊, 故記錄在此有空好好來學習挖寶. 

2026年2月9日 星期一

蝦皮購買 Beston 9V 恆壓充電鋰電池 x2

我在蝦皮又找到一個 9V 充電鋰電池的賣家, 容量較低 (1000mAh), 比較續航力用 : 






明華智取店取付免運 330 元, 2/15 前保證到貨.


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

蝦皮買的 ESP32-CAM 上周到貨, 周末帶回鄉下將接腳焊上, 並把攝影鏡頭 OV2640/OV5640 裝上去, 方法參考之前 ESP32-WROOM-DEV 開發板的筆記 :





然後將 UART 埠連接 PC USB 插槽, 開啟裝置管理員查看 COM 埠編號 (此處為 COM4) : 




開啟命令提示字元視窗, 先查詢 Flash 資訊 :

D:\ESP32>esptool --port COM4 flash_id   
esptool.py v4.6.2
Serial port COM4
Connecting.............
Detecting chip type... ESP32-S3
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:dd:d4
Uploading stub...
Running stub...
Stub running...
Manufacturer: 68
Device: 4018
Detected flash size: 16MB
Flash type set in eFuse: quad (4 data lines)
Hard resetting via RTS pin...

可見 Flash 確實是 16MB. 用下列指令清除 Flash :

D:\ESP32>esptool --chip esp32s3 --port COM4 erase_flash   
esptool.py v4.6.2
Serial port COM4
Connecting..................
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:dd:d4
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 3.6s
Hard resetting via RTS pin...

然後到 cnadler86 的 Camera API (目前社群首推)  repo 庫, 下載有 OCTAL 字眼的 S3 韌體, 這款才能使用到 8MB PSRAM (N16R8 中的 8 是指 PSRAM/SPI RAM). 注意, ESP32 和 ESP32-S3 的 CPU 架構不同 (Xtensa LX6 vs LX7), 韌體完全不通用, ESP32-S3 若燒錄 ESP32, 板子可能會完全無法開機, 就算能開機也抓不到那 8MB 的 PSRAM 記憶體. 





解開下載的 mpy_cam-v1.27.0-ESP32_GENERIC_S3-SPIRAM_OCT.zip 會得到映像檔 firmware.bin. 檔名中的 mpy_cam 表示內建 camera 模組, GENERIC_S3 表示適用於標準 ESP32-S3 開發板, SPIRAM_OCT 代表支援 Octal SPI RAM, 這正是 N16R8 (R8=8MB Octal RAM) 必須要有的驅動. 沒有 OCTAL 的是一般 SPIRAM (Quad) 的版本, 雖然能開機, 但只會抓到 2MB 或更少的記憶體且速度較慢. 

接下來參考之前 ESP32-S3 開發板燒錄韌體指令將此 MicroPython 韌體燒錄到 Flash 中 :


D:\ESP32>esptool --chip esp32s3 --port COM4 write_flash -z 0 firmware.bin    
esptool.py v4.6.2
Serial port COM4
Connecting...................
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
MAC: e0:72:a1:d7:dd:d4
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash will be erased from 0x00000000 to 0x001dbfff...
Compressed 1948272 bytes to 1238576...
Wrote 1948272 bytes (1238576 compressed) at 0x00000000 in 108.1 seconds (effective 144.1 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

燒錄成功後在 Thonny 點選 "執行/停止-重新啟動" 出現 "not in bootloader mode" 訊息 : 

Device is busy or does not respond. Your options:

  - wait until it completes current work;
  - use Ctrl+C to interrupt current work;
  - reset the device and try again;
  - check connection properties;
  - make sure the device has suitable MicroPython / CircuitPython / firmware;
  - make sure the device is not in bootloader mode.

按 RST 鍵 (左鍵) 出現 "waiting for download" : 

ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x1 (POWERON),boot:0x20 (DOWNLOAD(USB/UART0))
waiting for download   

我詢問 Gemini, 它說這個訊息的意思是 "我現在處於下載模式 (Download Mode), 因為有人按著我的 BOOT 鍵 (右鍵), 所以我正在等待電腦傳送新的程式給我", 板子根本沒有試著去讀取剛燒進去的 MicroPython, 而是以為還要繼續燒錄, 所以一直停在門口發呆, 這通常是由硬體原因造成的, 最常見的兇手是鏡頭模組, 相機的某些腳位可能與 Strapping Pins (決定開機模式的腳位) 共用, 當插著 OV2640/OV5640 開機時, 鏡頭內部的電路可能會把板子上的 GPIO 0 拉低, 導致晶片誤以為 BOOT 鍵被按著不放.

拔掉電源拆下鏡頭後重新插入 USB 槽, 果然就順利進入 MicroPython 執行環境了 : 

MPY: soft reboot
MicroPython v1.27.0-dirty on 2026-01-12; Generic ESP32S3 module with Octal-SPIRAM with ESP32S3

Type "help()" for more information.

>>> import esp  
>>> print(f"Flash 大小: {esp.flash_size() / 1024 / 1024:.2f} MB")   
Flash 大小: 16.00 MB

然後用下列程式檢查是否可載入 camera 模組, 以及 PSRAM 是否啟用 :

>>> import gc   
import camera
import micropython

# 分配記憶體與整理
gc.collect()

print("="*30)
print("🚀 N16R8 Vibe Check")
print("="*30)

# 1. 檢查 RAM (關鍵指標)
# N16R8 成功驅動後,這裡應該要顯示 4MB ~ 8MB 之間的數值
free_ram_mb = gc.mem_free() / 1024 / 1024
print(f"✅ 剩餘 RAM: {free_ram_mb:.2f} MB")

if free_ram_mb > 4:
    print("   -> 狀態:完美!Octal PSRAM 已啟用。")
else:
    print("   -> 警告:RAM 過少,可能燒錄成 Non-Octal 版本。")

# 2. 檢查 Camera 模組
try:
    print(f"✅ Camera 模組版本: {camera}")
    print("   -> 驅動載入成功。")
except Exception as e:
    print(f"❌ Camera 模組載入失敗: {e}")

print("="*30)
==============================
🚀 N16R8 Vibe Check
==============================
✅ 剩餘 RAM: 7.93 MB
   -> 狀態:完美!Octal PSRAM 已啟用
✅ Camera 模組版本: <module 'camera'>
   -> 驅動載入成功。
==============================

可見 8MB 的 PSRAM 已成功啟用, 剩餘 PSRAM 為 7.93MB. 

做完上述測試確認可讀到 8MB PSRAM 後拔除 USB, 將鏡頭插回去, 找了一片 8GB TF 卡插入背板的卡槽, 重新插上 USB, 這次居然順利進入 MicroPython 環境, 沒有卡在等待 Bootloader 那邊 (萬一出現此情況, 可以試著多按幾次 RST 鍵, 按一下 RST 等 2 秒再按一下; 或者按住 BOOT 鍵不放, 按一下 RST 鍵然後放開, 繼續按著 BOOT 鍵等待 1-2 秒再放開). 

先用 Gemini 建議的程式測試看看能否讀取 TF 卡 : 

MicroPython v1.27.0-dirty on 2026-01-12; Generic ESP32S3 module with Octal-SPIRAM with ESP32S3

Type "help()" for more information.

>>> import machine
import os

def mount_sd():
    try:
        # 針對 Generic ESP32-S3 CAM,通常使用 SDMMC Slot 1
        # width=1 (1-bit mode) 比較省腳位且常見,若失敗可改 width=4
        # 注意:不同板子的 clk/cmd/d0 腳位可能不同,請查閱您的板子 Schematic
        # 以下是常見的 Freenove/Generic S3 CAM 預設腳位:
        sd = machine.SDCard(slot=1, width=1, 
                            clk=39, cmd=38, d0=40) 
                            # 若上面的腳位報錯,試試不指定腳位: machine.SDCard(slot=1, width=1)
        
        os.mount(sd, "/sd")
        print("✅ SD Card mounted at /sd")
        
        # 檢查容量
        fs_stat = os.statvfs('/sd')
        free_mb = (fs_stat[0] * fs_stat[3]) / 1024 / 1024
        print(f"📦 Free space: {free_mb:.2f} MB")
        return True
        
    except Exception as e:
        print(f"❌ Failed to mount SD: {e}")
        return False

# 測試
mount_sd()
❌ Failed to mount SD: extra keyword arguments given
False

回報給 Gemini, 它說 extra keyword arguments given 意思是 "您給了我一個我不認識的參數名稱", 原因是上面的程式碼中使用了 clk 這個參數名稱, 但在 MicroPython 的標準 machine.SDCard 函式庫中, 時鐘腳位的標準參數名稱通常是 sck (Serial Clock) 而不是 clk, 它建議使用標準 ESP32-S3 的寫法, 將 clk 改為 sck 並明確使用 Pin() 物件, 程式修正如下 :

>>> import machine
import os
from machine import Pin

def mount_sd():
    try:
        # 修正點:
        # 1. clk 改為 sck
        # 2. 建議加上 Pin() 包裝 (比較保險)
        sd = machine.SDCard(slot=1, width=1, 
                            sck=Pin(39), 
                            cmd=Pin(38), 
                            d0=Pin(40))
        
        os.mount(sd, "/sd")
        print("✅ SD Card mounted at /sd")
        
        # 檢查容量
        fs_stat = os.statvfs('/sd')
        free_mb = (fs_stat[0] * fs_stat[3]) / 1024 / 1024
        print(f"📦 Free space: {free_mb:.2f} MB")
        return True
        
    except Exception as e:
        print(f"❌ Failed to mount SD: {e}")
        return False

mount_sd()
❌ Failed to mount SD: extra keyword arguments given
False

結果還是不行, 回報 Gemini, 它說可能是韌體底層把 cmd 和 d0 對應到了 mosi 和 miso 的參數位置 (這是某些 S3 移植版的怪癖), 叫我試試看下面這個版本測試程式 : 

>>> import machine
import os
from machine import Pin

def mount_sd():
    try:
        # 修正點:
        # 1. clk 改為 sck
        # 2. 建議加上 Pin() 包裝 (比較保險)
        sd = machine.SDCard(slot=1, width=1, 
                            sck=Pin(39), 
                            mosi=Pin(38), # 對應 CMD
                            miso=Pin(40)) # 對應 D0
        os.mount(sd, "/sd")
        print("✅ SD Card mounted at /sd")
        
        # 檢查容量
        fs_stat = os.statvfs('/sd')
        free_mb = (fs_stat[0] * fs_stat[3]) / 1024 / 1024
        print(f"📦 Free space: {free_mb:.2f} MB")
        return True
        
    except Exception as e:
        print(f"❌ Failed to mount SD: {e}")
        return False

mount_sd()
❌ Failed to mount SD: invalid config: SDMMC slot with SPI pin arguments
False

錯誤訊息變成 invalid config 了, Gemini 綜合上面測試結果, 認為是軟硬體不匹配問題, 目前燒錄的這版 cnadler86 "Generic" 韌體為了維持通用性鎖住了 SDMMC 的腳位設定功能, 它強制使用 ESP32-S3 的官方預設腳位 (CLK=14, CMD=15, D0=2), 不允許透過 Python 程式碼 (clk=39...) 去修改它. 但這塊開發板 (N16R8 CAM) 的硬體線路是 (CLK=39, CMD=38, D0=40), 軟體想走東但硬體在西所以讀不到卡. 

Gemini 認為我的板子應該是 Freenove ESP32-S3 開發板的仿板, 建議我改燒錄 Freenove 專為 S3 WROOM (N16R8) 設計的韌體才能解決此問題, 還好這塊開發板我買了三塊, 打算用另一塊來燒錄 Freenove 的韌體, 等測試 OK 再洗掉目前這塊重新燒錄. 


2026-02-10 補充 :

此韌體雖然有 camera 模組, 但拍照卻失敗, 檢視開發板的記憶體資訊 : 

>>> import micropython   
>>> micropython.mem_info()  
stack: 736 out of 15360
GC: total: 64000, used: 16752, free: 47248, max new split: 8257536
 No. of 1-blocks: 408, 2-blocks: 48, max blk sz: 32, max free sz: 2941

請 Gemini 解析如下 : 

1. stack: 736 out of 15360 (堆疊記憶體)
意義 : 這是 C 語言層面的堆疊, 用來處理函數呼叫, 遞迴和局部變數.
解讀 : 總容量約 15 KB (15360 bytes), 目前使用 736 bytes.
結論 : 非常健康, 程式邏輯沒有造成深層遞迴或堆疊溢位 (Stack Overflow). 

2. GC: total: 64000, used: 16752, free: 47248 (堆積記憶體 / Heap)
意義 : 這是 MicroPython 用來存放物件 (變數, 字串, 列表, 一般的 Python 物件) 的主要區域. 
解讀 : total: 64000 (約 62.5 KB), 這是一個警訊!對於一般的 ESP32 來說這很正常, 但對於號稱 N16R8 (8MB RAM) 的板子來說這個數字太小了. 這代表 MicroPython 的主堆積 (Main Heap) 只被配置在速度最快的 "內部 SRAM", 而沒有直接把那 8MB 的外掛 RAM (SPIRAM) 納入這個主要的管理池. free: 47248 (約 46 KB), 現在只剩下 46KB 的空間可以用來建立 Python 物件. 這讓相機拍照容易失敗, 一張 VGA (640x480) 的 JPEG 照片通常在 30KB~50KB 之間, 如果要把照片讀入變數 (例如 buf = cam.capture()), 這個變數可能會瞬間佔滿剩下的所有空間甚至塞不下.

3. max new split: 8257536 (最大可分割區塊) : 
意義 : 這是最大的亮點!
解讀 : 8,257,536 bytes (約 7.8 MB) 表示板子確實有 8MB 的 SPIRAM (R8), 而且 MicroPython 抓到了它. 雖然主堆積 (GC total) 只有 64KB, 但 MicroPython 知道旁邊還有一個巨大的 8MB 倉庫, 當需要分配超大記憶體 (例如相機的 Frame Buffer) 時, 系統會嘗試去切這塊 8MB 的蛋糕. 

4. max free sz: 2941 (最大連續可用空間)
意義 : 這代表在那個 64KB 的主堆積中最大的一塊 "連續空地" 有多大. 
解讀 : 2941 bytes (約 2.8 KB) 是致命傷, 雖然總共有 46KB 空閒 (free), 但因為記憶體碎片化 (Fragmentation), 最大的連續空間只有 2.8 KB. 後果是如果在程式中做字串相加或建立一個稍微大一點的陣列 (超過 2.9KB), 程式就會報錯 (MemoryError), 即使總空間還夠. 

從以上記憶體資訊來看, ESP32-S3 硬體是強大的, 有 8MB SPIRAM (max new split 證實了這點), 但軟體配置是保守的, 主堆積被限制在 64KB 內部 RAM, 導致一般 Python 操作很擁擠, 但相機運作時, 相機的驅動 (camera.capture()) 會聰明地使用了那 8MB 的 SPIRAM 來存原始數據, 所以能拍到照片. 但當 Python 嘗試處理這張照片 (例如存檔) 時會遇到瓶頸, 如果操作不當, 試圖把數據搬回狹小的 64KB 主堆積會導致溢位或失敗. 

Gemini 的總評是 : ESP32-S3 CAM 擁有巨大的財富 (8MB PSRAM), 但皮夾 (Heap) 卻很小 (64KB) 而且零錢很亂 (Fragmentation). 解決這個問題需要改變軟體使用記憶體的方式 : 


1. 強制執行垃圾回收 :

在每次耗費記憶體的大動作例如拍照或存檔前, 手動呼叫垃圾車 : 

import gc

# 1. 拍照前清理:確保有最大的連續空間給相機緩衝區
gc.collect() 
buf = cam.capture()

# 2. 存檔前清理:確保寫入檔案時的緩衝區足夠
gc.collect()
with open("image.jpg", "wb") as f:
    f.write(buf)

# 3. 用完馬上丟:切斷變數參照,再次清理
buf = None 
gc.collect()

此處 gc.collect() 會把散落在記憶體裡的碎片整合在一起, 把小的 free 區塊合併成大的區塊以便能塞進更大的資料. 


2. 程式碼優化 :

避免 "中間人" 與字串相加, 不要直接用 print(buf) 印出內容或用 str(buf) 把緩衝區資料 buf  (在 PSRAM) 轉成字串 (在 Heap), 這樣會瞬間吃掉幾十 KB, 正確做法例如 :

# 直接檢查數據長度,不要印出內容
print(f"Size: {len(buf)}") 
# 使用 f-string (Python 3.6+) 效率較好,或預先定義好檔名
filename = f"img_{count}.jpg"


3. 利用 bytearray 佔領 PSRAM :

MicroPython 通常設定小物件放內部 SRAM (速度快但只有 64KB); 大物件放外部 PSRAM (速度慢但有 8MB 大容量), 如果需要緩衝區儲存大資料, 不要一點一點 append, 應該用 bytearray() 直接要一塊大的 (例如 100KB), 強迫 MicroPython 把資料丟到 PSRAM 儲存 (系統會因為內部 Heap 放不下自動把它分配到那一塊 8MB 的 PSRAM 裡), 例如 : 

large_buffer = bytearray(100 * 1024)

事實上 camera.capture() 內部已經是這樣做了 (它回傳的那個 bytes 物件通常就直接指在 PSRAM 上), 但關鍵在於千萬不要把它複製到內部 Heap. 


4. 改用 SPIRAM 專用韌體 : 

如果上面三招都無法解決, 還是經常遇到 MemoryError, 那可能是因為目前使用的 MicroPython 韌體採取了分離式記憶體管理方法 (SRAM 當 Heap, PSRAM 當倉庫), 這時可以改用 "全域 SPIRAM (Generic SPIRAM) " 編譯設定的韌體, 它會直接把那 8MB 的 PSRAM 全部當作 Python 的 Heap 來用. 這種針對記憶體配置的韌體版本檔名中帶有 "SPIRAM", 且描述中有 "Unified" 或 "Board Variant: SPIRAM" (不是 OCT). 

蝦皮購買 9V 充電電池

因為物聯網監控盒實驗需要, 找到蝦皮這賣家的 3700 mWh 的充電鋰電池 : 






明華智取店取付免運 279 元, 2/15 前保證到貨. 

2026 年第 6 周記事

本周仍然忙年前大掃除與裝修, 學研進度則放緩, 還是以學習 vibe coding 為主. 

周五下班前到家樂福買衣櫥與三層櫃, 但周末也沒時間安裝. 周六早上載爸去三聖體檢與拿慢性處方簽, 回程去小漢買三分管與電線, 下午開始動工製作要裝在水圳側門蓮霧樹上的照明燈, 我用以前榮發舅媽裝牲禮的紅色塑膠盤當頂蓋遮雨, 鎖在蓮霧樹上後開始拉線, 連到籃球架支架上的插座, 那是之前安裝路旁攝影機時從房間拉出來的. 裝上 Tuya 小圓智慧插座後, 就可以從手機點亮與關閉照明燈了. 




周日早上開始處理舊豬舍廁所的馬桶水箱漏水問題與更換馬桶蓋, 先放空水箱後擦乾彎管, 然後用膠帶將螺紋纏上 4~5 圈鎖進馬桶, 再用矽膠覆蓋周圍 : 




最後安裝新的馬桶蓋 : 




周日中午載爸去牛將吃牛肉麵, 然後去岳父農舍大理石板茶几載回來, 換掉我家那張舊茶几. 下午將買來已數月的浪板抬上二樓, 修補車庫上方已有漏洞的舊透光板, 順便把修廁所漏水用的矽膠全部打完, 否則過幾天乾掉就太可惜了 : 




由於小舅擴充菜園的小番茄栽植區, 使得原本四處蔓生的草莓快絕跡, 我打算載晒衣場邊搭一個草莓垂直栽培架來復育, 如果用角鐵來搭一支要 270 元, 預估需要 6 支, 費用超過千元成本太高, 打算改搭木架, 施工也方便, 但得過年後才有時間進行. 

離過年剩一周, 周六早上載爸去體檢時順路買了金香紙燭與米酒, 周日早上去全聯已買好拜土地公用的餅乾, 也已打電話給榮發舅媽預定除夕祭祖牲禮一份, 本周只要準備水果, 糖果, 與素麵等即可. 

2026年2月5日 星期四

申請插卡手錶用的 SIM 卡門號

上周在蝦皮訂購了一支插卡手錶 DW100 Pro+ 給爸使用, 周一晚上尾牙後回家途中順路去裕誠智取店取貨, 周二下班直接去自由路中華電信門市申請一張 SIM 卡, 目前最便宜方案是月租 199 元, 有每月 2GB 上網額度, 網內網外 20 分鐘免費, 用爸名字申辦享有 50 元月租優惠 (80 歲以上), 變成月租 149 元 (綁約兩年), 且再加 1GB 流量合計 3GB. 

昨天 (周三) 將手錶帶回家給爸試用, 音量還不錯, 不需要舉手拿近耳邊就聽得到. 可從 Google Play 下載安裝 App, 但安裝 LINE 後申請新帳號卻一直卡在 "我不是機器人" 的驗證頁面, 店家目前也沒有解決辦法, 問 AI 答覆是找一支沒有 LINE 的安卓手機, 插入手錶 SIM 卡安裝 LINE 註冊新帳號後再取出放回手表, 但我手邊沒有這樣的手機只好暫時擱著, 反正主要是方便打電話給爸而已. 此種手錶使用環保鋰電池, 只能使用 1A 以下充電頭, 不可使用快充頭, 且待機續航力大概就是一天左右, 早上帶晚上充電. 


爸今年以來記憶退化很明顯, 明明以前都知道無線話機左接右切, 但仍搞不清楚要按哪個按鈕才是接電話, 經常震鈴後被切斷, 後來透過監視器才發現他拿起電話按了右邊的掛斷鈕. 去年下半年還會打高雄家裡的電話或用手機聯絡人打給我, 現在已不再打電話, 想必是忘記電話號碼與怎麼打, 看來病程正往二級推進, 失智是不可逆的, 即使有門診吃藥, 似乎也擋不住退化的力量. 


2026-02-09 補充 :

此手錶待機 2 天電量大約剩下 30%.  

註冊 mooPub 電子書出版服務

前陣子在夢時代熊老闆的店買了 BOOX 閱讀器後, 開始使用 HyRead 從市圖借閱電子書與雜誌, 在搜尋時發現 HyRead 也可從讀墨 readmoo 購買電子書 : 


讀墨還提供出版電子書的 mooPub 服務 : 


以後退休時間很多, 射手座的人最怕無聊了, 我打算來寫書. 多年前就曾計畫要寫書 (最早是 AutoIt 自動化實戰), 但一直都被忙不完的事與追趕不上的技術學習纏住, 大綱擬好了就是沒空靜下心來好好打字. 先申請吧! 或許審核過後就有動力來寫了. 

先註冊 redmoo 帳號, 然後準備身分證與銀行存摺正反面圖檔, 到 mooPub 服務網站申請出版者帳戶, 填完四個步驟的資料後提交申請 :




需等待 7~10 天的人工審查 : 




上架電子書前可參考 FAQ :


讀墨 mooPub 不收取上架費或年費, 僅收平台服務費 (Readmoo 站內為 EPUB 30% 或 PDF 50%), 但計算給付權利金前會先扣除 5% 營業稅後再扣平台服務費. 

讀墨亦提供代為上架至 Apple Books 與 Google Play 的服務, 但會有通路抽成 (例如 Apple Books 通常抽 30%, Google Play Books 無固定比例, 依市場而定), 扣除抽成後再與讀墨均分. 總結讀墨出版服務權利金計算如下表 :




因此若希望利潤最大化, 製作標準的 EPUB 格式並主力在 Readmoo 站內銷售才是最佳策略 (我以前偏好 PDF, 不喜 EPUB 的說). 權利金採月結制, 但當月需滿 3000 元才會匯款至指定帳戶 (我是設定中國信託民族分行). 若當月未達門檻, 金額會自動累積至下個月直到超過門檻為止. 

PS : 

讀墨不使用固定密碼, 以 Email 登入時會寄一封臨時驗證碼到信箱, 驗證後才能進入平台. 

2026年2月3日 星期二

2026 年第 5 周記事

今年開始的生日快樂假月底是期限, 我周五請了假早上洗高雄的浴廁, 下午回鄉下載爸回診拿藥. 離過年還有兩周, 我上周就開始大掃除了, 先整理客廳前面的小書桌, 丟掉一整箱的舊文件與雜誌, 本周繼續清小書桌旁的四層櫃, 庫房, 但最費力的還是門前路旁蓮霧樹下堆積的枯葉, 我用小推車來回清了二, 三十趟才清完 :




由於小舅說要用枯葉當堆肥, 裝了好幾包放在樹下兩三年了也未曾拿來用, 路旁的鐵門因為被擋住從此無法打開, 以前媽都是在此洗衣服, 我清庫房時找到許多要清洗的袋子, 想說小圳水量豐沛也很清澈, 無奈沒打不開只好從大門繞出去, 於是決定清理這些枯葉與淤積的泥土, 恢復舊觀. 

清庫房時找到媽以前留下的一口大砂鍋, 周日將菜園的高麗菜全採收, 做了一鍋滷高麗菜封, 菁菁聞訊馬上說要, 剛好周日晚上要回高雄家, 就裝了兩大盒給她 :




下周大掃除目標是祖堂, 書房以及舊雞舍庫房, 還有更換舊豬舍廁所的馬桶座. 

2026年1月31日 星期六

樹莓派學習筆記 : 如何用 VNC 從樹莓派傳送檔案到 PC

今天整理鄉下老家客廳門口的櫃子, 打算把放在上面的 Pi 3 移到酒櫥那邊放, 關機前看到下載資料夾下有以前下載的 Anydesk 32 bit 版, 因 Anydesk 似乎不再維護 32 位元的版本, 所以想將此檔案抓回筆電中保存, 我以前都使用 WinSCP 連線來互傳檔案, 其實這可以用 VNC Cloud 桌面的 File Transfer 功能來達成. 

首先按桌面右上方的 VNC 按鈕, 再按 VNC Server 視窗右上角的三條槓按鈕, 點選彈出選單的 File Transfer 選項 :




在 Transfer Files 視窗中, 按左下角的 Send files 鈕, 於彈出視窗點選要抓取的檔案後, 按 OK 即將該檔案傳送到 PC (預設試桌面) :






傳送完畢後在 PC 桌面即可找到該檔案. 

Gemini CLI 學習筆記 : Python 專案開發的標準工作流程

在前一篇測試中, 我們使用 uv 工具建立 Python 專案後, 手動編輯描語境檔 GEMINI.md, 然後啟動 Gemini CLI, 它會自動讀取 GEMINI.md, 確認它所擬定的執行計畫後, 要求它開始撰寫程式. 本篇旨在摘要整理使用 Gemini CLI 開發 Python 專案時的 SOP.

本系列全部測試文章索引參考 :


後續的測試都會使用 uv 工具來建立 Python 專案結構, 關於 uv 參考 :

在 Windows 下使用 Gemini CLI 進行 Vibe Coding 的工作流程如下 :
  1. 生成語境檔 : 使用 Gemini 或其他 AI 生成 GEMINI.md 內容
    提示詞模板 : "我要用 Gemini CLI 開發一個 Python 專案 (使用 uv 工具), 目標是 {}, 技術堆疊是 {}, 請以繁體中文幫我撰寫 GEMINI.md 語境檔." 
  2. 開啟終端機 : 開啟 PowerShell 視窗
  3. 建立地基 : uv init <專案名稱> (建立資料夾與環境)
  4. 進入專案 : cd <專案名稱>
  5. 建立語境檔 :
    • 直接在 PowerShell 輸入 notepad GEMINI.md
    • 系統會問 "找不到檔案, 是否建立?", 選 "是" 
    • 貼上內容後存檔關閉記事本 (notepad 預設存檔格式 UTF-8)
  6. 啟動 AI : 輸入 gemini 進入 Gemini CLI
  7. 載入與確認 (Prompt 優化) :
    輸入 "請讀取當前目錄下的 GEMINI.md, 根據該文件規範, 你了解這個專案的目標與限制了嗎? 請先列出你的實作計畫 (Step-by-step plan), 不需要先寫程式碼"
    (說明:明確指出要讀取的檔案, 並強調 "根據該文件" 以確保 AI 不會忽略它)
  8. 執行專案 : 
    輸入 "計畫沒問題, 請開始執行第一步 (或是 Go ahead)"

如下圖所示 : 




uv init 建立的專案架構中的 pyproject.toml 是給 Python 執行環境看的專案描述檔, 此流程中我們先用 uv init 把地基打好, 然後再用 gemini 把 Gemin CLI 叫進來幹活. 

2026年1月30日 星期五

Gemini CLI 學習筆記 : 郵遞區號查詢小幫手專案 (三)

在前兩篇的查詢 6 碼郵遞區號的測試中, 我們只利用提示詞與 Gemini CLI 對話便能讓其生成程式碼, 透過 API 正確取得給定地址的郵遞區號. 本篇則要改用語境檔 GEMINI.md 來達成, 它是寫程式用的 PRD (專案需求文件檔), 關於語境檔 GEMINI.md 參考 :


以下測試使用 uv 工具來管理專案與 Python 版本, 關於 uv 參考 : 


本系列全部測試文章索引參考 :



首先開啟 PS 視窗, 用 uv init 建立一個名為 postal-helper-proj3 的 Python 專案資料夾 : 

PS D:\gemini> uv init postal-helper-proj3   
Initialized project `postal-helper-proj3` at `D:\gemini\postal-helper-proj3`

切換到專案資料夾檢視 uv 建立的檔案 : 

PS D:\gemini> cd postal-helper-proj3    
PS D:\gemini\postal-helper-proj3> dir   

    目錄: D:\gemini\postal-helper-proj3

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----       2026/1/30  上午 11:13            109 .gitignore
-a----       2026/1/30  上午 11:13              5 .python-version
-a----       2026/1/30  上午 11:13             97 main.py
-a----       2026/1/30  上午 11:13            165 pyproject.toml
-a----       2026/1/30  上午 11:13              0 README.md

然後開啟記事本編輯 GEMINI.md, 輸入如下內容 (可請 Gemini 產生) :

# 專案設定:台灣郵遞區號查詢工具 (Taiwan ZipCode CLI)

## 1. 角色與語氣
- **角色**: Python 腳本開發者 (Python Scripter)。
- **語氣**: 輕鬆、直觀、實用導向。
- **語言**: 繁體中文 (Traditional Chinese)。

## 2. 技術堆疊與工具 (Tech Stack)
- **專案管理**: **uv** (由 Astral 開發的超快 Python 工具)。
  - 安裝套件請用: `uv add <package>`
  - 執行程式請用: `uv run <script.py>`
  - **禁止使用**: 傳統的 `pip install` 或手動建立 `venv` 指令。
- **語言版本**: Python 3.12+
- **關鍵套件**: 
  - `requests` (若需爬蟲或呼叫 API)
  - `beautifulsoup4` (若需解析 HTML)
  - `typer` 或 `argparse` (用於處理 CLI 指令參數)

## 3. 專案目標 (Project Goal)
開發一個簡單的 Command Line Interface (CLI) 工具,功能如下:
1.  **輸入**: 使用者輸入「縣市」+「行政區」 (例如:`高雄市三民區`) 或 「完整地址」。
2.  **處理**: 程式透過網路查詢 (中華郵政/開放資料 API) 或 內建字典檔。
3.  **輸出**: 回傳對應的 **3+3 郵遞區號** (優先) 或 **3 碼地區號**。

## 4. 程式碼規範
- **結構**: 保持簡單,盡量將功能封裝在 `main.py` 模組中。
- **錯誤處理**: 如果使用者輸入的地址不存在或打錯字,請回傳友善的中文提示 (例如:「找不到此地區,請確認輸入格式」),而非 Python Traceback。
- **註解**: 程式碼中請加入簡單的中文註解,說明資料來源或邏輯。

## 5. 互動範例 (Expected Behavior)
使用者在終端機執行:
```bash
uv run main.py "台北市信義區市府路1號"

以 utf-8 編碼存檔後, 在此專案目錄下輸入 gemini 啟動 Gemini CLI : 




可見 Gemini CLI 一啟動就會先讀取 GEMINI.md 的內容作為對話中持續性的系統提示, 我先問它是否經由此語境檔已完全了解專案目標, 並要求它先提供執行計畫再做 :

> 請問你了解此專案了嗎? 請不要開始寫程式,先告訴我你的計劃

它回答如下, 看來並無問題, 輸入 ok, 好的, 或 go ahead 等授權它撰寫程式 : 





程式撰寫完畢要求寫入 main.py :





要求授權用 uv 執行測試 :




測試 "台北市信義區市府路1號" 結果正確, 要求再授權測試 "高雄市三民區" : 




測試成功取回 3 碼郵遞區號, 要求授權刪除虛擬環境中沒用到的套件 : 




完成專案 : 




最後開啟 main.py 將程式碼抄錄如下 : 

import typer
import requests
import json
import sys
from typing import Optional

app = typer.Typer(help="台灣郵遞區號查詢工具 (Taiwan ZipCode CLI)")

# 3碼郵遞區號備援字典 (部分範例,實務上可擴充或改用完整資料檔)
THREE_DIGIT_ZIPCODES = {
    "台北市": {
        "中正區": "100", "大同區": "103", "中山區": "104", "松山區": "105", 
        "大安區": "106", "萬華區": "108", "信義區": "110", "士林區": "111", 
        "北投區": "112", "內湖區": "114", "南港區": "115", "文山區": "116"
    },
    "高雄市": {
        "新興區": "800", "前金區": "801", "苓雅區": "802", "鹽埕區": "803", 
        "鼓山區": "804", "旗津區": "805", "前鎮區": "806", "三民區": "807", 
        "楠梓區": "811", "小港區": "812", "左營區": "813", "仁武區": "814",
        "大社區": "815", "岡山區": "820", "路竹區": "821"
    },
    "台中市": {
        "中區": "400", "東區": "401", "南區": "402", "西區": "403", "北區": "404",
        "北屯區": "406", "西屯區": "407", "南屯區": "408"
    }
}

def query_zip5_api(address: str) -> Optional[str]:
    """
    使用 zip5.5432.tw API 查詢 3+3 郵遞區號
    """
    try:
        # 使用知名的 zip5 服務 (非官方但穩定)
        url = "https://zip5.5432.tw/zip5json.py"
        params = {"adrs": address}
        response = requests.get(url, params=params, timeout=5)
        response.raise_for_status()
        
        # 該 API 回傳格式為 JSON
        data = response.json()
        
        # 檢查是否有回傳 zipcode
        if data and "zipcode" in data and data["zipcode"]:
            return data["zipcode"]
            
    except requests.RequestException:
        # 網路連線錯誤,靜默失敗,交給備援機制
        return None
    except json.JSONDecodeError:
        return None
    
    return None

def lookup_local_3digit(address: str) -> Optional[str]:
    """
    從內建字典查詢 3 碼郵遞區號
    """
    for city, districts in THREE_DIGIT_ZIPCODES.items():
        if city in address:
            for district, code in districts.items():
                if district in address:
                    return code
    return None

@app.command()
def main(
    address: str = typer.Argument(..., help="請輸入完整地址或縣市行政區,例如:'台北市信義區市府路1號' 或 '高雄市三民區'"),
    show_json: bool = typer.Option(False, "--json", "-j", help="以 JSON 格式輸出")
):
    """
    查詢台灣郵遞區號 (優先嘗試 3+3 碼,失敗則回傳 3 碼)
    """
    result_code = None
    source = "未知"

    # 1. 嘗試網路查詢 (3+3)
    print(f"正在查詢 '{address}' 的郵遞區號...", file=sys.stderr)
    result_code = query_zip5_api(address)
    
    if result_code:
        source = "網路查詢 (3+3碼)"
    else:
        # 2. 失敗則嘗試內建字典 (3碼)
        result_code = lookup_local_3digit(address)
        if result_code:
            source = "內建字典 (3碼地區號)"

    # 3. 輸出結果
    if result_code:
        if show_json:
            print(json.dumps({"address": address, "zipcode": result_code, "source": source}, ensure_ascii=False))
        else:
            print(f"--------------------------------")
            print(f"查詢結果: {result_code}")
            print(f"資料來源: {source}")
            print(f"--------------------------------")
    else:
        print(f"❌ 找不到此地區,請確認輸入格式 (例如:縣市 + 行政區 + 路名)。")
        sys.exit(1)

if __name__ == "__main__":
    app()

可見 Gemini CLI 這次採取自己的方式查詢郵遞區號, 如果在 GEMINI.md 中有限制它使用 zip5.5432.tw 的 API 查詢的話, 實作方式應該會跟前面第一篇測試文章的實作結果差不多.