docs: 新增三平台 build troubleshooting 參考
把 M10 這輪三平台 build 踩的雷整理成 troubleshooting 文件,供未來 build 安裝包時參考。 涵蓋 9 條逐項 troubleshooting(統一格式:症狀 → 根因 → 為什麼發生 → 解法 → 如何根治)、快速症狀索引表、5 條橫向教訓、KneronPLUS 2.0.0 vs 3.1.2 版本相容附錄、build 驗收 checklist。 核心教訓:症狀常不指向根因(Error 12 是 input size、pip 秒退是 wheels 多版本、中文亂碼是 stdio code page),macOS 開發環境測不到大量 Windows-only 問題(編碼、DLL 路徑、SDK 版本差異)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f62141a925
commit
9397a4d31f
@ -0,0 +1,371 @@
|
||||
# Build Troubleshooting — visionA-local
|
||||
|
||||
> **這份文件的用途**:記錄 visionA-local 打包安裝檔(macOS / Windows / Linux)時實際踩過的雷,
|
||||
> 讓下次遇到同樣症狀能**快速定位**,而不是重新 debug 一輪。
|
||||
>
|
||||
> **目標讀者**:未來要在 Windows / Linux build 這個專案安裝包的開發者(可能是你自己,也可能是團隊成員)。
|
||||
>
|
||||
> **這不是** build 教學。build 流程本身看 `build-pipeline.md`(Makefile 骨架、vendor 目錄、CI 策略)。
|
||||
> 這份是它的 troubleshooting 補充:**症狀 → 根因 → 為什麼會發生 → 解法 → 如何預防/根治**。
|
||||
>
|
||||
> **紀錄來源**:2026-07 這輪(M10 classification + 特規打包 + 三平台實機 build)踩的雷,
|
||||
> 每條都對到 progress.md 或實際 code 的 `檔案:行號`。若與現況不符,以 code 為準。
|
||||
|
||||
---
|
||||
|
||||
## 快速症狀索引表
|
||||
|
||||
先在這張表找你看到的症狀,跳到對應章節。**症狀常常不指向真正的根因**——這正是這份文件存在的理由。
|
||||
|
||||
| 你看到的症狀 | 可能的雷 | 跳到 |
|
||||
|-------------|---------|------|
|
||||
| 裝置 reset 後 `load_model Error code 24`、停在 `KDP2 Loader` | 開發模式 `dist/scripts/` 缺 `firmware/` | §1 |
|
||||
| 前端顯示「載入模型失敗」、terminal 出現 `darwin_usb.c:584` / SIGABRT | 除錯時 curl 與 UI 搶同一顆 USB | §2 |
|
||||
| Windows build:`go: command not found` / `wails: command not found` / `pnpm not found`(明明裝了)| MSYS2 login shell 重建 PATH | §3 |
|
||||
| Build 後置檢查失敗「預期 2 個 .nef,實際 8 個」 | `copy_bundled_data` 沒清空目標、殘留舊 .nef | §4 |
|
||||
| Windows 推論 `KP_ERROR_INVALID_PARAM_12`(Error 12) | input size 用了使用者亂填的 declared 值 | §5 |
|
||||
| 每次啟動都「Python 相依未通過」但又不重裝,怎樣都好不了 | venv 半套安裝、只檢查 python.exe 存在就跳過 | §6 |
|
||||
| 明明環境是好的,卻被判「Python 相依驗證失敗」擋住啟動 | 健康檢查 probe 沒補 Windows DLL 搜尋路徑 | §7 |
|
||||
| App 一啟動 pip 就秒退 `ResolutionImpossible`(certifi ×3 之類) | vendor wheels 累積多版本 | §8 |
|
||||
| Windows 中文標籤亂碼(「布」→「撣<E3808C>」、log 出現 `<60>`) | Python stdio 綁 cp950 而非 UTF-8 | §9 |
|
||||
|
||||
如果症狀不在表上,先讀 §10 共通教訓——很多雷的「表面症狀」都不指向根因。
|
||||
|
||||
---
|
||||
|
||||
## 環境前置需求(三平台)
|
||||
|
||||
打包前的一次性環境安裝,用 bootstrap 腳本,不要手動裝:
|
||||
|
||||
| 平台 | 腳本 | 裝什麼 |
|
||||
|------|------|--------|
|
||||
| Windows | `scripts/bootstrap-windows.ps1` | winget 裝 git / go / node / pnpm / python / **MSYS2**(提供 bash + make)/ Inno Setup + wails |
|
||||
| Linux | `scripts/bootstrap-linux.sh` | apt 裝 go 1.22.5 / node 20 / pnpm / wails + GTK/WebKit/libusb dev headers |
|
||||
| macOS | (手動)| go / node / pnpm / wails / `brew install create-dmg`(DMG 美化,選用) |
|
||||
|
||||
三平台都需要 `make vendor-sync` 先把第三方二進位(Python runtime / wheels / ffmpeg)下載到 `vendor/`。
|
||||
|
||||
**⚠️ Windows 特別注意**:make 在 Windows 上是透過 `C:\msys64\usr\bin\bash.exe` 執行的(Windows 沒有原生 make),
|
||||
這帶來 §3 的 PATH 坑。詳見該節。
|
||||
|
||||
---
|
||||
|
||||
## 逐條 Troubleshooting
|
||||
|
||||
### §1 開發模式資源不同步 → 裝置 reset 後 load_model Error 24
|
||||
|
||||
**症狀**
|
||||
- 裝置首次連線、reset 後,`load_model` 回 `Error code 24`。
|
||||
- Python bridge 卡在 `KDP2 Loader` 狀態、進不到 `KDP2 Comp`。
|
||||
- **只在手動開發模式(`dist/scripts/`)發生,打包出來的安裝包沒這問題。**
|
||||
|
||||
**根因**
|
||||
- 開發模式下 `dist/scripts/` 需要同時含 **`kneron_bridge.py` + `firmware/` + `drivers/`**,`dist/data/` 需含 `models.json` 與 `.nef`。
|
||||
- 曾經只手動複製了 `kneron_bridge.py`、**漏掉 `firmware/`** → 裝置 reset 後 bridge 重啟時找不到 firmware → 停在 Loader → `load_model Error 24`。
|
||||
|
||||
**為什麼會發生**
|
||||
- 手動同步「就複製那個我改到的檔案」很直覺,但 bridge 執行期會去讀 `firmware/`、`drivers/` 這些不會每次都動、容易被忘記的相依資源。
|
||||
- 打包流程之所以沒事,是因為 Makefile 三平台都用整包搬(`cp -R server/scripts/*` / `find | cp` 整包),不是逐檔挑。
|
||||
|
||||
**解法**
|
||||
- 開發模式同步時,`dist/scripts/` 一律帶齊 `kneron_bridge.py` + `firmware/` + `drivers/`,`dist/data/` 帶齊 `models.json` + `.nef`。不要只複製「這次改到的那個檔」。
|
||||
|
||||
**如何預防 / 根治**
|
||||
- 走打包流程驗證而不是手動同步:`make payload-macos`(progress.md L413 實跑驗證過 firmware/drivers 都在)。
|
||||
- 已寫入 memory:`~/.claude/projects/-Users-jimchen-visionA/memory/project_local_tool_dev_env.md`。
|
||||
- 來源:progress.md L409-414。
|
||||
|
||||
---
|
||||
|
||||
### §2 除錯時 curl 戳 flash 與 UI 搶同一顆 USB → SIGABRT
|
||||
|
||||
**症狀**
|
||||
- 使用者端顯示「載入模型失敗」。
|
||||
- terminal 出現 libusb 的 assert:`darwin_usb.c:584`,程式 SIGABRT 直接掛掉。
|
||||
|
||||
**根因**
|
||||
- 除錯時用 `curl` 觸發 flash(load model),**同時**使用者還在用 UI 操作同一顆裝置 → 兩個行程同時對同一個 USB endpoint 下命令 → libusb 在 macOS 上直接 assert / abort。
|
||||
|
||||
**為什麼會發生**
|
||||
- USB 裝置不是可多路複用的資源。KneronPLUS SDK / libusb 沒有替你做互斥;兩個 client 同時搶就炸。
|
||||
|
||||
**解法 / 預防**
|
||||
- **除錯期間不要碰裝置**——要嘛用 curl 手動測、要嘛用 UI 測,不要兩個同時來。
|
||||
- 這不是 code bug,是除錯操作紀律。記住這個 assert 訊息(`darwin_usb.c:584`),下次看到就知道是雙頭搶 USB,不用往 code 裡挖。
|
||||
- 來源:progress.md L412。
|
||||
|
||||
---
|
||||
|
||||
### §3 bootstrap-windows 的 MSYS2 PATH → Go / wails / node / pnpm 找不到
|
||||
|
||||
**症狀**
|
||||
- Windows build 時,`go` / `wails` / `node` / `pnpm` 明明用 winget 裝好了,跑 make 卻報 `command not found`。
|
||||
|
||||
**根因**
|
||||
- Windows 沒有原生 make,本專案透過 `C:\msys64\usr\bin\bash.exe -l`(**login shell**)來跑 Makefile。
|
||||
- login shell 啟動時會重跑 `/etc/profile`,**把 PATH 整個重建**成 MSYS2 自己的一套,Windows 上用 winget 裝的工具目錄就這樣被洗掉了。
|
||||
- `MSYS2_PATH_TYPE=inherit`(bootstrap 有設,`bootstrap-windows.ps1:74`)**只影響 MSYS2 自己的 shell 啟動器**(`msys2.exe` / `mingw64.exe`);直接呼叫 `bash.exe -l` 時它不生效。
|
||||
|
||||
**為什麼會發生**
|
||||
- `inherit` 的作用範圍與「直接呼叫 bash.exe -l」的實際入口不重疊,是一個很容易誤解的設定。這也是為什麼 Inno Setup 呼叫、Python 呼叫本來就必須各自手動 export PATH。
|
||||
|
||||
**解法**
|
||||
- 跟 Inno Setup 一樣,**明確把工具目錄轉成 MSYS2 路徑格式(`C:\foo` → `/c/foo`)後再 export**,補在 `$PATH` 之前。
|
||||
- 實作見 `bootstrap-windows.ps1:73-113`(註解完整說明)、`:200`(去重合併成單一 export,避免多行 export 互相覆蓋)、`:284-294`。
|
||||
|
||||
**如何預防 / 根治**
|
||||
- 任何要在 MSYS2 `bash.exe -l` 下被找到的工具,都不能依賴 `inherit`,一律顯式 export 轉譯後的 MSYS2 路徑。
|
||||
- 來源:`bootstrap-windows.ps1:73-113`、progress.md L1401(M7 同型坑的前身)。
|
||||
|
||||
---
|
||||
|
||||
### §4 copy_bundled_data 不冪等 → 「預期 2 個,實際 8 個」
|
||||
|
||||
**症狀**
|
||||
- Build 後置檢查失敗:`!! ERROR: 預期 2 個 .nef,實際 8 個 !!`,build 中止。
|
||||
|
||||
**根因**
|
||||
- `payload-windows` / `payload-linux` **刻意不 `rm -rf` 整個 `payload/<os>/`**(因為 `build-server-*` 已先把 binary 放進 `bin/`),所以前次 build 的 `data/` 殘留會留著。
|
||||
- 白名單機制(M5-a、`BUNDLED_NEFS` 只留 2 個 .nef)上線前,舊 build 會把 8 個 .nef 全複製進去。升級到白名單版本後再 build,就變成「新複製 2 個 + 殘留 6 個 = 8 個」,被後置檢查擋下。
|
||||
- `payload-macos` 沒踩到,只是因為它有 `rm -rf payload/darwin`。
|
||||
|
||||
**為什麼這個後置檢查是「對的」**
|
||||
- installer(`installer/windows/visiona-local.iss`)是用 `data\* + recursesubdirs` **整包收**,殘留什麼就出貨什麼。所以「數 .nef 個數不符就 fail」是防止安裝包偷偷多帶不該帶的模型——這個 fail-loud 檢查要保留。
|
||||
|
||||
**解法**
|
||||
- `copy_bundled_data` helper 先**清空整個目標 data 目錄**再複製(步驟 (a)),確保重複執行結果一致、不受既有內容影響。
|
||||
- 為什麼清整包而不是只清 `nef/`:目標目錄內容 100% 由這個 helper 產生(Makefile 中只有這裡寫入 `payload/<os>/data/`),沒有其他來源需要保留;只清 `nef/` 的話,未來從 `server/data/` 移除的其他檔案殘留仍會被 installer 整包打包出貨。清整包才是真正冪等。
|
||||
- 實作 + 完整註解見 `Makefile:39-118`(`define copy_bundled_data`)。
|
||||
|
||||
**如何預防 / 根治**
|
||||
- helper 內建 fail-loud 後置檢查:白名單缺檔直接 fail(`Makefile:104-108`)、複製後驗 `models.json` 存在(`:101-103`)+ .nef 數量 == 白名單長度(`:113-116`)。
|
||||
- **不用 rsync**:Windows CI 跑在 Git Bash(`windows-2022` + `shell: bash`),該環境**沒有 rsync** → 一律用 POSIX `find` / `cp`(`Makefile:71-72`)。
|
||||
- 來源:`Makefile:39-118`、progress.md L112-124。
|
||||
|
||||
---
|
||||
|
||||
### §5 input size 優先序錯 → Windows 推論 Error 12
|
||||
|
||||
**症狀**
|
||||
- Windows 上推論回 `KP_ERROR_INVALID_PARAM_12`(Error 12)。
|
||||
- 同一個模型在 macOS 上正常。
|
||||
|
||||
**根因**
|
||||
- input size 的來源可信度排序,**declared(使用者在上傳表單手填的值)不能排在檔名解析之前**。
|
||||
- 上傳表單的 `inputSize` 欄位長期沒有實際作用,使用者是「隨手填」的(實際案例:填 640×640,模型其實是 224×224)。
|
||||
- KneronPLUS **3.1.2(Windows)不再提供 2.0.0(macOS)的 `shape_onnx` 屬性**,SDK 這層在 Windows 直接落空 → 於是**垃圾 declared 值成為實際採用值**,送進 NPU 得到 Error 12。
|
||||
- 檔名的 `wNNNhNNN` 是模型編譯工具鏈產生的,沒有人為亂填空間,**明確解析出來時比 declared 可信**。
|
||||
|
||||
**為什麼是 Windows 才炸**
|
||||
- 見 §附錄「KneronPLUS 版本相容性」:macOS 用 2.0.0、Windows 用 3.1.2,`shape` 欄位在 3.x 搬進巢狀 union(`TensorDescriptor.tensor_shape_info.data`),舊寫法 `getattr(node, "shape_onnx")` 在 3.1.2 拋 AttributeError 被靜默吃掉 → SDK 來源落空 → 掉到 declared。
|
||||
|
||||
**解法**
|
||||
- input size 可信度排序改為:**SDK(0) → filename(1) → declared(2) → known-model-id(3) → default(4)**。declared 降到第 3。
|
||||
- 常數與完整根因註解見 `kneron_bridge.py:351-371`(`INPUT_SIZE_SOURCE_RANK`)。
|
||||
- 逐軸解析(不再沿用單一純量 `_model_input_size`,因為模型輸入不保證正方形):`kneron_bridge.py:159-193`。
|
||||
- 相容兩版 SDK 的 shape 讀取(先試 3.x 巢狀 `tensor_shape_info`、再退平鋪欄位):`kneron_bridge.py:479-523`。
|
||||
|
||||
**如何預防 / 根治**
|
||||
- 實機驗收時 grep log 印出的 input size source(`kneron_bridge.py:347-349` 說明這是唯一能一眼看出尺寸怎麼來的線索)——尺寸錯掉時 NPU **不一定報錯**,可能只是安靜給錯結果。
|
||||
- ⚠️ 這是 §10「湊巧正確」教訓的實例:舊版檔名猜 224 湊巧對,改成「更可信」的 declared 反而錯。
|
||||
- 來源:`kneron_bridge.py:345-376, 479-523`。
|
||||
|
||||
---
|
||||
|
||||
### §6 venv 半套安裝 → 永久卡住、怎樣都好不了
|
||||
|
||||
**症狀**
|
||||
- 每次啟動都跑「Python 相依驗證未通過」,但又不重裝、也沒有提示,怎樣都好不了。
|
||||
- 只能手動刪掉 `runtime/venv` 才能恢復。
|
||||
|
||||
**根因**
|
||||
- 舊邏輯只檢查 `python.exe`(`venv/Scripts/python.exe` 或 `venv/bin/python3`)**存在**就跳過安裝。
|
||||
- 但「python 執行檔存在」**不等於**「相依裝好了」:pip install 中途失敗(斷網 / 磁碟滿 / 防毒攔截)會留下 venv 與 python.exe 都在、但 `import kp` 失敗的**半套環境**。
|
||||
- 於是每次啟動都「看到 python.exe → 跳過安裝 → 但 import 失敗」,永久卡死且無提示。
|
||||
|
||||
**解法**
|
||||
- venv 已存在的日常啟動路徑改為「驗健康、必要時嘗試修復」:`reuseExistingVenv`(`app.go:1078-1103`)。
|
||||
- venv 在但相依看起來壞了 → **只補裝 wheels、不整個重建 venv**(重建要重解壓 ~100MB tarball,而失敗幾乎都出在 pip 階段):`app.go:1091-1093`。
|
||||
- 修復失敗時記錄 warning 但**不阻斷啟動**(見 §7),改以現有 venv 繼續:`app.go:1096-1101`。
|
||||
|
||||
**如何預防 / 根治**
|
||||
- 狀態檢查要驗「真的能用」而非「檔案在」——這是 §10 的橫向教訓。
|
||||
- 快路徑用標記檔 `venv-ready.txt` 記錄「wheels 已成功裝完 + 當前 wheels 指紋」,指紋不符才真的跑一次 import 驗證(`app.go:1105-1152`)。
|
||||
- 來源:`app.go:995-1103`、progress.md(M4 之後的 fix)。
|
||||
|
||||
---
|
||||
|
||||
### §7 健康檢查阻斷啟動(自造 regression)→ 健康的 venv 被判壞、擋住啟動
|
||||
|
||||
**症狀**
|
||||
- 環境明明是好的(bridge 實際能跑、能掃到裝置),app 卻因「Python 相依驗證失敗」擋住啟動。
|
||||
|
||||
**根因(這是修 §6 時自己造成的 regression)**
|
||||
- §6 的健康檢查 probe 裸跑 `python -c "import kp"`,**沒有補 Windows 的 DLL 搜尋路徑**。
|
||||
- `kp` 在 import 時就會 `ctypes.CDLL` 載入 `kp/lib` 下的 native DLL(libkplus / libusb-1.0 / libwdi + MinGW runtime,共 6 個)。Windows 上這些 DLL 不在預設搜尋路徑、必須先 `add_dll_directory`。
|
||||
- 真正跑 bridge 時 `kl720_driver.go` 的 `startPython()` 與 `platform_windows.go` 的 driver 安裝腳本**有**做這件事,但 probe 沒做 → 健康的環境也 import 失敗 → 被判壞掉 → 擋住啟動。
|
||||
|
||||
**解法**
|
||||
- probe 的環境要盡量貼近「真正跑 bridge 的環境」:`pythonProbeScript` 加一段 preamble,先把 `site-packages/*/lib` 掛進 `PATH` + `os.add_dll_directory` 再 import(`app.go:1182-1205`)。
|
||||
- **更重要的原則:健康檢查絕不可以成為啟動的阻斷點。** 完整理由見 `app.go:999-1016`:
|
||||
1. 這個 probe 會誤判(環境貼合度永遠不可能 100%)。
|
||||
2. 就算真的壞了,讓 bridge 自己回報「掃不到裝置」這類具體錯誤,比在啟動前用一個間接的 probe 攔死更好——kneron_bridge.py 本來就把 import kp 失敗當可降級的情況處理。
|
||||
- 型別設計落地這個原則:`reuseExistingVenv` **沒有 error 回傳值**(`app.go:1078-1081`)——型別本身就保證了「健康檢查不會阻斷啟動」。
|
||||
|
||||
**如何預防 / 根治**
|
||||
- 加任何「啟動前健康檢查」時,先問:檢查失敗會不會擋住啟動?如果會,它就有能力誤殺健康環境。健康檢查應該是「發警告 + 嘗試修復」,不是 gate。
|
||||
- 來源:`app.go:999-1016, 1078-1103, 1154-1205`。
|
||||
|
||||
---
|
||||
|
||||
### §8 wheels 多版本 → pip ResolutionImpossible、app 一啟動就秒退
|
||||
|
||||
**症狀**
|
||||
- App 首次啟動安裝依賴時,pip 在 ~2 秒內 `ResolutionImpossible` 失敗,使用者完全無法啟動。
|
||||
- vendor 目錄裡同一套件有多顆(如 `certifi ×3`、`numpy ×2`、`idna ×2`)。
|
||||
|
||||
**根因**
|
||||
- `vendor-wheels*` 用 `pip download --dest`,而 `pip download` **只「補下載缺的」、不移除舊版**。
|
||||
- upstream 每發一次新版,`vendor/wheels/<os>` 就多留一顆 whl → 實機累積成 16 顆(正常應為 9 顆)。
|
||||
- 這些多版本原封不動被 `payload-*` 複製進安裝包 → app 啟動 `pip install a.whl b.whl ...` 同時收到 `certifi 2026.2.25` 與 `2026.6.17` → 直接 ResolutionImpossible。
|
||||
- **debug 成本放大**:這個 pip 錯誤還被 Makefile 的 Auto 分支吞掉(`|| echo WARN`),導致三輪來回都拿不到線索。
|
||||
|
||||
**解法(兩層都要)**
|
||||
- **源頭**:`clean_wheels_dir` helper 在 `vendor-wheels*` 前先清空並重建 `vendor/wheels/<os>/`,確保冪等、不累積多版本(`Makefile:143-166`,完整事故迴歸註解在 `:121-142`)。
|
||||
- **救援**:app 端 `selectLatestWheelPerPackage` 對每個套件只保留版本最高的一顆再交給 pip(`app.go:1314-1325+`),能救「已出貨」的舊安裝包。
|
||||
- 兩層都要有:源頭杜絕讓新安裝包乾淨、救援讓舊安裝包也能自癒。
|
||||
|
||||
**如何預防 / 根治**
|
||||
- 版本比較用逐段整數比較的簡化 PEP 440(`compareWheelVersions`,`app.go:1282-1312`);套件名依 PEP 503 正規化,讓 `opencv_python_headless` 與 `opencv-python-headless` 視為同套件(`normalizeDistName`,`app.go:1255-1271`)。
|
||||
- **錯誤不要被吞掉**:這條的三輪 debug 成本就是 pip 錯誤被 `|| echo WARN` 吞掉造成的。詳見 §10。
|
||||
- 來源:`Makefile:121-166`、`app.go:1207-1325`、progress.md L135-136。
|
||||
|
||||
---
|
||||
|
||||
### §9 Windows 中文亂碼 → 標籤「布」變「撣<E3808C>」
|
||||
|
||||
**症狀**
|
||||
- Windows(繁中)上 classification 標籤亂碼:「布」變「撣<E3808C>」。
|
||||
- stderr log 的中文與 em dash(U+2014)出現 `<60>`(如 `<60>X SDK did not report`)。
|
||||
|
||||
**根因**
|
||||
- Windows 的 Python 把 stdio 綁到「系統 ANSI code page」而非 UTF-8。繁中 Windows 的 ANSI code page 是 **cp950**。
|
||||
- bridge 的 JSON-RPC 兩個方向**不對稱**:
|
||||
- **Go → Python (stdin)**:Go 的 `encoding/json` **不** escape 非 ASCII,`{"labels":["布"]}` 在 wire 上是 raw UTF-8 `e5 b8 83`。以 cp950 解碼得到「撣」+ U+FFFD(截圖的亂碼);嚴格模式下直接 UnicodeDecodeError 讓整個 bridge 掛掉。
|
||||
- **Python → Go (stdout)**:`json.dumps` 預設 `ensure_ascii=True`,中文被 escape 成 `\uXXXX` 純 ASCII,所以這條「目前剛好沒壞」——但那是隱性依賴,任何人加上 `ensure_ascii=False` 就會壞。
|
||||
- **stderr**:`_log()` 的中文與 em dash 現在就會壞。
|
||||
|
||||
**解法**
|
||||
- `_force_utf8_stdio()` 把 stdin / stdout / stderr **一律** `reconfigure(encoding="utf-8", errors="replace")`(`kneron_bridge.py:22-71`),不理會系統預設編碼。
|
||||
- **在 module import 時就呼叫**(`kneron_bridge.py:71`),不是在 `main()` 裡——必須早於任何 I/O(`main()` 會 `os.dup()` stdout、import 期間的例外也走 stderr)。
|
||||
- 用 `errors="replace"` 而非 `strict`:這是診斷 log 通道與協定通道,遇到極端無效位元組寧可看到一個 U+FFFD 也不要讓整個 bridge 因一行 log 而崩潰(bridge 掛掉 = 裝置失聯,比壞字元嚴重)。
|
||||
|
||||
**如何預防 / 根治**
|
||||
- 把「stdout 是 UTF-8」變成**顯式契約而非巧合**(連目前沒壞的 stdout 方向也一併綁定)。
|
||||
- ⚠️ `PYTHONUTF8` 與 `PYTHONIOENCODING` 管的範圍不同:前者是整個 Python 的 UTF-8 mode(含檔案系統編碼等)、後者只管 stdio 的編碼。這裡用程式內 `reconfigure` 是最直接、不依賴環境變數是否被正確傳入的做法。
|
||||
- 測試釘死:`server/scripts/test_kneron_bridge_encoding.py`。
|
||||
- 來源:`kneron_bridge.py:22-71`。
|
||||
|
||||
---
|
||||
|
||||
## §10 共通教訓(橫向 pattern)
|
||||
|
||||
**這節比逐條 bug 更有價值**——上面 9 個雷裡反覆出現的幾個 pattern,下次寫 code / debug 時記住這些,能少踩一半。
|
||||
|
||||
### 10.1 macOS 開發 ≠ Windows 實機
|
||||
|
||||
好幾個雷都是「macOS 有、Windows 沒有」或「兩邊行為不同」造成的。macOS 上測不到、只有 Windows 實機才會爆的類別:
|
||||
|
||||
| 類別 | macOS | Windows | 踩到的雷 |
|
||||
|------|-------|---------|---------|
|
||||
| stdio 編碼 | 預設 UTF-8 | 預設 cp950(繁中 ANSI code page) | §9 中文亂碼 |
|
||||
| native DLL 搜尋路徑 | dyld(可用 ctypes 絕對路徑預載) | 需 `add_dll_directory`、不在預設路徑 | §7 健康檢查誤殺 |
|
||||
| KneronPLUS SDK 版本 | 2.0.0(有 `shape_onnx`) | 3.1.2(shape 搬進巢狀 union) | §5 input size Error 12 |
|
||||
| shell / make | 原生 bash + make | MSYS2 login shell 重建 PATH | §3 工具找不到 |
|
||||
| 既有環境 | 開發機常有現成工具 | 乾淨機、什麼都要 bootstrap | §3 / §4 |
|
||||
|
||||
**原則**:任何「編碼 / DLL 路徑 / SDK 版本 / code page / shell」相關的東西,**不能只在 macOS 驗**,一定要 Windows 實機或至少想清楚兩邊差異。
|
||||
|
||||
### 10.2 「檔案存在」≠「可用」
|
||||
|
||||
§6(venv 半套安裝)和 §7(健康檢查)都栽在這。
|
||||
- venv 只檢查 `python.exe` 存在就跳過安裝 → 半套環境永久卡死。
|
||||
- 狀態檢查要驗**「真的能用」**(實際 import / 實際跑一次),而非「檔案在」。
|
||||
- 但驗「能用」時要注意 §7 的教訓:驗證環境要貼近真實執行環境,否則會誤殺健康的環境;而且**健康檢查不能是啟動阻斷點**。
|
||||
|
||||
### 10.3 錯誤被吞掉會讓 debug 成本爆炸
|
||||
|
||||
§8(wheels 多版本)的三輪來回,根因是 pip 錯誤被 Makefile 的 `|| echo WARN` 分支吞掉,拿不到真正的錯誤訊息。
|
||||
- 吞錯誤 = 把「一次就能定位」變成「三輪還在猜」。
|
||||
- fail-loud 優於 fail-silent。§4 的後置檢查(數 .nef 個數不符就直接 fail)就是正面教材——它讓「安裝包偷偷多帶模型」在 build 期就爆,而不是等出貨後使用者才發現。
|
||||
- 對照 §5:input size 錯掉時 NPU **不報錯只給錯結果**——這種「靜默失敗」最貴,所以要靠 log 印出 source 當唯一線索。
|
||||
|
||||
### 10.4 「湊巧正確」的行為改動要格外小心
|
||||
|
||||
§5(input size 優先序)是經典案例:
|
||||
- 舊版用檔名猜測,某些模型湊巧猜到 224、剛好對。
|
||||
- 改成「看起來更可信」的 declared(使用者手填值)反而錯——因為使用者是隨手填的。
|
||||
- **教訓**:當你把一個「能動但你覺得不夠嚴謹」的邏輯改成「更正確」的版本時,先確認新來源真的更可信。「更正式的欄位」不代表「更可信的值」——declared 是正式欄位、但值是垃圾。
|
||||
|
||||
### 10.5 兩層防護(源頭 + 救援)
|
||||
|
||||
§8 的解法是兩層都做:`clean_wheels_dir` 從源頭杜絕(新安裝包乾淨)+ `selectLatestWheelPerPackage` 救援(舊安裝包自癒)。
|
||||
- 只做源頭 → 已出貨的舊安裝包救不了。
|
||||
- 只做救援 → 每個新安裝包都帶著髒 vendor 出貨、依賴 app 端補救。
|
||||
- 涉及「已出貨產物」的問題,通常源頭與救援都要有。
|
||||
|
||||
---
|
||||
|
||||
## §附錄 A:KneronPLUS 版本相容性
|
||||
|
||||
三平台的 KneronPLUS wheel 版本**不一致**,這是好幾個雷的隱形根因。寫任何碰 SDK 的 code 時必讀。
|
||||
|
||||
| 平台 | KneronPLUS wheel 版本 | 影響 |
|
||||
|------|----------------------|------|
|
||||
| macOS | 2.0.0 | `TensorDescriptor` 有平鋪的 `shape_onnx` / `shape_npu` 屬性 |
|
||||
| Linux | 2.0.0 | 同上 |
|
||||
| Windows | 3.1.2 | **無** `shape_onnx` 屬性;shape 搬進巢狀 `tensor_shape_info.data`(V1/V2 union);DLL 需求更嚴 |
|
||||
|
||||
**具體差異與注意事項**:
|
||||
|
||||
1. **shape 欄位位置不同**(→ §5 Error 12)
|
||||
- 2.0.0:`node.shape_onnx` / `node.shape_npu` 直接可讀。
|
||||
- 3.1.2:`node.tensor_shape_info.version`(`ModelTensorShapeInformationVersion`)+ `.data`(`TensorShapeInfoV1` 有 `shape_onnx`/`shape_npu`;`TensorShapeInfoV2` 只有 `.shape`,docstring 明寫是 ONNX shape)。
|
||||
- 舊寫法 `getattr(node, "shape_onnx")` 在 3.1.2 拋 AttributeError → 被 except 靜默吃掉 → SDK 這層以為「模型沒帶 shape」,其實是讀錯欄位。
|
||||
- 相容兩版的讀法見 `kneron_bridge.py:479-523`(先試巢狀、再退平鋪)。
|
||||
|
||||
2. **native DLL 需求**(→ §7 健康檢查誤殺)
|
||||
- `import kp` 在 Windows 會載入 `kp/lib` 下的 native DLL(libkplus / libusb-1.0 / libwdi + MinGW runtime,共 6 個),不在預設搜尋路徑 → 需 `add_dll_directory`。
|
||||
- macOS 走 dyld,可用 ctypes 絕對路徑預載(`_preload_kneron_dylibs_macos`,`kneron_bridge.py:74+`)。
|
||||
|
||||
3. **wheel 三平台版本不一致本身是風險**(progress.md M9-6 findings L587)
|
||||
- 若未來要加 KL630/KL730 等新晶片支援,macOS/Linux 的 2.0.0 wheel **沒有**對應 enum,必須先升 wheel。
|
||||
- `update_kdp_firmware_from_files` 在 3.1.2 Python wrapper 中不存在(warrenchen 是 ctypes 直打 .so C symbol),這類 API 差異在跨版本時要逐一確認。
|
||||
|
||||
**原則**:任何讀 SDK 結構(shape / enum / API 簽章)的 code,都要同時對 2.0.0 與 3.1.2 驗,或明確寫成「先試新版結構、失敗再退舊版」的相容寫法。
|
||||
|
||||
---
|
||||
|
||||
## §附錄 B:Build 完驗收 Checklist
|
||||
|
||||
Build 完一個安裝包後,**至少**驗這些(每項標了對應的雷,避免重蹈覆轍):
|
||||
|
||||
- [ ] **.nef 數量正確**:打包版(不是開發模式)確認只帶白名單的 2 個 `.nef`(`FCOS Detection` / `Tiny YOLOv3`),不是 8 個。→ §4
|
||||
(開發模式驗不出來:`server/data/nef/` 8 個都在、過濾器只濾「檔案不存在」,開發模式會顯示 7 個模型。M5 必須用打包版驗、progress.md L206。)
|
||||
- [ ] **models.json 有進安裝包**:啟動後 `/api/models` 回非空、log 印 `Loaded N built-in models`(不是 0)。
|
||||
- [ ] **中文顯示正常**(Windows 繁中機):classification 標籤、log 的中文都不亂碼。→ §9
|
||||
- [ ] **input size source 正確**:實機推論後 grep log 的 input size source,確認不是掉到 declared / default 撿到垃圾值;Windows 上尤其要看。→ §5
|
||||
- [ ] **推論不報 Error 12 / Error 24**:Windows 推論不 Error 12(§5)、裝置 reset 後 load_model 不 Error 24(§1)。
|
||||
- [ ] **Python venv 健康**:全新機器首次啟動能自動裝好 wheels(不 ResolutionImpossible §8);且啟動不被健康檢查誤擋(§7)。
|
||||
- [ ] **driver 綁定**:Windows 上 KneronPLUS native DLL 能被載入(`import kp` 成功、掃得到裝置)。→ §7 / 附錄 A
|
||||
- [ ] **Wails 視窗確認**:**開 app window** 確認主 UI 是 Next.js 而非 splash / installer wizard / 白畫面(progress.md L1602 的歷史教訓:M1 只用瀏覽器連 localhost 驗、沒開 window,讓 wizard 殘留混過 M1-M6)。
|
||||
|
||||
**驗收原則**:不要只驗「server 有回應」——很多雷(§1 / §4 / §5 / §9)在 `/api/health` 200 的情況下照樣存在。要驗到「真的能推論 + 顯示正確」。
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- `build-pipeline.md` — Makefile 骨架、vendor 目錄結構、CI 策略、版本號管理(build 流程本體)。
|
||||
- progress.md(`.autoflow/progress.md`)— M10 各段開發紀錄與踩坑細節的一手來源。
|
||||
- memory:`~/.claude/projects/-Users-jimchen-visionA/memory/project_local_tool_dev_env.md` — 開發模式資源同步坑。
|
||||
Loading…
x
Reference in New Issue
Block a user