Compare commits

..

No commits in common. "69b25f89e20fcb19d2fa4c94816da2121332b6ce" and "17371ca2ed10c2bb121d188f907a508f62350236" have entirely different histories.

13 changed files with 19 additions and 3185 deletions

View File

@ -1,173 +0,0 @@
# 專案健檢報告 — KNEO Academy
## 基本資訊
- **專案名稱**KNEO AcademyInnovedus AI Playground
- **版本**v2.0
- **程式碼來源**:本地路徑 `C:\Users\sungs\Documents\abin\KNEO-Academy`
- **主要語言**Python 3.12
- **最後更新時間**2026-04-04
- **Git branch**main
---
## 技術堆疊
| 層級 | 技術 | 版本 |
|------|------|------|
| GUI 框架 | PyQt5 | 5.15.11 |
| 電腦視覺 | OpenCV | 4.10.0.84 |
| AI 推論 SDK | KneronPLUS | 3.1.2 |
| AI 推論(通用) | PyTorch, TensorFlow, ONNX Runtime | 最新 |
| 音訊 | librosa, sounddevice | — |
| 打包 | PyInstaller | 6.12.0 |
| 加密 | PyArmor | — |
---
## 專案結構概覽
```
KNEO-Academy/
├── main.py # Entry pointAppControllerQStackedWidget 管理頁面)
├── src/
│ ├── config.py # 全域常數、路徑、顏色、DeviceType enum
│ ├── controllers/
│ │ ├── device_controller.py # Kneron dongle 掃描、連接、韌體上傳
│ │ ├── inference_controller.py # 推論工具選擇、model 載入、queue 管理
│ │ └── media_controller.py # 相機、影片捕捉
│ ├── models/
│ │ ├── inference_worker.py # 標準推論 worker thread動態載入 script.py
│ │ ├── custom_inference_worker.py # 自定義模型推論 workerYOLOv5 後處理)
│ │ └── video_thread.py # 相機影像擷取 thread
│ ├── views/
│ │ ├── mainWindows.py # 主應用視窗(推論結果顯示、工具選擇)
│ │ ├── selection_screen.py # 首頁選擇畫面
│ │ ├── login_screen.py # 工具程式登入頁
│ │ └── utilities_screen.py # 裝置管理工具頁
│ ├── services/
│ │ ├── device_service.py # 掃描 Kneron 裝置(含 timeout 機制)
│ │ ├── file_service.py # 檔案上傳服務
│ │ └── script_service.py # 推論腳本執行服務
│ └── utils/
│ ├── config_utils.py # 設定工具
│ └── image_utils.py # QImage ↔ NumPy 轉換
├── uxui/ # 靜態 UI 資源PNG、SVG、GIF
├── dist/
│ └── test.iss # Inno Setup 安裝包設定
├── flowchart.md # 裝置連接流程設計圖Mermaid
└── env.txt # pip 套件清單
```
**資料目錄(執行期,存在 %LOCALAPPDATA%/Kneron_Academy/**
```
uploads/ # 使用者上傳的圖片/影片
utils/
config.json # 全域 plugin 設定
{mode}/{model}/
script.py # 推論腳本
config.json # 模型設定
*.nef # 模型檔
firmware/
{device}/
fw_scpu.bin
fw_ncpu.bin
```
---
## 應用頁面流程
```
SelectionScreen首頁
├── → LoginScreen → UtilitiesScreen裝置管理工具需登入
└── → MainWindowDemo AI App直接進入
```
---
## 主要功能清單
| 功能 | 描述 | 狀態 |
|------|------|------|
| 頁面路由 | QStackedWidget 管理多頁面切換 | ✅ 完成 |
| 裝置掃描 | 掃描連接的 Kneron dongleKL520 / KL720 | ✅ 完成 |
| 裝置連接 | 連接裝置並上傳 firmware | ✅ 完成 |
| 裝置中斷 | 安全中斷連接 | ✅ 完成 |
| Video 推論 | 相機即時推論QThread + queue | ✅ 完成 |
| Image 推論 | 上傳圖片單次推論 | ✅ 完成 |
| 動態 Script 載入 | 從 utils/ 目錄動態 import script.py | ✅ 完成 |
| 自定義模型推論 | 上傳 .nef + firmware使用 YOLOv5 後處理 | ✅ 完成 |
| Plugin 系統 | 透過 config.json 定義 mode/model 結構 | ✅ 完成 |
| APP 打包 | PyInstaller 打包 + Inno Setup 安裝包 | ✅ 有設定 |
| 登入驗證 | Server 驗證(詳細流程見 flowchart.md | 🔄 設計中(有 flowchart |
| Dongle 授權管理 | KN 號碼查詢、授權卡驗證 | 🔄 設計中(有 flowchart |
---
## 文件完整度
| 文件類型 | 狀態 | 位置 | 備註 |
|---------|------|------|------|
| README | ✅ 有 | `README.md` | 完整,含安裝、架構、功能說明 |
| 流程圖 | ⚠️ 部分 | `flowchart.md` | 僅有裝置連接/授權流程,缺主 App 完整流程 |
| PRD / 需求文件 | ❌ 無 | — | 無正式產品需求文件 |
| 架構設計文件 | ❌ 無 | — | 無正式架構文件 |
| API 文件 | ❌ 無 | — | 無script.py 介面未有規格文件) |
| 設計稿 | ⚠️ 部分 | `uxui/` | 有 UI 資源圖,無 Wireframe 或設計規格 |
| TDD技術設計文件 | ❌ 無 | — | 無 |
| 測試文件 | ❌ 無 | — | README 提到 tests/ 目錄,但實際不存在 |
| 部署文件 | ⚠️ 部分 | `dist/test.iss` | 有 Inno Setup 設定,無完整部署指南 |
---
## 程式碼健康度
| 項目 | 狀態 | 備註 |
|------|------|------|
| 測試覆蓋率 | ❌ 無測試 | tests/ 目錄不存在 |
| 程式碼組織 | ✅ 良好 | MVC 架構清晰,職責分明 |
| Docstring 完整度 | ✅ 良好 | 主要類別和方法均有完整 docstring |
| 錯誤處理 | ⚠️ 部分 | 多處使用 try/except但部分只 print 不處理 |
| Thread 安全 | ⚠️ 待確認 | Queue 管理有,但 UI 更新路徑需確認 |
| 技術債 | ⚠️ 少量 | debug print 語句散落在 controller 中 |
---
## 基礎設施
| 項目 | 狀態 | 備註 |
|------|------|------|
| Docker | ❌ 無 | 桌面應用,暫不需要 |
| CI/CD | ❌ 無 | 無自動化建置/測試流程 |
| 打包 | ✅ 有 | PyInstaller + Inno Setup |
| 加密 | ⚠️ 計畫中 | PyArmor 已列在計畫中 |
| 監控 | ❌ 無 | 無 |
---
## 目前正在修改的檔案git status
以下檔案有未提交的修改:
- `src/controllers/device_controller.py`
- `src/controllers/inference_controller.py`
- `src/models/custom_inference_worker.py`
- `src/models/video_thread.py`
- `src/services/device_service.py`
- `src/views/utilities_screen.py`
---
## 缺失項目摘要
**高優先:**
- PRD / 需求文件(產品功能邊界不清晰)
- 測試(完全沒有自動化測試)
**中優先:**
- 架構文件 / TDDplugin script.py 介面規格未文件化)
- 完整流程圖(主 App 推論流程尚未有流程圖)
**低優先:**
- 部署完整指南
- 設計規格文件Wireframe + Design Tokens

View File

@ -1,625 +0,0 @@
# KNEO AcademyInnovedus AI Playgroundv2.0 — 產品需求文件PRD
> **文件性質說明**:本 PRD 為從既有程式碼與文件反向整理而成,已對照 README.md、flowchart.md、主要 controller、view 和 service 原始碼推導。標注「⚠️ 待確認」的項目為推斷內容,請產品負責人核實。
---
## 1. 產品概述
### 1.1 產品定位
**KNEO Academy**(對外名稱:**Innovedus AI Playground**)是一款 Windows 桌面應用程式,讓擁有 Kneron NPU USB Dongle 硬體的客戶能夠在本機端執行 AI 推論,無需雲端服務、無需自行撰寫程式碼。
**核心價值主張**
- 即插即用的 Edge AI 體驗——插上 Kneron dongle 即可執行 AI 模型
- 支援多種推論模式(即時攝影機、圖片上傳、自定義模型)
- 透過 Plugin 系統讓第三方或 Kneron 官方模型能被彈性載入
- 提供硬體管理工具(韌體更新、驅動安裝、裝置授權)
### 1.2 目標使用者
**主要使用者Persona A企業/研發客戶**
- 角色:使用 Kneron NPU 硬體進行 AI PoC 或產品評估的工程師、研究人員
- 目標:快速驗證 Kneron NPU 在特定任務上的推論效果
- 技術素養:中等(懂 AI 概念,不一定熟悉 SDK 整合)
- 痛點Kneron SDK 入門門檻高,想要一個能快速展示能力的工具
**次要使用者Persona B內部 Demo / 業務展示**
- 角色Kneron / Innovedus 業務或技術支援人員
- 目標:向潛在客戶展示 Kneron NPU 的 AI 推論能力
- 技術素養:低至中等
- 痛點:需要一個無需開發、開箱即用的展示工具
**⚠️ 待確認**:上述 Persona 定義是從產品功能推斷。實際目標客群請與產品負責人確認。
### 1.3 應用名稱對照
| 名稱 | 用途 |
|------|------|
| KNEO Academy | 專案內部/開發用名稱 |
| Innovedus AI Playground | 對外正式名稱(`APP_NAME` 常數定義) |
---
## 2. 支援的硬體裝置
應用程式目前支援以下 Kneron NPU Dongle 型號(來自 `config.py``DeviceType` Enum
| 裝置型號 | Product IDHex | 支援狀態 |
|---------|-----------------|---------|
| KL520 | 0x100 | ✅ 已支援(有 icon、model map |
| KL720 | 0x720 | ✅ 已支援(有 icon、model map |
| KL720_L | 0x200 | ⚠️ Enum 有定義,但無 DongleModelMap 對應,**可能不完整** |
| KL530 | — | ⚠️ Enum 有定義,但無 DongleModelMap 對應 |
| KL832 | — | ⚠️ Enum 有定義,但無 DongleModelMap 對應 |
| KL730 | — | ⚠️ Enum 有定義,但無 DongleModelMap 對應 |
| KL630 | — | ⚠️ Enum 有定義,但無 DongleModelMap 對應 |
| KL540 | — | ⚠️ Enum 有定義,但無 DongleModelMap 對應 |
**⚠️ 待確認**KL520 和 KL720 以外的裝置是否已在計畫中支援?`DongleModelMap` 僅有這兩型。
---
## 3. 應用程式頁面架構
### 3.1 頁面流程圖
```
應用程式啟動
SelectionScreen首頁 / 入口選擇畫面)
├── [Demo App 按鈕] ──────────────────→ MainWindowAI Demo 推論主視窗)
└── [Utilities 按鈕] → LoginScreen登入畫面
├── [登入成功] → UtilitiesScreen裝置管理工具
└── [返回] → SelectionScreen
```
### 3.2 各頁面功能摘要
| 頁面 | Class 名稱 | 主要用途 | 需要登入 |
|------|-----------|---------|---------|
| 首頁 | `SelectionScreen` | 選擇進入 Demo App 或 Utilities 工具 | 否 |
| 登入畫面 | `LoginScreen` | Server 驗證,作為進入 Utilities 的門禁 | — |
| 裝置管理工具 | `UtilitiesScreen` | 裝置掃描、韌體更新、驅動安裝、已購項目 | 是 |
| AI Demo 主視窗 | `MainWindow` | AI 推論執行、攝影機顯示、工具選擇 | 否 |
---
## 4. 功能需求
### 4.1 SelectionScreen首頁選擇畫面
**功能描述**:應用程式的入口,提供兩個主要路徑。
**功能清單**
- 顯示應用程式 Logo 與名稱
- 「Demo App」按鈕直接進入 AI 推論主視窗(不需登入)
- 「Utilities」按鈕跳轉到登入畫面登入後才能進入裝置管理工具
**⚠️ 待確認**:首頁是否有其他 UI 元素(介紹文字、版本號等)?
---
### 4.2 LoginScreen登入畫面
**功能描述**:透過 Server 驗證機制保護 Utilities 功能的存取。
**功能清單**
- 帳號密碼輸入欄位
- 登入按鈕 → 觸發 Server 驗證
- 返回按鈕 → 回到 SelectionScreen
**登入結果處理**
| 情境 | 行為 |
|------|------|
| 驗證成功 | 發出 `login_success` signal → 進入 UtilitiesScreen |
| 帳密錯誤 | 顯示「無效用戶名或密碼」提示 |
| 多次錯誤 | Server 停止服務,顯示提示訊息 |
| 返回 | 發出 `back_to_selection` signal → 回到 SelectionScreen |
**⚠️ 待確認**
- 登入 Server 的端點 URL 在哪裡設定?(程式碼中未見明確設定)
- 是否有記住密碼/Token 快取機制?
---
### 4.3 UtilitiesScreen裝置管理工具頁面
**功能描述**:登入後才能存取的裝置管理介面,包含兩個子頁面。
#### 4.3.1 Utilities 子頁面(裝置管理)
**功能清單**
**裝置掃描與列表**
- 掃描當前連接的 Kneron dongle 裝置(呼叫 `check_available_device()`,含 5 秒 timeout
- 以表格(`QTableWidget`顯示裝置清單欄位包含裝置型號、KN Number、狀態
- 「Refresh」按鈕重新掃描裝置
- 未偵測到裝置時顯示提示
**裝置連接操作**
- 選擇裝置後可執行連接(透過 `DeviceController.connect_device()`
- 連接過程:
1. 驗證 firmware 檔案是否存在(`%LOCALAPPDATA%/Kneron_Academy/firmware/{device}/`
2. 呼叫 `kp.core.connect_devices()`
3. 上傳 firmware`fw_scpu.bin` + `fw_ncpu.bin`
- 中斷連接按鈕(`DeviceController.disconnect_device()`
**Firmware 管理**
- 顯示目前 Firmware 版本
- 偵測到版本需要更新時,提示使用者是否更新
- 支援從本機載入 firmware 檔案進行更新
**Driver 管理**(⚠️ 設計中,尚未完整實作)
- 偵測 Kneron USB driver 是否已安裝
- 尚未安裝時:提示安裝,詢問使用者是否執行安裝程序
- 安裝失敗處理:顯示錯誤訊息
**Dongle 授權管理**(⚠️ 設計中,尚未完整實作)
- 查詢 KN NumberDongle 唯一識別號)
- 授權卡Authorization Card驗證流程
- 已授權裝置:顯示已授權提示
- 未授權裝置:詢問是否啟動授權,執行授權流程
- **詳細流程見 `flowchart.md`**
**狀態顯示**
- `QProgressBar`:顯示操作進度(連接、韌體上傳等)
- `QLabel`status_label顯示目前狀態訊息
#### 4.3.2 Purchased Items 子頁面(已購項目)
**功能描述**:顯示用戶已購買的 AI 模型,可以下載到本機。
**功能清單**
- 表格顯示已購買項目欄位Select勾選、Product產品名、Model模型名、Current Version版本、Compatible Dongles相容裝置
- 多選Checkbox功能
- 「Refresh Items」按鈕重新從 Server 取得已購列表
- 「Download Selected」按鈕下載所選項目到本機
**⚠️ 待確認**
- 目前程式碼中有 `populate_mock_purchased_items()` 方法,顯示此功能使用 Mock 資料,**尚未對接真實 API**
- 下載後的模型存放路徑?(推斷為 `%LOCALAPPDATA%/Kneron_Academy/utils/`
- 購買平台/Store 是否已確定?
---
### 4.4 MainWindowAI Demo 推論主視窗)
**功能描述**:核心的 AI 推論展示視窗,整合攝影機顯示、工具選擇與推論結果呈現。
#### 4.4.1 畫面布局組件
| 組件 | Class 名稱 | 功能 |
|------|-----------|------|
| 攝影機顯示區 | `CanvasArea` | 顯示攝影機畫面和推論結果Bounding Box 等) |
| 裝置清單 | `DeviceList` | 顯示可連接的 dongle讓使用者選擇 |
| 裝置連接彈窗 | `DevicePopup` | 彈出式裝置連接確認介面 |
| 媒體控制面板 | `MediaPanel` | 攝影機開啟/暫停、圖片上傳按鈕 |
| AI 工具箱 | `Toolbox` | 顯示可選擇的 AI 模型(來自 config.json |
| 自定義模型上傳 | `CustomModelBlock` | 上傳自定義 .nef 模型的 UI |
#### 4.4.2 AI 推論模式
##### 模式一Video 模式(即時攝影機推論)
**流程**
1. 使用者選擇 Video 類型的 AI 工具
2. 呼叫 `MediaController.start_camera()` 啟動相機
3. `VideoThread` 持續擷取影像幀640×480, 30fps轉為 QImage 發出 signal
4. 影像幀轉為 NumPy 陣列後,透過 `InferenceController.add_frame_to_queue()` 放入推論 queue最大容量5 幀)
5. `InferenceWorkerThread` 從 queue 取出影像幀,呼叫對應的 `script.py` 執行推論
6. 推論結果透過 `inference_result_signal` 傳回主視窗
7. 主視窗在 `CanvasArea` 上繪製 Bounding Box 等視覺化結果
**效能優化機制**
- MSEMean Squared Error幀差異偵測若連續幀的 MSE 低於 `mse_threshold=500`,沿用上一次推論結果(避免重複計算靜止畫面)
- 最短推論間隔:`min_interval=2` 秒(標準模式)
- 模型超時設定:`MODEL_TIMEOUT = 5000` ms`config.py`
##### 模式二Image 模式(上傳圖片單次推論)
**流程**
1. 使用者選擇 Image 類型的 AI 工具
2. 使用者透過 `MediaPanel` 上傳圖片(存入 `%LOCALAPPDATA%/Kneron_Academy/uploads/`
3. 使用 `cv2.imread()` 讀取圖片,放入推論 queue`once_mode=True`,只推論一次)
4. `InferenceWorkerThread` 執行推論並回傳結果
5. 切換到 Image 模式時,相機信號會暫時斷開(非停止),以避免 Video 模式的幀繼續進入 queue
**注意**:切回 Video 工具時,相機信號會重新連接,無需重新開啟相機。
##### 模式三Custom Model使用者自定義模型
**流程**
1. 使用者透過 `CustomModelBlock` 上傳以下三個檔案:
- `.nef` 模型檔
- `fw_scpu.bin`SCPU firmware
- `fw_ncpu.bin`NCPU firmware
- 可選自定義標籤清單class labels
2. `InferenceController.select_custom_tool()` 被呼叫
3. 啟動 `CustomInferenceWorkerThread`(與標準 worker 不同的實作)
4. Custom worker 會在首次推論時自行完成:
- 連接裝置(`kp.core.connect_devices()`
- 上傳 firmware
- 上傳模型(`kp.core.load_model_from_file()`
5. 預處理:影像縮放至 640×640轉為 BGR565 格式
6. 後處理:使用 YOLOv5 演算法NMS threshold=0.5)解析輸出
7. 若有提供自定義標籤,使用自定義標籤;否則使用預設 COCO 80 類別
**Custom Model 預設行為**
- 輸入類型Video即時攝影機
- 後處理演算法:固定使用 YOLOv5**⚠️ 待確認**:是否計畫支援其他後處理演算法?)
- 預設偵測閾值:`thresh=0.2`
- 推論間隔:`min_interval=0.5` 秒(比標準模式更頻繁)
#### 4.4.3 裝置相容性檢查
當使用者選擇 AI 工具時:
- 讀取 `config.json` 中的 `compatible_devices` 欄位
- 比對目前選中的 dongle 型號
- 若不相容,彈出警告對話框並阻止推論啟動
---
### 4.5 Plugin 系統Script & Model 配置)
**功能描述**:透過設定檔驅動的 Plugin 架構,讓 AI 模型能被動態載入,無需修改應用程式原始碼。
#### 4.5.1 檔案結構
```
%LOCALAPPDATA%/Kneron_Academy/
├── uploads/ # 使用者上傳的圖片/影片
├── utils/
│ ├── config.json # 全域 plugin 設定(必要)
│ └── {mode}/ # 推論模式資料夾e.g., face_recognition
│ └── {model}/ # 模型資料夾e.g., face_detection
│ ├── config.json # 模型設定(必要)
│ ├── script.py # 推論腳本(必要)
│ └── *.nef # 模型檔(必要)
└── firmware/
└── {device}/ # 裝置型號資料夾e.g., KL520
├── fw_scpu.bin
└── fw_ncpu.bin
```
#### 4.5.2 全域 config.json 格式
```json
{
"plugins": [
{
"mode": "face_recognition",
"display_name": "人臉辨識",
"models": [
{
"name": "face_detection",
"display_name": "人臉偵測 (ResNet-18)",
"description": "基於ResNet-18的高精度人臉偵測",
"compatible_devices": ["KL520", "KL720"]
}
]
}
]
}
```
#### 4.5.3 模型 config.json 格式
```json
{
"display_name": "人臉偵測 (ResNet-18)",
"description": "使用ResNet-18架構的高精度人臉偵測模型",
"model_file": "face_detection.nef",
"input_info": {
"type": "video",
"supported_formats": ["mp4", "avi", "webm"]
},
"input_parameters": {
"threshold": 0.75,
"max_faces": 10,
"tracking": true
},
"compatible_devices": ["KL520", "KL720"]
}
```
**`input_info.type` 的有效值**
- `"video"` — 啟動攝影機進行即時推論
- `"image"` — 使用者上傳圖片進行單次推論
- `"voice"` — 音訊輸入模式(⚠️ **程式碼中有對應處理路徑但未見完整實作**
#### 4.5.4 script.py 介面規範
`InferenceWorkerThread` 會動態 import 各模型的 `script.py`
**⚠️ 待確認(重要)**`script.py` 的標準介面(函數名稱、參數格式、回傳格式)目前無正式文件。
`InferenceWorkerThread` 的呼叫方式推斷:
- `script.py` 應提供一個可被 worker 呼叫的推論函數
- 接收 `input_params` 字典(包含 `usb_port_id``device_group``model``scpu_path``ncpu_path``file_path` 等)
- 回傳 Bounding Box 結果,格式為:
```json
{
"num_boxes": 2,
"bounding boxes": [[x1, y1, x2, y2], [x3, y3, x4, y4]],
"results": ["label1", "label2"]
}
```
---
## 5. 非功能需求
### 5.1 效能需求
| 項目 | 規格 | 來源 |
|------|------|------|
| 推論 Queue 大小 | 最大 5 幀 | `InferenceController.__init__` |
| 標準模式最短推論間隔 | 2 秒 | `InferenceController.select_tool()` |
| Custom Model 最短推論間隔 | 0.5 秒 | `InferenceController.select_custom_tool()` |
| 模型推論超時 | 5,000 ms | `config.py MODEL_TIMEOUT` |
| 裝置掃描超時 | 5 秒 | `device_service.check_available_device()` |
| 攝影機開啟超時 | 5 秒 / 最多 3 次嘗試 | `VideoThread._camera_timeout` |
| 攝影機解析度 | 640×480 | `VideoThread.run()` |
| 攝影機目標幀率 | 30 fps | `VideoThread.run()` |
| Custom Model 輸入大小 | 640×640 | `custom_inference_worker.preprocess_frame()` |
| 模型推論設備超時Custom | 5,000 ms | `CustomInferenceWorkerThread.initialize_device()` |
### 5.2 平台相容性
| 項目 | 規格 |
|------|------|
| 作業系統 | Windows使用 `%LOCALAPPDATA%`PowerInstaller 打包) |
| Python 版本 | Python 3.12 |
| 視窗尺寸 | 1200×900 像素(固定,`WINDOW_SIZE` 常數) |
**⚠️ 待確認**:是否計畫支援 macOS 或 Linux目前路徑處理和打包設定為 Windows 專屬。
### 5.3 應用程式打包
| 項目 | 工具 | 狀態 |
|------|------|------|
| 打包工具 | PyInstaller 6.12.0 | ✅ 已設定 |
| 安裝包製作 | Inno Setup`dist/test.iss` | ✅ 有設定檔 |
| 程式碼加密 | PyArmor | ⚠️ 計畫中 |
**打包注意事項**
- 需要包含 KneronPLUS SDK 的 `kp/lib` 資料夾
- 需包含 `uxui/` 資源目錄和 `src/` 目錄
### 5.4 資料儲存
所有執行期資料存放於 `%LOCALAPPDATA%/Kneron_Academy/`
- **uploads/**:使用者上傳的圖片/影片
- **utils/**Plugin 設定與模型檔案
- **firmware/**:裝置韌體檔案
**⚠️ 待確認**:安裝包是否會預先建立這些目錄,或由應用程式首次執行時自動建立?
---
## 6. 裝置連接與授權流程(詳細)
根據 `flowchart.md` 的設計(⚠️ 此流程尚未完整實作於程式碼中,為設計規格):
```
登入畫面
Server 驗證(帳密驗證)
├── 成功 → 進入 Dongle 模組畫面
└── 失敗 → 顯示錯誤(帳密錯誤 / 服務被暫停)
Dongle 模組畫面
連接 Dongle
├── 未偵測到 → 顯示「未偵測到裝置」提示
└── 偵測到 → 檢查 Driver 是否安裝
Driver 檢查
├── 已安裝 → 檢查 FW 版本
└── 未安裝 → 詢問是否安裝 Driver
├── 使用者同意 → 執行安裝(可能失敗)
└── 失敗 → 顯示錯誤
FW 版本檢查
├── 版本符合 → 取得 Dongle KN Number
└── 需要更新 → 詢問是否更新 FW
├── 使用者同意 → 下載並安裝 FW
└── 失敗 → 顯示錯誤
取得 KN Number
└── 成功 → 檢查授權狀態
授權狀態檢查
├── 已授權 → 顯示已授權提示 → 回到 App 主頁
└── 未授權 → 詢問是否啟動授權
├── 使用者同意 → 執行授權流程
│ ├── 成功 → 顯示成功提示 → 回到主頁
│ └── 失敗 → 顯示失敗提示
└── 使用者不重試 → 顯示提示 → 回到主頁
```
---
## 7. 已知限制與未完成功能
以下為從程式碼分析發現的限制與尚未完成的功能:
### 7.1 尚未完整實作
| 功能 | 狀態 | 說明 |
|------|------|------|
| 登入 Server 驗證 | 🔄 部分實作 | `LoginScreen` 存在,但 Server 端點設定不明確 |
| Driver 自動安裝 | 🔄 設計中 | `flowchart.md` 有設計,程式碼中未見完整實作 |
| Dongle 授權管理 | 🔄 設計中 | `flowchart.md` 有設計,程式碼中未見完整實作 |
| 音訊voice推論模式 | 🔄 設計中 | `config.py` 有引入 `librosa``sounddevice`,但推論路徑不完整 |
| Purchased Items API 對接 | 🔄 Mock 資料 | `utilities_screen.py``populate_mock_purchased_items()` 方法,使用假資料 |
| 程式碼加密PyArmor | ⏳ 計畫中 | 僅列在 `env.txt`,尚未見到具體加密流程 |
| KL520/KL720 以外裝置支援 | ⏳ 計畫中 | `DeviceType` enum 有定義,但 `DongleModelMap` 僅有這兩型 |
### 7.2 已知技術問題
| 問題 | 嚴重度 | 說明 |
|------|--------|------|
| Debug print 語句殘留 | 低 | 多個 controller 中有大量 `print()` debug 訊息 |
| 錯誤處理不完整 | 中 | 部分 try/except 只 print 不處理(如 `device_controller.py` |
| 測試覆蓋率為零 | 高 | 完全沒有自動化測試 |
| UI 更新 Thread 安全性 | 中 | 待確認推論結果回傳到主線程的路徑是否完全使用 Qt Signal |
| `custom_inference_worker.py` 中引用未導入的 `kp` | 中 | `_boxes_scale()``post_process_yolo_v5()` 在模組頂層使用 `kp.HwPreProcInfo` 等型別,但 `import kp` 在函數內才執行 |
### 7.3 設計假設(未驗證)
- Custom Model 後處理固定使用 YOLOv5未提供自定義後處理的擴充機制
- 應用程式假設攝影機 index 為 0第一個相機無法選擇其他相機
- 應用程式固定使用 Windows 路徑格式,不支援跨平台
---
## 8. 功能優先級RICE 分析)
> ⚠️ 以下 RICE 分數為從現有程式碼功能狀態推斷Effort 值基於程式碼完成度估算。**請與產品負責人及開發團隊確認 Reach、Impact 和 Confidence 的實際數值。**
| 功能 | Reach | Impact | Confidence | Effort人天 | RICE 分數 | 階段 |
|------|-------|--------|------------|--------------|----------|------|
| Video 模式推論 | 100% | 3 | 90% | — | 高 | ✅ 已完成 |
| Image 模式推論 | 90% | 2 | 90% | — | 高 | ✅ 已完成 |
| Plugin 系統config.json | 100% | 3 | 85% | — | 高 | ✅ 已完成 |
| Custom Model 推論 | 60% | 3 | 80% | — | 中高 | ✅ 已完成 |
| 裝置掃描 / 連接 | 100% | 3 | 90% | — | 高 | ✅ 已完成 |
| 登入驗證(完整實作) | 100% | 3 | 70% | 5 | 高 | 🔄 進行中 |
| Dongle 授權管理 | 100% | 3 | 65% | 10 | 高 | 🔄 設計中 |
| Purchased Items API 對接 | 80% | 2 | 70% | 7 | 中 | 🔄 設計中 |
| Driver 自動安裝 | 70% | 2 | 60% | 5 | 中 | 🔄 設計中 |
| 音訊推論模式 | 40% | 2 | 50% | 10 | 低中 | ⏳ 待實作 |
| 多攝影機選擇 | 30% | 1 | 70% | 3 | 低 | ⏳ 未排期 |
| 跨平台支援macOS/Linux | 50% | 2 | 50% | 20 | 中 | ⏳ 未排期 |
---
## 9. 輸入資料格式規範
### 9.1 攝影機影像
- 擷取方式OpenCV `cv2.VideoCapture`DirectShow backendWindows 優先)
- 解析度640×480
- 格式:`QImage.Format_RGB888`(透過 `VideoThread` 發出),後轉為 NumPy 陣列 `(height, width, 3)`,通道順序 RGB888
### 9.2 上傳圖片
- 讀取方式:`cv2.imread()`
- 格式NumPy 陣列BGR 格式OpenCV 預設)
- 儲存位置:`%LOCALAPPDATA%/Kneron_Academy/uploads/`
### 9.3 Custom Model 輸入(預處理後)
- 目標大小640×640
- 格式BGR565`cv2.COLOR_BGR2BGR565`
### 9.4 input_params 字典格式
`InferenceWorkerThread` 接收的 `input_params` 標準結構:
```python
{
"usb_port_id": 32, # Dongle USB port ID
"device_group": <kp.DeviceGroup>, # 已連接的裝置群組物件
"scpu_path": "...\\firmware\\KL520\\fw_scpu.bin",
"ncpu_path": "...\\firmware\\KL520\\fw_ncpu.bin",
"fw_folder": "...\\firmware", # 全域 firmware 目錄
"file_path": "...\\uploads\\image.jpg", # 圖片模式使用
"model": "...\\utils\\{mode}\\{model}\\{model_file}.nef",
"model_descriptor": <kp.ModelDescriptor>, # 已上傳的模型描述符
# 以下為 config.json 中的 input_parameters 欄位(由模型設定決定)
"threshold": 0.75,
"max_faces": 10,
...
}
```
---
## 10. 推論結果輸出格式
`script.py` 回傳或 `CustomInferenceWorkerThread` 發出的標準推論結果:
```json
{
"num_boxes": 2,
"bounding boxes": [[x1, y1, x2, y2], [x3, y3, x4, y4]],
"results": ["label1", "label2"]
}
```
其中座標為絕對像素值,對應原始影像尺寸(非縮放後的模型輸入尺寸)。
---
## 11. 安裝需求
### 11.1 開發環境安裝
```shell
# 1. 安裝 KneronPLUS SDK
cd ./external/kneron_plus_{version}/package/{platform}/
pip install KneronPLUS-{version}-py3-none-any.whl
# 2. 安裝其他依賴
pip install PyQt5 opencv-python pyinstaller pyarmor
# 3. 執行應用程式
python main.py
```
### 11.2 打包指令
```shell
pyinstaller --onefile --windowed main.py \
--additional-hooks-dir=hooks \
--add-data "uxui;uxui" \
--add-data "src;src" \
--add-data "{conda_env}\\Lib\\site-packages\\kp\\lib;kp\\lib"
```
---
## 12. 風險與緩解措施
| 風險 | 可能性 | 影響 | 緩解措施 |
|------|--------|------|----------|
| `script.py` 介面無標準文件,第三方難以開發 Plugin | 高 | 高 | 盡速制定並文件化 `script.py` 介面規範 |
| Purchased Items 功能仍是 Mock 資料,影響客戶體驗 | 中 | 高 | 確認後端 API 規格,完成對接 |
| 缺乏自動化測試,難以安全重構 | 高 | 中 | 補齊關鍵路徑的 unit test推論 queue、裝置連接 |
| `custom_inference_worker.py``kp` 引入問題,可能在非 kp 環境崩潰 | 中 | 中 | 修復模組頂層的型別引用問題 |
| 硬體相依性強,無 dongle 時無法測試核心功能 | 高 | 中 | 建立 Mock/Stub 的 kp 測試層 |
| Windows 專屬設計,未來擴展平台成本高 | 低 | 中 | 現階段維持 Windows 優先,抽象化路徑處理為未來鋪路 |
---
## 附錄 A程式碼結構快速參照
| 模組 | 路徑 | 功能 |
|------|------|------|
| `AppController` | `main.py` | 應用程式入口,管理 QStackedWidget 頁面切換 |
| `SelectionScreen` | `src/views/selection_screen.py` | 首頁選擇畫面 |
| `LoginScreen` | `src/views/login_screen.py` | 登入驗證畫面 |
| `UtilitiesScreen` | `src/views/utilities_screen.py` | 裝置管理工具頁面 |
| `MainWindow` | `src/views/mainWindows.py` | AI 推論主視窗 |
| `DeviceController` | `src/controllers/device_controller.py` | 裝置掃描、連接、中斷 |
| `InferenceController` | `src/controllers/inference_controller.py` | 推論工具選擇、queue 管理 |
| `MediaController` | `src/controllers/media_controller.py` | 攝影機操作 |
| `InferenceWorkerThread` | `src/models/inference_worker.py` | 標準推論 Worker動態載入 script.py |
| `CustomInferenceWorkerThread` | `src/models/custom_inference_worker.py` | Custom Model 推論 WorkerYOLOv5 後處理) |
| `VideoThread` | `src/models/video_thread.py` | 攝影機影像擷取 Thread |
| `check_available_device()` | `src/services/device_service.py` | 含 timeout 的裝置掃描服務 |
| `DeviceType`, `DongleModelMap` | `src/config.py` | 支援裝置型號列舉與對應表 |
---
*本 PRD 版本v1.0(從程式碼反推)*
*建立日期2026-04-04*
*建立者PM AgentAutoflow*
*狀態:待產品負責人確認標注「⚠️ 待確認」的項目*

View File

@ -1,729 +0,0 @@
# KNEO AcademyInnovedus AI Playground設計規格文件
> **文件性質說明**:本設計規格從既有程式碼反向整理,涵蓋 `src/config.py`、各 View 檔案及 `uxui/` 目錄下的 UI 資源。標注「⚠️ 待確認」的項目為推斷內容或程式碼中資訊不足的部分。
---
## ① Design Tokens
> 以下 Token 直接從 `src/config.py` 及各 View 的 `setStyleSheet()` 提取。
### 1.1 色彩系統Color Tokens
#### Reference Tokens原始值
| Token 名稱 | 色碼 | 說明 |
|-----------|------|------|
| color.navy.900 | `#143058` | 深海軍藍MainWindow 背景) |
| color.blue.600 | `#005ED7` | 亮藍SecondaryColorPopup / 元件背景) |
| color.blue.500 | `#3498DB` | 標準藍UtilitiesScreen 按鈕、表格 Header |
| color.blue.400 | `#2980B9` | 藍 Hover 色 |
| color.blue.300 | `#1F618D` | 藍 Pressed 色 |
| color.slate.800 | `#2C3E50` | 深石板藍LoginScreen/UtilitiesScreen Header 背景) |
| color.slate.700 | `#34495E` | 石板藍SelectionScreen Header、按鈕文字 |
| color.gray.100 | `#F8F9FA` | 極淺灰(進度區塊背景) |
| color.gray.50 | `#F5F7FA` | 淺灰LoginScreen / UtilitiesScreen 頁面背景) |
| color.gray.200 | `#E0E0E0` | 邊框灰(卡片、輸入框邊框) |
| color.gray.400 | `#BDC3C7` | 中灰(非選中導航按鈕文字) |
| color.gray.500 | `#95A5A6` | 灰Footer 文字、Back 按鈕) |
| color.gray.600 | `#7F8C8D` | 深灰(描述文字、次要標籤) |
| color.green.500 | `#2ECC71` | 標準綠Download 按鈕) |
| color.green.600 | `#27AE60` | 綠 Hover |
| color.green.700 | `#1E8449` | 綠 Pressed |
| color.green.400 | `#4CAF50` | 成功狀態綠Custom Model 上傳成功) |
| color.orange.500 | `#F39C12` | 橘Firmware Update 按鈕) |
| color.orange.600 | `#D35400` | 橘 Hover |
| color.orange.700 | `#A04000` | 橘 Pressed |
| color.purple.500 | `#9B59B6` | 紫Install Driver 按鈕) |
| color.purple.600 | `#8E44AD` | 紫 Hover |
| color.purple.700 | `#7D3C98` | 紫 Pressed |
| color.red.500 | `#E74C3C` | 紅(錯誤訊息文字) |
| color.red.400 | `#ff6b6b` | 亮紅Stop 按鈕文字及邊框) |
| color.white | `#FFFFFF` | 純白 |
| color.black | `#000000` | 純黑Canvas 背景) |
| color.selection | `#F8F9FA` | 選取頁面背景 |
#### Semantic Tokens語義映射
| Semantic Token | Reference Token | 說明 |
|---------------|----------------|------|
| color.bg.primary | color.navy.900`#143058` | MainWindow 頁面背景 |
| color.bg.light | color.gray.50`#F5F7FA` | Login / Utilities 頁面背景 |
| color.bg.selection | color.selection`#F8F9FA` | SelectionScreen 頁面背景 |
| color.bg.card | color.white | 卡片、表單容器背景 |
| color.bg.header.dark | color.slate.800`#2C3E50` | Login / Utilities Header 背景 |
| color.bg.header.alt | color.slate.700`#34495E` | SelectionScreen Header 背景 |
| color.bg.component | color.blue.600`#005ED7` | 側邊欄元件背景Device Panel、Custom Model Block、Media Panel、Popup |
| color.bg.canvas | color.black | Camera / 推論顯示區域背景 |
| color.bg.mask | `rgba(0, 0, 0, 0.7)` | Device Popup 蒙版 |
| color.border.default | color.gray.200`#E0E0E0` | 卡片、輸入框、表格邊框 |
| color.text.primary | color.slate.700`#34495E` | 主要文字(深色背景上) |
| color.text.secondary | color.gray.600`#7F8C8D` | 次要說明文字 |
| color.text.on-dark | color.white | 深色背景上的文字 |
| color.text.placeholder | color.gray.500`#95A5A6` | 輸入框 Placeholder |
| color.text.error | color.red.500`#E74C3C` | 錯誤訊息 |
| color.text.success | color.green.400`#4CAF50` | 成功狀態訊息 |
| color.text.footer | color.gray.500`#95A5A6` | Footer 版權文字 |
| color.action.primary | color.blue.500`#3498DB` | 主要 CTA 按鈕Login、Refresh、Utilities Tab |
| color.action.primary.hover | color.blue.400`#2980B9` | |
| color.action.primary.pressed | color.blue.300`#1F618D` | |
| color.action.success | color.green.500`#2ECC71` | 下載、Register 按鈕 |
| color.action.warning | color.orange.500`#F39C12` | Firmware Update 按鈕 |
| color.action.danger | color.red.400`#ff6b6b` | Stop 按鈕 |
| color.action.neutral | color.gray.500`#95A5A6` | Back 按鈕(中性) |
| color.action.special | color.purple.500`#9B59B6` | Install Driver 按鈕 |
| color.input.focus-border | color.blue.500`#3498DB` | 輸入框 Focus 狀態邊框 |
| color.table.header | color.blue.500`#3498DB` | 表格 Header 背景 |
| color.table.selected | color.blue.500`#3498DB` | 表格選中列背景 |
---
### 1.2 尺寸系統Size Tokens
#### 視窗尺寸
| Token | 數值 | 說明 |
|-------|------|------|
| window.width | 1200px | 固定視窗寬度(`WINDOW_SIZE` |
| window.height | 900px | 固定視窗高度(`WINDOW_SIZE` |
| popup.ratio | 0.67 | Device Popup 相對視窗的寬高比(`POPUP_SIZE_RATIO` |
| popup.width | ~804px | 計算值1200 × 0.67 |
| popup.height | ~603px | 計算值900 × 0.67 |
#### 佈局尺寸
| Token | 數值 | 說明 |
|-------|------|------|
| layout.left-panel.width | 260px | MainWindow 左側面板固定寬度 |
| layout.canvas.width | 900px | Camera Canvas 固定寬度 |
| layout.canvas.height | 750px | Camera Canvas 固定高度 |
| layout.canvas.inner.width | 880px | Canvas Label 最小寬度 |
| layout.canvas.inner.height | 730px | Canvas Label 最小高度 |
| layout.header.height | 60px | UtilitiesScreen Header 固定高度 |
| layout.header.height.alt | 100px | LoginScreen / SelectionScreen Header 固定高度 |
#### 元件尺寸
| Token | 數值 | 說明 |
|-------|------|------|
| component.device-panel.width | 240px | Device Panel 固定寬度 |
| component.device-panel.height | 200px | Device Panel 固定高度 |
| component.custom-model.width | 240px | Custom Model Block 固定寬度 |
| component.custom-model.height | 270px | Custom Model Block 固定高度 |
| component.media-panel.width | 90px | Media Panel 固定寬度 |
| component.media-panel.height | 290px | Media Panel 固定高度 |
| component.selection-card.min-width | 300px | SelectionScreen 選項卡片最小寬度 |
| component.selection-card.min-height | 250px | SelectionScreen 選項卡片最小高度 |
#### 按鈕尺寸
| Token | 數值 | 說明 |
|-------|------|------|
| button.standard.min-height | 40px | 標準操作按鈕最小高度UtilitiesScreen |
| button.login.min-height | 45px | 登入頁按鈕最小高度 |
| button.icon.size | 50×50px | Media Panel 圖示按鈕尺寸 |
| button.icon.inner | 40×40px | Media Panel SVG 圖示尺寸 |
| button.popup.size | 150×45px | Popup 中 Refresh / Done 按鈕尺寸 |
| button.detail.size | 72×30px | Device Panel 中 Details 按鈕尺寸 |
| button.back.size | 40×40px | Header Back 按鈕尺寸 |
| button.file-upload.size | 28×22px | Custom Model 上傳按鈕("...")尺寸 |
| button.custom-model-action.height | 32px | Custom Model Stop / Run 按鈕高度 |
#### 圖示尺寸
| Token | 數值 | 說明 |
|-------|------|------|
| icon.header.window | 35×35px | Popup 視窗 title 圖示 |
| icon.nav | 20×20px | Device Panel title 圖示 |
| icon.title.custom-model | 28×28px | Custom Model Block title 圖示 |
| icon.card.selection | 64×64px | SelectionScreen 卡片圖示 |
| icon.dongle.popup | 30×30px | Device Popup 中裝置圖示 |
| icon.dongle.list | 30×30px | Device List 中裝置圖示容器 |
| icon.logo.main | 104×40px | MainWindow / UtilitiesScreen 縮小 Logo |
| icon.logo.selection | 150×60px | SelectionScreen / LoginScreen 大型 Logo |
| icon.combo-arrow | 12×12px | QComboBox 下拉箭頭圖示 |
---
### 1.3 間距系統Spacing Tokens
| Token | 數值 | 使用場景 |
|-------|------|---------|
| spacing.xs | 2px | FileUploadRow 上下 margin |
| spacing.sm | 5px | List item padding、button spacing |
| spacing.md | 10px | 容器內部間距、部分 Padding |
| spacing.lg | 15px | 區塊內部間距Device Panel、Custom Model Block |
| spacing.xl | 20px | 頁面主要 MarginPopup、MainWindow canvas |
| spacing.xxl | 30px | 表單容器 PaddingLogin、SelectionScreen |
| spacing.form | 40px | SelectionScreen 主要 Margin |
| spacing.card | 40px | SelectionScreen 兩張卡片之間的間距 |
| spacing.nav | 10px | Header 導航按鈕之間的間距 |
---
### 1.4 圓角系統Border Radius Tokens
| Token | 數值 | 使用場景 |
|-------|------|---------|
| radius.sm | 3px | 檔案上傳標籤、小型 UI 元素 |
| radius.md | 5px | 按鈕UtilitiesScreen、輸入框、表格 |
| radius.lg | 8px | 卡片區塊Device Section、Purchased Items、Status Section |
| radius.xl | 10px | 主要卡片容器SelectionScreen、LoginScreen form |
| radius.xxl | 15px | 選項卡片SelectionScreen、側邊欄元件Device Panel、Custom Model Block |
| radius.pill | 20px | Popup、Media Panel大圓角 |
---
### 1.5 字型系統Typography Tokens
> **注意**:程式碼中未定義全域字型 family使用 PyQt5 系統預設字型Windows 環境下通常為 Segoe UI。以下為從程式碼提取的字型大小與字重規格。
| Token | 數值 | 使用場景 |
|-------|------|---------|
| font.size.xs | 10px | 檔案名稱標籤FileUploadRow、ComboBox 箭頭 |
| font.size.sm | 11px | Custom Model 小型標籤、狀態訊息、Run 按鈕文字 |
| font.size.body | 12px | Footer 文字、Device Popup 狀態標籤 |
| font.size.md | 14px | 表單標籤、描述文字、按鈕文字UtilitiesScreen、輸入框、KN 號碼標籤 |
| font.size.lg | 16px | 選項卡片描述、Device 標題標籤、Device LabelPopup 中、Custom Model title |
| font.size.xl | 18px | 區塊標題Device Connection、Device Status、Purchased Items |
| font.size.xxl | 20px | Device Panel 標題 |
| font.size.xxxl | 22px | 選項卡片標題 |
| font.size.display | 24px | Canvas 載入文字、UtilitiesScreen Header title |
| font.size.h2 | 28px | LoginScreen title、SelectionScreen APP title |
| font.size.h1 | 32px | Device Popup title |
| font.weight.normal | normal | 一般文字 |
| font.weight.bold | bold | 標題、按鈕文字、重要標籤 |
---
### 1.6 按鈕狀態樣式Button State Tokens
#### 全域按鈕樣式(`BUTTON_STYLE` — 深色背景通用)
```
背景透明transparent
文字顏色:白色
邊框2px solid white
圓角15px
Padding5px 10px
Hoverbackground-color: rgba(255, 255, 255, 50)(約 20% 白色透明)
Pressedbackground-color: rgba(255, 255, 255, 100)(約 39% 白色透明)
```
#### 主要 CTA 按鈕(藍色,淺色背景場景)
```
背景:#3498DB
文字:白色
邊框none或 2px solid #2980B9
圓角5px
MinHeight40-45px
Padding10px 15px
Hover#2980B9
Pressed#1F618D
Disabled#3498DB(同 Default視覺不變
```
#### 成功/下載按鈕(綠色)
```
背景:#2ECC71
文字:白色
邊框2px solid #27AE60
圓角5px
Hover#27AE60
Pressed#1E8449
```
#### 警告按鈕(橘色 Firmware Update
```
背景:#F39C12
文字:白色
邊框2px solid #D35400
圓角5px
Hover#D35400
Pressed#A04000
```
#### 特殊功能按鈕(紫色 Install Driver
```
背景:#9B59B6
文字:白色
邊框2px solid #8E44AD
圓角5px
Hover#8E44AD
Pressed#7D3C98
```
#### 中性返回按鈕(灰色)
```
背景:#95A5A6
文字:白色
圓角5px
Hover#7F8C8D
Pressed#616A6B
```
#### Custom Model — Run 按鈕(綠色半透明)
```
背景rgba(76, 175, 80, 0.3)
文字:白色
邊框1px solid #4CAF50
圓角8px
高度32px
Hoverrgba(76, 175, 80, 0.5)
Disabledrgba(128, 128, 128, 0.2),文字 #666,邊框 #666
```
#### Custom Model — Stop 按鈕(紅色透明)
```
背景:透明
文字:#ff6b6b
邊框1px solid #ff6b6b
圓角8px
高度32px
Hoverrgba(255, 107, 107, 0.2)
```
#### Media Panel 圖示按鈕
```
背景:透明
邊框1px transparent
圓角10px
尺寸50×50px
Hoverrgba(255, 255, 255, 50)
Pressedrgba(255, 255, 255, 100)
```
#### Header Back 按鈕(圖示透明按鈕)
```
背景:透明
邊框none
尺寸40×40px
Hoverrgba(255, 255, 255, 0.1),圓角 20px
```
---
## ② UI 資源清單
> 路徑:`uxui/`
### 2.1 PNG 圖片(`Assets_png/`
| 檔名 | 推斷用途 | 使用位置 |
|------|---------|---------|
| `kneron_logo.png` | Kneron / Innovedus 品牌 Logo | MainWindow 歡迎畫面、所有頁面 Header、MainWindow 左側面板 |
| `ic_dongle_520.png` | KL520 Dongle 裝置圖示 | Device Popup、Device List裝置列表項 |
| `ic_dongle_720.png` | KL720 Dongle 裝置圖示 | Device Popup、Device List裝置列表項 |
> **⚠️ 待確認**:程式碼中 `create_header()` 呼叫 `Assets_png/back_arrow.png` 作為 Back 按鈕圖示,但此檔案**不在 `Assets_png/` 目錄中**。此按鈕目前可能顯示為空白或破圖。
---
### 2.2 SVG 圖示(`Assets_svg/`
#### 功能按鈕圖示(`bt_function_*`
| 檔名 | 狀態 | 推斷用途 |
|------|------|---------|
| `bt_function_camera_disabled.svg` | disabled | 相機功能按鈕(停用) |
| `bt_function_mic_disabled.svg` | disabled | 麥克風功能按鈕(停用) |
| `bt_function_mic_hover.svg` | hover | 麥克風功能按鈕 |
| `bt_function_mic_normal.svg` | normal | 麥克風功能按鈕 |
| `bt_function_mic_pressed.svg` | pressed | 麥克風功能按鈕 |
| `bt_function_recording_hover.svg` | hover | 錄製功能按鈕 |
| `bt_function_recording_normal.svg` | normal | 錄製功能按鈕 |
| `bt_function_recording_pressed.svg` | pressed | 錄製功能按鈕 |
| `bt_function_screencapture_disabled.svg` | disabled | 螢幕截圖按鈕(停用) |
| `bt_function_screencapture_hover.svg` | hover | 螢幕截圖按鈕 |
| `bt_function_screencapture_normal.svg` | normal | 螢幕截圖按鈕Media Panel 第一個按鈕) |
| `bt_function_screencapture_pressed.svg` | pressed | 螢幕截圖按鈕 |
| `bt_function_upload_disabled.svg` | disabled | 上傳檔案按鈕(停用) |
| `bt_function_upload_hover.svg` | hover | 上傳檔案按鈕 |
| `bt_function_upload_normal.svg` | normal | 上傳檔案按鈕Media Panel 第二個按鈕) |
| `bt_function_upload_pressed.svg` | pressed | 上傳檔案按鈕 |
| `bt_function_video_hover.svg` | hover | 影片/恢復播放按鈕(暫停→恢復時切換) |
| `bt_function_video_normal.svg` | normal | 影片按鈕 |
| `bt_function_video_pressed.svg` | pressed | 影片按鈕 |
> **⚠️ 待確認**:目前 Media Panel「暫停/恢復」按鈕使用 `btn_result_image_delete_hover.svg` 作為暫停狀態圖示,切換後顯示 `bt_function_video_hover.svg`。此對應關係看起來不是最終設計,圖示使用可能有誤。
#### 對話框按鈕(`btn_dialog_*`
| 檔名 | 狀態 | 推斷用途 |
|------|------|---------|
| `btn_dialog_customization_delete_hover.svg` | hover | 自定義對話框刪除按鈕 |
| `btn_dialog_customization_delete_normal.svg` | normal | 自定義對話框刪除按鈕 |
| `btn_dialog_customization_delete_pressed.svg` | pressed | 自定義對話框刪除按鈕 |
| `btn_dialog_customization_upload_hover.svg` | hover | 自定義對話框上傳按鈕 |
| `btn_dialog_customization_upload_normal.svg` | normal | 自定義對話框上傳按鈕 |
| `btn_dialog_customization_upload_pressed.svg` | pressed | 自定義對話框上傳按鈕 |
| `btn_dialog_device_disconnect_hover.svg` | hover | 裝置斷線按鈕 |
| `btn_dialog_device_disconnect_normal.svg` | normal | 裝置斷線按鈕 |
| `btn_dialog_device_disconnect_pressed.svg` | pressed | 裝置斷線按鈕 |
> **⚠️ 待確認**`btn_dialog_*` 系列圖示在目前的程式碼中**找不到對應的呼叫位置**(自定義對話框的刪除/上傳、裝置斷線按鈕),可能屬於尚未實作的功能 UI或被移除但資源保留。
#### 結果區域按鈕(`btn_result_*`
| 檔名 | 狀態 | 推斷用途 |
|------|------|---------|
| `btn_result_edit_hover.svg` | hover | 推論結果編輯按鈕 |
| `btn_result_edit_normal.svg` | normal | 推論結果編輯按鈕 |
| `btn_result_edit_pressed.svg` | pressed | 推論結果編輯按鈕 |
| `btn_result_image_delete_hover.svg` | hover | 推論結果圖片刪除按鈕(目前被借用為暫停圖示) |
| `btn_result_image_delete_normal.svg` | normal | 推論結果圖片刪除按鈕 |
| `btn_result_image_delete_pressed.svg` | pressed | 推論結果圖片刪除按鈕 |
> **⚠️ 待確認**`btn_result_*` 在程式碼中僅 `btn_result_image_delete_hover.svg` 被使用(且用途有疑問),其他均未見使用。
#### 設定與下載(單一圖示)
| 檔名 | 推斷用途 | 使用位置 |
|------|---------|---------|
| `btn_setting.svg` | 設定按鈕 | ⚠️ 程式碼中未見呼叫 |
| `ic_result_download_diabled.svg` | 結果下載(停用) | ⚠️ 程式碼中未見呼叫 |
| `ic_result_download_hover.svg` | 結果下載 Hover | ⚠️ 程式碼中未見呼叫 |
| `ic_result_download_normal.svg` | 結果下載 Normal | ⚠️ 程式碼中未見呼叫 |
| `ic_result_download_pressed.svg` | 結果下載 Pressed | ⚠️ 程式碼中未見呼叫 |
| `ic_result_folder_hover.svg` | 結果資料夾 Hover | ⚠️ 程式碼中未見呼叫(除被借用為 ComboBox 下拉箭頭) |
| `ic_result_folder_normal.svg` | 結果資料夾 Normal | 被用作 LoginScreen QComboBox 下拉箭頭 |
| `ic_result_folder_pressed.svg` | 結果資料夾 Pressed | ⚠️ 程式碼中未見呼叫 |
#### 通用圖示(`ic_*`
| 檔名 | 推斷用途 | 使用位置 |
|------|---------|---------|
| `ic_customization_upload_folder.svg` | 自定義模型上傳資料夾圖示 | ⚠️ 程式碼中未見呼叫 |
| `ic_dialog_customization.svg` | 自定義對話框圖示 | ⚠️ 程式碼中未見呼叫 |
| `ic_dialog_device.svg` | 裝置對話框圖示 | SelectionScreen Utilities 卡片圖示 |
| `ic_dialog_missing_camera.svg` | 找不到攝影機圖示 | ⚠️ 程式碼中未見呼叫(應用於相機找不到的提示) |
| `ic_dongle_520.svg` | KL520 Dongle 圖示SVG 版) | ⚠️ 程式碼中未見呼叫PNG 版本被使用) |
| `ic_recording_camera.svg` | 攝影機/錄影圖示 | SelectionScreen Demo App 卡片圖示Media Panel 第五個按鈕(錄影) |
| `ic_recording_voice.svg` | 錄音圖示 | Media Panel 第四個按鈕(錄音) |
| `ic_window_customization.svg` | 自定義視窗圖示 | ⚠️ 程式碼中未見呼叫 |
| `ic_window_device.svg` | 裝置視窗圖示 | Device Popup 標題列Device Panel 標題列 |
| `ic_window_toolbox.svg` | 工具箱視窗圖示 | Custom Model Block 標題列 |
---
### 2.3 動態圖(`Assets_gif/`
| 檔名 | 用途 | 使用位置 |
|------|------|---------|
| `no_device_temp.gif` | 無裝置狀態動態提示圖 | `MainWindow.show_no_device_gif()`(無裝置時在主視窗顯示) |
---
### 2.4 根目錄 SVG 資源
| 檔名 | 推斷用途 | 使用位置 |
|------|---------|---------|
| `canvas_background.svg` | 攝影機畫布背景 SVG | ⚠️ 程式碼中未見呼叫 |
| `usb_dongle.svg` | USB Dongle 圖示 | ⚠️ 程式碼中未見呼叫 |
---
## ③ 頁面元件清單
### 3.1 SelectionScreen首頁選擇畫面
**背景色**`#F8F9FA`(淺灰白)
**佈局**QVBoxLayoutMargin 40px 全周Spacing 20px
| 元件 | Class/類型 | 尺寸/規格 | 樣式摘要 |
|------|-----------|---------|---------|
| 頁面背景 | QWidget | 1200×900px | `background: #F8F9FA` |
| Header Frame | QFrame | 全寬 × 100px 固定高 | `background: #34495E; border-radius: 10px` |
| Kneron Logo | QLabel + QPixmap | 縮放至 150×60px | 水平居中 |
| 內容容器 | QFrame | 彈性高(填充剩餘空間)| `background: white; border-radius: 10px; border: 1px solid #E0E0E0`Padding 30px |
| APP 名稱標題 | QLabel | — | `font-size: 28px; font-weight: bold; color: #34495E`,水平居中 |
| 副標題 | QLabel | — | `font-size: 16px; color: #7F8C8D`,水平居中,文字:「請選擇您要使用的功能」 |
| 按鈕容器 | QWidget + QHBoxLayout | — | Margin 20px 左右Spacing 40px |
| **Utilities 卡片** | QFrame | min 300×250px | `background: white; border-radius: 15px; border: 1px solid #E0E0E0`Hover`background: #F5F9FF; border: #5DADE2`cursor: PointingHand |
| Utilities 卡片圖示 | QLabel + QPixmap | 64×64px | `ic_dialog_device.svg`,水平居中 |
| Utilities 卡片標題 | QLabel | — | `font-size: 22px; font-weight: bold; color: #34495E`,居中 |
| Utilities 卡片說明 | QLabel | — | `font-size: 14px; color: #7F8C8D`,居中,換行 |
| **Demo App 卡片** | QFrame | min 300×250px | `background: white; border-radius: 15px; border: 1px solid #E0E0E0`Hover`background: #F5FFF7; border: #7DCEA0`cursor: PointingHand |
| Demo App 卡片圖示 | QLabel + QPixmap | 64×64px | `ic_recording_camera.svg`,水平居中 |
| Demo App 卡片標題 | QLabel | — | `font-size: 22px; font-weight: bold; color: #34495E`,居中 |
| Demo App 卡片說明 | QLabel | — | `font-size: 14px; color: #7F8C8D`,居中,換行 |
| Footer | QLabel | — | `font-size: 12px; color: #95A5A6`,居中,文字:「© 2025 Innovedus Inc. All rights reserved.」 |
---
### 3.2 LoginScreen登入畫面
**背景色**`#F5F7FA`(淺灰)
**佈局**QVBoxLayoutMargin 40px 全周Spacing 20px
| 元件 | Class/類型 | 尺寸/規格 | 樣式摘要 |
|------|-----------|---------|---------|
| 頁面背景 | QWidget | 1200×900px | `background: #F5F7FA` |
| Header Frame | QFrame | 全寬 × 100px 固定高 | `background: #2C3E50; border-radius: 10px` |
| Kneron Logo | QLabel + QPixmap | 縮放至 150×60px | 水平居中 |
| 表單容器 | QFrame | 彈性高(填充剩餘空間)| `background: white; border-radius: 10px; border: 1px solid #E0E0E0`Padding 30px |
| 登入標題 | QLabel | — | `font-size: 28px; font-weight: bold; color: #2C3E50; margin-bottom: 10px`,居中,文字:"Login" |
| Server 驗證標籤 | QLabel | — | `font-size: 14px; font-weight: bold; color: #2C3E50`,文字:"Server Authentication Type" |
| **驗證類型 ComboBox** | QComboBox | min-height 40px | `border: 1px solid #E0E0E0; border-radius: 5px; padding: 5px 10px; font-size: 14px; color: #2C3E50`;下拉箭頭使用 `ic_result_folder_normal.svg` 12×12px |
| 帳號標籤 | QLabel | — | `font-size: 14px; font-weight: bold; color: #2C3E50`,文字:"Username" |
| **帳號輸入框** | QLineEdit | min-height 40px | `border: 1px solid #E0E0E0; border-radius: 5px; padding: 5px 10px; font-size: 14px; color: #2C3E50`Focus`border: 1px solid #3498DB`Placeholder"Enter your username" |
| 密碼標籤 | QLabel | — | `font-size: 14px; font-weight: bold; color: #2C3E50`,文字:"Password" |
| **密碼輸入框** | QLineEdit | min-height 40px | 同帳號輸入框EchoMode: PasswordPlaceholder"Enter your password" |
| 錯誤訊息 | QLabel | — | `color: #E74C3C; font-size: 14px`;預設隱藏(`hide()`),驗證失敗時顯示 |
| Back 按鈕 | QPushButton | min-height 45px | `background: #95A5A6; color: white; border-radius: 5px; font-size: 14px; font-weight: bold`Hover`#7F8C8D`Pressed`#616A6B` |
| Login 按鈕 | QPushButton | min-height 45px | `background: #3498DB; color: white; border-radius: 5px; font-size: 14px; font-weight: bold`Hover`#2980B9`Pressed`#1F618D` |
| Footer | QLabel | — | `font-size: 12px; color: #95A5A6`,居中 |
---
### 3.3 UtilitiesScreen裝置管理工具
**背景色**`#F5F7FA`(淺灰)
**佈局**QVBoxLayoutMargin 20px 全周Spacing 20px
#### 3.3.1 Header
| 元件 | 類型 | 尺寸/規格 | 樣式摘要 |
|------|------|---------|---------|
| Header Frame | QFrame | 全寬 × 60px 固定高 | `background: #2C3E50; border-radius: 0px` |
| Back 按鈕 | QPushButton | 40×40px | 圖示按鈕(`Assets_png/back_arrow.png`⚠️ 檔案缺失Hover`rgba(255,255,255,0.1) border-radius 20px` |
| 頁面標題 | QLabel | — | `color: white; font-size: 24px; font-weight: bold`,文字:"Utilities" |
| **Utilities 導航按鈕**(選中) | QPushButton | padding 5px 10px | `background: #3498DB; color: white; border: none; border-radius: 5px; font-weight: bold` |
| **Purchased Items 導航按鈕**(未選) | QPushButton | padding 5px 10px | `background: transparent; color: #BDC3C7; border: none; border-radius: 5px; font-weight: bold`Hover`color: white` |
| Kneron Logo | QLabel + QPixmap | 縮放至 104×40px | 靠右對齊 |
#### 3.3.2 Utilities 子頁面 — 裝置管理區塊
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| 區塊容器 | QFrame | — | `background: white; border-radius: 8px; border: 1px solid #E0E0E0`Padding 15px |
| 區塊標題 | QLabel | — | `font-size: 18px; font-weight: bold; color: #2C3E50`,文字:"Device Connection" |
| 區塊說明 | QLabel | — | `font-size: 14px; color: #7F8C8D`,文字:"Connect and manage your Kneron devices" |
| **裝置資料表** | QTableWidget | min-height 可捲動 | 6 欄Device Type / Port ID / Firmware Version / KN Number / Link Speed / StatusHeader `background: #3498DB; color: white; font-weight: bold; padding: 8px`Item padding 8pxSelected `background: #3498DB; color: white`gridline `#E0E0E0` |
| Refresh Devices 按鈕 | QPushButton | min-height 40px | `background: #3498DB; border: 2px solid #2980B9; border-radius: 5px; font-size: 14px; font-weight: bold`;狀態色如前 |
| Register Device 按鈕 | QPushButton | min-height 40px | `background: #2ECC71; border: 2px solid #27AE60`;狀態色如前 |
| Update Firmware 按鈕 | QPushButton | min-height 40px | `background: #F39C12; border: 2px solid #D35400`;狀態色如前 |
| Install Driver 按鈕 | QPushButton | min-height 40px | `background: #9B59B6; border: 2px solid #8E44AD`;狀態色如前 |
#### 3.3.3 Utilities 子頁面 — 狀態區塊
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| 區塊容器 | QFrame | — | `background: white; border-radius: 8px; border: 1px solid #E0E0E0`Padding 15px |
| 區塊標題 | QLabel | — | `font-size: 18px; font-weight: bold; color: #2C3E50`,文字:"Device Status" |
| 狀態訊息 | QLabel | — | `font-size: 14px; color: #7F8C8D`,預設文字:"No devices found" |
| 進度區塊(隱藏) | QFrame | — | `background: #F8F9FA; border-radius: 5px; border: 1px solid #E0E0E0; padding: 10px`;預設 `setVisible(False)` |
| 進度標題 | QLabel | — | `font-size: 14px; font-weight: bold; color: #2C3E50` |
| **進度條** | QProgressBar | height 20px | `border: 1px solid #E0E0E0; border-radius: 5px; background: white; text-align: center`Chunk`background: #3498DB; border-radius: 5px` |
#### 3.3.4 Purchased Items 子頁面
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| 區塊容器 | QFrame | — | `background: white; border-radius: 8px; border: 1px solid #E0E0E0`Padding 15px |
| 區塊標題 | QLabel | — | `font-size: 18px; font-weight: bold; color: #2C3E50`,文字:"Your Purchased Items" |
| 區塊說明 | QLabel | — | `font-size: 14px; color: #7F8C8D`,文字:"Select items to download to your device" |
| **已購項目表格** | QTableWidget | min-height 300px | 5 欄SelectCheckbox/ Product / Model / Current Version / Compatible Dongles樣式同裝置表格藍色 Header選中藍色 |
| Refresh Items 按鈕 | QPushButton | min-height 40px | `background: #3498DB; border: none; border-radius: 5px; font-size: 14px; font-weight: bold`Hover`#2980B9`Pressed`#1F618D` |
| Download Selected 按鈕 | QPushButton | min-height 40px | `background: #2ECC71; border: none; border-radius: 5px; font-size: 14px; font-weight: bold`Hover`#27AE60`Pressed`#1E8449` |
---
### 3.4 MainWindowAI Demo 推論主視窗)
**背景色**`#143058`(深海軍藍)
**佈局**QVBoxLayout 外層啟動時先顯示歡迎畫面Logo500ms 後切換至主頁面
#### 3.4.1 歡迎畫面500ms 轉場)
| 元件 | 類型 | 規格 | 說明 |
|------|------|------|------|
| 歡迎 Logo | QLabel + QPixmap | 原始尺寸 | `kneron_logo.png`,水平居中 |
#### 3.4.2 主頁面佈局QHBoxLayout
左右兩欄佈局:
- **左欄**:固定寬度 260pxQVBoxLayoutMargin 10pxSpacing 10px
- **右欄**彈性寬度stretch factor 2QGridLayoutMargin 0px
#### 3.4.3 左欄元件
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| Kneron Logo | QLabel + QPixmap | 縮放至 104×40px | — |
| **Device Panel** | QFrame | 240×200px 固定 | `background: #005ED7; border-radius: 15px; border: none` |
| — Device 圖示 | QSvgWidget | 20×20px | `ic_window_device.svg` |
| — Device 標題 | QLabel | — | `color: white; font-size: 20px; font-weight: bold` |
| — 裝置列表 | QListWidget | — | 透明背景Itempadding 5pxSelected`rgba(255,255,255,0.2)`;無 Scrollbar |
| — Details 按鈕 | QPushButton | 72×30px | 全域 `BUTTON_STYLE`(透明背景、白色邊框) |
| **Custom Model Block** | QFrame | 240×270px 固定 | `background: #005ED7; border-radius: 15px; border: none` |
| — 工具箱圖示 | QSvgWidget | 28×28px | `ic_window_toolbox.svg` |
| — Custom Model 標題 | QLabel | — | `color: white; font-size: 16px; font-weight: bold` |
| — 分隔線 | QFrame HLine | 1px 高 | `background: rgba(255,255,255,0.2)` |
| — Model 上傳列(.nef | FileUploadRow | — | 標籤 50px 寬;檔案框 22px 高;上傳按鈕 28×22px |
| — SCPU 上傳列(.bin | FileUploadRow | — | 同上 |
| — NCPU 上傳列(.bin | FileUploadRow | — | 同上 |
| — Labels 上傳列(.txt | FileUploadRow | — | 同上 |
| — 狀態標籤 | QLabel | — | `color: #4CAF50; font-size: 11px`;成功綠色,錯誤時 `#ff6b6b` |
| — Stop 按鈕 | QPushButton | 高 32px | 透明背景,紅色邊框文字(`#ff6b6b` |
| — Run Inference 按鈕 | QPushButton | 高 32px | 半透明綠色(`rgba(76,175,80,0.3)`),綠色邊框 |
#### 3.4.4 右欄元件
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| **Canvas Area** | QFrame | 900×750px 固定 | `border: 1px solid gray; background: black; border-radius: 20px`Padding 10px |
| Canvas Label顯示區| QLabel | 880×730px 最小 | 黑色背景,透明邊框;顯示攝影機 QImage 或靜態圖片 |
| **Media Panel**(浮動右下)| QFrame | 90×290px 固定 | `background: #005ED7; border-radius: 20px`;使用 `Qt.AlignBottom | Qt.AlignRight` 疊加在 Canvas 右下 |
| — 截圖按鈕 | QPushButton | 50×50px | 圖示 `bt_function_screencapture_normal.svg`40×40px |
| — 上傳按鈕 | QPushButton | 50×50px | 圖示 `bt_function_upload_normal.svg`40×40px |
| — 暫停/恢復按鈕 | QPushButton | 50×50px | 暫停時:`btn_result_image_delete_hover.svg`;恢復時:`bt_function_video_hover.svg` |
| — 錄音按鈕 | QPushButton | 50×50px | 圖示 `ic_recording_voice.svg`40×40px |
| — 錄影按鈕 | QPushButton | 50×50px | 圖示 `ic_recording_camera.svg`40×40px |
#### 3.4.5 Device Popup蒙版彈出視窗
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| 蒙版 Overlay | QWidget | 1200×900px全視窗 | `background: rgba(0, 0, 0, 0.7)` |
| **Device Popup** | QWidget | 804×603px視窗 × 0.67 | `background: #005ED7; border-radius: 20px; padding: 20px` |
| — 裝置圖示 | QSvgWidget | 35×35px | `ic_window_device.svg` |
| — 彈窗標題 | QLabel | — | `color: white; font-size: 32px; font-weight: bold`,文字:"Device Connection" |
| — 裝置列表Popup | QListWidget | min-height 250px | `background: rgba(255,255,255,0.1); border-radius: 10px; color: white`Item Selected`rgba(52,152,219,0.5)` |
| — 裝置列表項 | 自訂 QWidget | 60px 固定高 | 圖示容器 30×30px`#182D4B` 背景border-radius 5px+ 裝置名16px bold+ KN 號碼14px+ 狀態12px 右對齊) |
| — Refresh 按鈕 | QPushButton | 150×45px | 全域 `BUTTON_STYLE` |
| — Done 按鈕 | QPushButton | 150×45px | 全域 `BUTTON_STYLE` |
---
## ④ 互動規格
### 4.1 頁面切換方式Signal/Slot
```
SelectionScreen.open_utilities → 主程式切換到 LoginScreen
SelectionScreen.open_demo_app → 主程式切換到 MainWindow
LoginScreen.login_success → 主程式切換到 UtilitiesScreen
LoginScreen.back_to_selection → 主程式切換到 SelectionScreen
UtilitiesScreen.back_to_selection → 主程式切換到 SelectionScreen
```
> **⚠️ 待確認**:頁面切換的具體實作(是否使用 QStackedWidget還是直接 hide/show需查看 `main.py` 或應用程式進入點。
### 4.2 MainWindow 啟動流程
```
1. 顯示歡迎畫面Kneron Logo
2. 500ms 後 → 清除歡迎畫面,建立主頁面
3. 100ms 後 → 呼叫 device_controller.refresh_devices()
4. 顯示 Device Popup蒙版覆蓋主頁面
5. 500ms 後 → 自動啟動攝影機auto_start_camera
```
### 4.3 按鈕 Enabled/Disabled 條件
| 按鈕 | 停用條件 | 啟用條件 |
|------|---------|---------|
| Media Panel 圖示按鈕(相機) | 相機功能不可用時(`bt_function_camera_disabled.svg` 存在暗示) | ⚠️ 未在程式碼中找到明確的 setEnabled 邏輯 |
| Custom Model — Run Inference | 無(任何時候都可點擊,但點擊後會驗證檔案) | 有效檔案選擇後 |
| Custom Model — Stop | 無推論進行時重置檔案選擇 | — |
| UtilitiesScreen 操作按鈕 | ⚠️ 未找到明確的停用條件(選取裝置後才應啟用的邏輯有待確認) | — |
### 4.4 對話框觸發條件
| 對話框 | 觸發條件 | 類型 |
|--------|---------|------|
| Device Popup | MainWindow 啟動時自動顯示 | 自訂 QWidget 蒙版 |
| Inference Result | 推論回傳非 Bounding Box 結果(分類等)時 | QMessageBox |
| Device 相容性警告 | 選擇不相容裝置的 AI 工具時 | QMessageBox推斷 |
| 無裝置 GIF | 推斷:無裝置連接時 | QLabel + QMovie |
| Download Complete | 所有選中項目下載完成 | QMessageBox.information |
| No Selection 警告 | 點擊 Download 但未勾選任何項目 | QMessageBox.warning |
| 相機失敗提示 | 相機開啟失敗 | ⚠️ 程式碼中有 `ic_dialog_missing_camera.svg` 資源,但未見對應邏輯 |
### 4.5 UtilitiesScreen 頁面切換
| 觸發 | 行為 |
|------|------|
| 點擊「Utilities」導航按鈕 | `utilities_page.show()``purchased_items_page.hide()`Utilities 按鈕樣式變為選中(`#3498DB`Purchased 按鈕變為未選中(透明) |
| 點擊「Purchased Items」導航按鈕 | `purchased_items_page.show()``utilities_page.hide()` |
> **⚠️ 待確認**:目前程式碼中未見導航按鈕選中/未選中狀態的動態切換邏輯(按鈕樣式可能沒有在切換時更新)。
### 4.6 Custom Model 檔案上傳互動
| 步驟 | 互動 | 視覺變化 |
|------|------|---------|
| 點擊「...」按鈕 | 開啟系統檔案選擇器 | — |
| 選擇檔案成功 | 顯示檔案名稱(超過 15 字截斷加「...」) | 標籤顏色從 `#aaa` 變為 `#4CAF50`;邊框 `#4CAF50`;背景 `rgba(76,175,80,0.1)` |
| 點擊 Run Inference缺少必要檔案 | — | 狀態標籤文字更新為錯誤提示,顏色變為 `#ff6b6b` |
| 點擊 Run Inference所有必要檔案已選 | 啟動推論 | 狀態標籤顯示 "Running: {model_name}",顏色 `#4CAF50` |
| 點擊 Stop有推論進行中 | 停止推論 | 狀態標籤顯示 "Stopped & Disconnected",顏色 `#ff6b6b` |
| 點擊 Stop無推論 | 重置所有檔案選擇 | 所有 FileUploadRow 回復到「未選擇」狀態 |
---
## ⑤ 缺失的設計規格
### 5.1 互動細節(程式碼中看不出)
| 項目 | 說明 |
|------|------|
| Bounding Box 繪製樣式 | 推論結果繪製在 Canvas 上的 Bounding Box 顏色、線條粗細、Label 字型樣式均無 Design Spec從程式碼僅知座標格式 |
| 相機找不到時的 UI | 有 `ic_dialog_missing_camera.svg` 資源,但找不到對應的顯示邏輯 |
| 推論進行中的 Loading 狀態 | Canvas 載入文字為 "Starting camera...",但推論進行中是否有 Loading indicator 不清楚 |
| UtilitiesScreen 導航按鈕切換動態 | 頁面切換時按鈕選中狀態的樣式切換邏輯不完整 |
| 相機暫停後的 Canvas 顯示 | 暫停時 Canvas 顯示最後一幀還是空白?未知 |
| MSE 幀差異偵測的視覺提示 | 技術機制存在,但使用者是否看到任何提示?未知 |
| 推論 Queue 滿時的提示 | Queue 最大 5 幀,滿時的行為是靜默丟棄,無視覺提示 |
### 5.2 未完整實作的 UI 功能
| 項目 | 說明 |
|------|------|
| Driver 安裝 UI | `Install Driver` 按鈕存在,但 `install_drivers()` 方法的實際 UI 流程未見完整實作 |
| Register Device UI | `Register Device` 按鈕存在,`register_device()` 方法實作未確認 |
| Dongle 授權管理 UI | PRD 中有完整描述,但 UtilitiesScreen 中未見對應 UI 元素 |
| Toolbox原始工具箱| `create_ai_toolbox()` 元件存在但在 MainWindow 中已被 `CustomModelBlock` 取代Toolbox 功能未整合 |
| 購買模型下載(真實 API| 目前為 Mock 資料,`populate_mock_purchased_items()` 方法,無真實下載邏輯 |
| 自定義對話框 UI | `btn_dialog_customization_*``ic_dialog_customization.svg` 資源存在,但無對應 UI 元件 |
| 裝置斷線按鈕 UI | `btn_dialog_device_disconnect_*` 資源存在,但無對應 UI 元件 |
| 結果下載/資料夾 UI | `ic_result_download_*``ic_result_folder_*``btn_result_edit_*` 資源存在,但程式碼中大多未呼叫 |
| 設定頁面 | `btn_setting.svg` 存在,但無對應的設定頁面 |
### 5.3 設計一致性問題(觀察到的設計債)
| 問題 | 說明 | 嚴重程度 |
|------|------|---------|
| 兩種截然不同的設計語言 | MainWindow 使用深海軍藍(`#143058`為主的深色設計SelectionScreen、LoginScreen、UtilitiesScreen 使用淺色(`#F5F7FA`)設計。同一個應用存在兩套視覺語言,缺乏統一感 | High |
| Back 按鈕圖示資源缺失 | UtilitiesScreen Header 使用的 `Assets_png/back_arrow.png` 不存在於 `uxui/Assets_png/` 目錄,按鈕為空白 | High |
| 暫停按鈕圖示錯誤 | Media Panel 暫停按鈕使用 `btn_result_image_delete_hover.svg`(刪除圖示)作為暫停狀態,語意不正確 | Medium |
| 未定義系統字型 | 無明確的 `font-family` 定義,依賴 Windows 系統預設字型,跨平台(若未來支援)可能不一致 | Low |
| Dark Mode 未規劃 | 完全無 Dark Mode 設計 | Low |
| Disabled 按鈕視覺未完整定義 | UtilitiesScreen 按鈕幾乎未定義 Disabled 狀態樣式 | Medium |
| Focus 狀態不完整 | 大多數按鈕無 Focus ring 設計(僅 LoginScreen QLineEdit 有 Focus 邊框) | Medium |
---
*本文件由 Design Agent 從既有程式碼反向整理版本日期2026-04-04*

View File

@ -1,970 +0,0 @@
# 技術設計文件TDD— KNEO Academy v2.0
**作者**Architect Agent
**狀態**Draft從既有程式碼反向整理
**日期**2026-04-04
**對應 Design Doc**`04-architecture/design-doc.md`
---
## 目錄
1. [模組 API 規格](#1-模組-api-規格)
- 1.1 [config.py — 全域設定](#11-configpy--全域設定)
- 1.2 [AppControllermain.py](#12-appcontrollermainpy)
- 1.3 [DeviceController](#13-devicecontroller)
- 1.4 [InferenceController](#14-inferencecontroller)
- 1.5 [MediaController](#15-mediacontroller)
- 1.6 [InferenceWorkerThread](#16-inferenceworkerthread)
- 1.7 [CustomInferenceWorkerThread](#17-custominferenceworkerthread)
- 1.8 [VideoThread](#18-videothread)
- 1.9 [DeviceService](#19-deviceservice)
- 1.10 [FileService](#110-fileservice)
- 1.11 [ConfigUtils](#111-configutils)
- 1.12 [image_utils](#112-image_utils)
2. [InferenceWorkerThread 與 script.py 介面規範](#2-inferenceworkerthread-與-scriptpy-介面規範)
3. [裝置連接 API 流程kp SDK 呼叫順序)](#3-裝置連接-api-流程kp-sdk-呼叫順序)
4. [推論 Queue 設計](#4-推論-queue-設計)
5. [Signal/Slot 對應表](#5-signalslot-對應表)
6. [config.json 完整 Schema 定義](#6-configjson-完整-schema-定義)
7. [已知限制與邊界條件](#7-已知限制與邊界條件)
---
## 1. 模組 API 規格
### 1.1 config.py — 全域設定
**路徑**`src/config.py`
#### 常數
| 常數名稱 | 型別 | 說明 |
|---------|------|------|
| `APPDATA_PATH` | str | `%LOCALAPPDATA%` 環境變數值 |
| `PROJECT_ROOT` | str | 專案根目錄絕對路徑 |
| `UXUI_ASSETS` | str | `{PROJECT_ROOT}/uxui/` |
| `UTILS_DIR` | str | `%LOCALAPPDATA%/Kneron_Academy/utils` |
| `SCRIPT_CONFIG` | str | `{UTILS_DIR}/config.json` |
| `UPLOAD_DIR` | str | `%LOCALAPPDATA%/Kneron_Academy/uploads` |
| `FW_DIR` | str | `%LOCALAPPDATA%/Kneron_Academy/firmware` |
| `APP_NAME` | str | `"Innovedus AI Playground"` |
| `WINDOW_SIZE` | tuple[int, int] | `(1200, 900)` |
| `BACKGROUND_COLOR` | str | `"#143058"` |
| `MODEL_TIMEOUT` | int | `5000`(毫秒) |
| `FIRMWARE_PATHS` | dict | `{"scpu": "../../res/firmware/fw_scpu.bin", "ncpu": "..."}` ⚠️ 見備注 |
> **⚠️ 備注**`FIRMWARE_PATHS` 使用相對路徑,但在打包後此相對路徑可能無效,實際運作中 `DeviceController.connect_device()` 改用 `os.path.join(FW_DIR, dongle, "fw_scpu.bin")` 計算路徑。`FIRMWARE_PATHS` 常數目前**未被使用**。
#### DeviceType Enum
```python
class DeviceType(Enum):
KL520 = 256 # product_id hex: 0x100
KL720 = 1824 # product_id hex: 0x720
KL720_L = 512 # product_id hex: 0x200 無 DongleModelMap 對應)
KL530 = 530 # (⚠️ 無 DongleModelMap 對應)
KL832 = 832 # (⚠️ 無 DongleModelMap 對應)
KL730 = 732 # (⚠️ 無 DongleModelMap 對應)
KL630 = 630 # (⚠️ 無 DongleModelMap 對應)
KL540 = 540 # (⚠️ 無 DongleModelMap 對應)
```
#### DongleModelMapproduct_id → model name
```python
DongleModelMap = {
"0x100": "KL520",
"0x720": "KL720",
}
# 注意key 為 lowercase hex string含 0x 前綴),如 "0x100"
```
#### DongleIconMapproduct_id → icon 檔名)
```python
DongleIconMap = {
"0x100": "ic_dongle_520.png",
"0x720": "ic_dongle_720.png",
}
```
---
### 1.2 AppControllermain.py
**路徑**`main.py`
#### Class: AppController
```python
class AppController:
app: QApplication
stack: QStackedWidget
selection_screen: SelectionScreen
login_screen: LoginScreen
utilities_screen: UtilitiesScreen
main_window: MainWindow
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `__init__()` | — | — | 初始化 QApplication、QStackedWidget、所有頁面並連接 Signal |
| `init_screens()` | — | None | 建立四個頁面物件並加入 stack |
| `connect_signals()` | — | None | 連接頁面間的 Signal/Slot |
| `show_selection_screen()` | — | None | 切換到 SelectionScreen |
| `show_login_screen()` | — | None | 切換到 LoginScreen |
| `show_utilities_screen()` | — | None | 切換到 UtilitiesScreen |
| `show_demo_app()` | — | None | 切換到 MainWindow |
| `run()` | — | int | 啟動 Qt event loop回傳 exit code |
**⚠️ 已知問題**:所有頁面在 `init_screens()` 時一次性建立,包含 `MainWindow`(內部會啟動相機等重資源操作)。若希望延遲初始化,需要重構。
---
### 1.3 DeviceController
**路徑**`src/controllers/device_controller.py`
#### Class: DeviceController
```python
class DeviceController:
main_window: QWidget # 持有 UI 參考(用於更新 device_list_widget
selected_device: dict | None # 當前選中的裝置dict 格式)
connected_devices: list[dict] # 已解析的裝置列表
device_group: kp.DeviceGroup | None # kp SDK 連接的裝置群組
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `__init__(main_window)` | main_window: QWidget | — | 初始化 |
| `refresh_devices()` | — | bool | 掃描裝置並更新 UITrue = 找到裝置 |
| `parse_and_store_devices(devices)` | devices: list | None | 解析 descriptor 存入 `connected_devices` |
| `display_devices(devices)` | devices: list | None | 更新 `main_window.device_list_widget` |
| `get_devices()` | — | list | 呼叫掃描並回傳 descriptor list |
| `get_selected_device()` | — | dict \| None | 回傳目前選中的裝置 |
| `select_device(device, list_item, list_widget)` | device, item, widget | None | 選取裝置,更新 UI 高亮 |
| `connect_device()` | — | bool | 連接 selected_device 並上傳 firmware |
| `disconnect_device()` | — | bool | 中斷連接,釋放 device_group |
| `get_device_group()` | — | kp.DeviceGroup \| None | 回傳目前的 device_group |
#### connected_devices 元素格式
```python
{
"usb_port_id": int, # USB port 編號(唯一識別)
"product_id": int, # 數字形式的 product_id
"kn_number": int | str, # KN number裝置序號
"dongle": str # 裝置型號名稱,如 "KL520"
}
```
---
### 1.4 InferenceController
**路徑**`src/controllers/inference_controller.py`
#### Class: InferenceController
```python
class InferenceController:
main_window: QWidget
device_controller: DeviceController
inference_worker: InferenceWorkerThread | CustomInferenceWorkerThread | None
inference_queue: queue.Queue # maxsize=5
current_tool_config: dict | None
previous_tool_config: dict | None
_camera_was_active: bool
original_frame_width: int # 預設 640
original_frame_height: int # 預設 480
model_descriptor: kp.ModelDescriptor | None
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `__init__(main_window, device_controller)` | — | — | 初始化,建立 inference_queuemaxsize=5 |
| `select_tool(tool_config)` | tool_config: dict | bool | 選擇標準工具,建立 InferenceWorkerThread |
| `select_custom_tool(tool_config)` | tool_config: dict | bool | 選擇自定義模型,建立 CustomInferenceWorkerThread |
| `_clear_inference_queue()` | — | None | 清空 inference_queue 中所有幀 |
| `add_frame_to_queue(frame)` | frame: np.ndarray | None | 若 queue 未滿,加入幀;同時更新 original_frame_width/height |
| `stop_inference()` | — | None | 停止 inference_worker |
| `process_uploaded_image(file_path)` | file_path: str | None | 清空 queue 後加入指定圖片,供 Image 模式使用 |
#### select_tool() 的 tool_config 期望格式
```python
{
"display_name": str, # 顯示名稱
"mode": str, # 推論模式目錄名稱(對應 utils/{mode}/
"model_name": str, # 模型目錄名稱(對應 utils/{mode}/{model_name}/
"input_info": {
"type": "video" | "image" | "voice"
},
"input_parameters": dict, # 傳入 script.py 的 params
"compatible_devices": list[str], # 如 ["KL520", "KL720"]
"model_file": str, # .nef 檔名(可選,若有則自動上傳模型)
}
```
---
### 1.5 MediaController
**路徑**`src/controllers/media_controller.py`
#### Class: MediaController
```python
class MediaController:
main_window: QWidget
inference_controller: InferenceController
video_thread: VideoThread | None
recording: bool
recording_audio: bool
recorded_frames: list[np.ndarray]
_signal_was_connected: bool
_inference_paused: bool
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `start_camera()` | — | None | 建立 VideoThread 並連接 Signal若已運行則跳過 |
| `stop_camera()` | — | None | 斷開 Signal停止並銷毀 VideoThread |
| `update_image(qt_image)` | qt_image: QImage | None | 接收相機幀,繪製 Bounding Box更新 canvas加入 inference_queue |
| `reconnect_camera_signal()` | — | None | 重新連接已斷開的 change_pixmap_signal |
| `record_video(button=None)` | button: QPushButton \| None | None | 切換錄影狀態;停止時彈出儲存對話框 |
| `record_audio(button=None)` | button: QPushButton \| None | None | 切換錄音狀態 |
| `take_screenshot()` | — | None | 儲存 canvas 當前幀為圖片 |
| `toggle_inference_pause()` | — | bool | 切換推論暫停狀態,回傳新狀態 |
| `is_inference_paused()` | — | bool | 回傳目前是否暫停推論 |
---
### 1.6 InferenceWorkerThread
**路徑**`src/models/inference_worker.py`
#### 模組函數
```python
def load_inference_module(mode: str, model_name: str) -> module:
"""
動態載入 {UTILS_DIR}/{mode}/{model_name}/script.py 並回傳 module 物件。
若檔案不存在importlib 會拋出 FileNotFoundError。
"""
```
#### Class: InferenceWorkerThreadQThread
```python
class InferenceWorkerThread(QThread):
inference_result_signal = pyqtSignal(object) # 回傳 dict | None
frame_queue: queue.Queue
mode: str
model_name: str
min_interval: float # 最小推論間隔(秒)
mse_threshold: float # MSE 閾值,低於此值視為相似幀
_running: bool
once_mode: bool # True = 處理一幀後停止Image 模式)
last_inference_time: float
last_frame: np.ndarray | None
cached_result: object | None
input_params: dict
inference_module: module # 動態載入的 script.py module
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `__init__(frame_queue, mode, model_name, min_interval=0.5, mse_threshold=500, once_mode=False)` | — | — | 初始化並立即載入 inference_module |
| `run()` | — | None | 主迴圈:取幀 → MSE 判斷 → 推論 → emit 結果 |
| `stop()` | — | None | 設 `_running=False``wait()` |
**⚠️ 注意**`InferenceController.select_tool()` 建立 Worker 時,`min_interval` 設定為 **2 秒**(非預設的 0.5 秒):
```python
self.inference_worker = InferenceWorkerThread(
...,
min_interval=2,
mse_threshold=500,
once_mode=once_mode
)
```
#### run() 主迴圈邏輯
```
while _running:
1. frame_queue.get(timeout=0.1) — 若 0.1 秒內無幀則 continue
2. 時間間隔檢查current_time - last_inference_time < min_interval continue
3. MSE 比較(若 last_frame 不為 None 且尺寸相同):
- mse < mse_threshold AND cached_result 不為 None emit cached_result continue
4. inference_module.inference(frame, params=input_params) → result
5. 更新 last_inference_time、last_frame、cached_result
6. result 不為 None → inference_result_signal.emit(result)
7. once_mode = True → _running = Falsebreak
quit()
```
---
### 1.7 CustomInferenceWorkerThread
**路徑**`src/models/custom_inference_worker.py`
#### 輔助 Classes
```python
class ExampleBoundingBox:
x1: int; y1: int; x2: int; y2: int
score: float
class_num: int
def get_member_variable_dict() -> dict
class ExampleYoloResult:
class_count: int
box_count: int
box_list: list[ExampleBoundingBox]
def get_member_variable_dict() -> dict
```
#### 模組函數
```python
def preprocess_frame(frame: np.ndarray, target_size: int = 640) -> tuple[np.ndarray, int, int]:
"""
Args:
frame: BGR 格式 numpy array
target_size: 縮放目標尺寸(預設 640
Returns:
(frame_bgr565, original_width, original_height)
- frame_bgr565: cv2 BGR565 格式
"""
def postprocess(output_list, hw_preproc_info, original_width, original_height,
target_size=640, thresh=0.2) -> ExampleYoloResult:
"""
YOLO V5 後處理主入口
- 呼叫 post_process_yolo_v5()
- 將 bounding box 座標縮放回原始影像尺寸
"""
def post_process_yolo_v5(inference_float_node_output_list, hardware_preproc_info,
thresh_value, with_sigmoid=True) -> ExampleYoloResult:
"""
完整 YOLO V5 後處理:
- sigmoid 激活
- anchor 解碼(使用內建 YOLO_V5_ANCHERS
- padding/縮放補償
- 分數閾值過濾
- NMSper-classIoU threshold=0.5
回傳 ExampleYoloResult
"""
```
#### Class: CustomInferenceWorkerThreadQThread
```python
class CustomInferenceWorkerThread(QThread):
inference_result_signal = pyqtSignal(object) # 回傳 dict | None
frame_queue: queue.Queue
min_interval: float
mse_threshold: float
_running: bool
last_inference_time: float
last_frame: np.ndarray | None
cached_result: object | None
input_params: dict
device_group: kp.DeviceGroup | None # Worker 內部管理的裝置連接
model_descriptor: kp.ModelDescriptor | None
is_initialized: bool
custom_labels: list[str] | None
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `__init__(frame_queue, min_interval=0.5, mse_threshold=500)` | — | — | 初始化 |
| `initialize_device()` | — | bool | 連接裝置、上傳 firmware 和模型(從 input_params 讀取路徑) |
| `run_single_inference(frame)` | frame: np.ndarray | dict \| None | 執行單次 YOLOv5 推論,回傳結果 dict |
| `run()` | — | None | 主迴圈(類似 InferenceWorkerThread但無 once_mode |
| `cleanup()` | — | None | 斷開 device_group |
| `stop()` | — | None | 設 `_running=False``wait()`,然後 `cleanup()` |
#### initialize_device() 的 input_params 期望 Keys
```python
input_params = {
"custom_model_path": str, # .nef 完整路徑
"custom_scpu_path": str, # fw_scpu.bin 完整路徑
"custom_ncpu_path": str, # fw_ncpu.bin 完整路徑
"usb_port_id": int, # USB port 編號
"custom_labels": list[str] | None # 自定義類別名稱(可選)
}
```
#### run_single_inference() 回傳格式
```python
{
"num_boxes": int,
"bounding boxes": [[x1, y1, x2, y2], ...], # 注意key 有空格
"results": ["label1", "label2", ...]
}
```
**⚠️ 注意**key 為 `"bounding boxes"`(有空格),`MainWindow.handle_inference_result()` 中對應的處理 key 也是 `"bounding boxes"`(有空格),需保持一致。
#### YOLOv5 後處理常數
```python
YOLO_V3_CELL_BOX_NUM = 3
NMS_THRESH_YOLOV5 = 0.5
YOLO_MAX_DETECTION_PER_CLASS = 100
YOLO_V5_ANCHERS = np.array([
[[10, 13], [16, 30], [33, 23]],
[[30, 61], [62, 45], [59, 119]],
[[116, 90], [156, 198], [373, 326]]
])
```
---
### 1.8 VideoThread
**路徑**`src/models/video_thread.py`
#### Class: VideoThreadQThread
```python
class VideoThread(QThread):
change_pixmap_signal = pyqtSignal(QImage) # 每幀觸發
camera_error_signal = pyqtSignal(str) # 目前未被任何 Slot 連接
_run_flag: bool
_camera_open_attempts: int
_max_attempts: int # = 3
_camera_timeout: int # = 5
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `_open_camera_with_timeout(camera_index, backend=None)` | camera_index: int, backend | cv2.VideoCapture \| None | 在 daemon thread 中開啟相機5 秒 timeout |
| `run()` | — | None | 嘗試最多 3 次開啟相機;成功後進入幀擷取迴圈 |
| `stop()` | — | None | 設 `_run_flag=False``wait()` |
#### 相機設定
```python
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 最小緩衝以降低延遲
```
開啟順序:
1. 優先嘗試 DirectShow backend`cv2.CAP_DSHOW`
2. 失敗則嘗試預設 backend
3. 兩者都失敗則等 1 秒後重試(最多 3 次)
幀格式轉換:
```
BGROpenCV 預設) → RGBcv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
→ QImage.Format_RGB888
→ change_pixmap_signal.emit(qt_image.copy())
```
---
### 1.9 DeviceService
**路徑**`src/services/device_service.py`
#### Class: EmptyDescriptor
```python
class EmptyDescriptor:
device_descriptor_number: int = 0
device_descriptor_list: list = []
```
#### 函數
```python
def check_available_device(timeout: float = 5.0) -> kp.DeviceDescriptors | EmptyDescriptor:
"""
在 daemon thread 中執行 kp.core.scan_devices(),設 timeout=5.0 秒。
Returns:
- 成功kp.DeviceDescriptors含 device_descriptor_number 和 device_descriptor_list
- 失敗/Timeout/無裝置EmptyDescriptordevice_descriptor_number = 0
注意事項:
- 使用 threading.Thread非 QThread
- thread 設為 daemon=True即使 timeout 後 thread 仍可能繼續執行
"""
```
---
### 1.10 FileService
**路徑**`src/services/file_service.py`
#### Class: FileService
```python
class FileService:
main_window: QWidget
upload_dir: str # = UPLOAD_DIR%LOCALAPPDATA%/Kneron_Academy/uploads
destination: str | None # 最近上傳的檔案完整路徑
_camera_was_active: bool
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `upload_file()` | — | str \| None | 開啟檔案選擇器,複製檔案,觸發推論;回傳目的路徑 |
| `show_message(icon, title, message)` | — | None | 顯示自訂樣式的 QMessageBox |
#### upload_file() 完整流程
```
1. 若相機正在運行:
a. 暫停推論toggle_inference_pause()
b. 斷開 change_pixmap_signal保留 VideoThread不停止
2. QFileDialog.getOpenFileName()
3. 若使用者取消 → return None
4. 確認/建立 upload_dir
5. 檢查來源檔案存在
6. 測試目的地寫入權限
7. shutil.copy2(source, destination)
8. 更新 main_window.destination
9. 若 inference_controller.current_tool_config 存在:
a. 讀取並顯示圖片於 canvas_label
b. inference_controller.process_uploaded_image(destination)
10. finally若相機之前活動重新連接 Signal 並恢復推論
```
---
### 1.11 ConfigUtils
**路徑**`src/utils/config_utils.py`
#### Class: ConfigUtils
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `generate_global_config()` | — | dict | 掃描 UTILS_DIR產生 config.json |
| `create_model_config_template(model_path)` | model_path: str | bool | 為指定模型目錄建立 config.json 範本 |
#### generate_global_config() 輸出範例
```json
{
"plugins": [
{
"mode": "object_detection",
"display_name": "Object Detection",
"models": [
{
"name": "yolov5_coco",
"display_name": "Yolov5 Coco",
"description": "YOLOv5 COCO object detection",
"compatible_devices": ["KL520", "KL720"]
}
]
}
]
}
```
---
### 1.12 image_utils
**路徑**`src/utils/image_utils.py`
```python
def qimage_to_numpy(qimage: QImage) -> np.ndarray:
"""
將 QImage 轉換為 numpy array。
先強制轉換為 QImage.Format_RGB888確保格式一致
Returns:
np.ndarray形狀 (H, W, 3)dtype=uint8RGB 格式
注意:使用 qimage.bits() 直接存取記憶體,需確保 qimage 在 array 使用期間有效。
"""
```
---
## 2. InferenceWorkerThread 與 script.py 介面規範
### 2.1 script.py 必須實作的函數
每個 Plugin 的 `script.py` **必須** 實作以下函數:
```python
def inference(frame: np.ndarray, params: dict) -> dict | None:
...
```
`InferenceWorkerThread` 呼叫方式:
```python
result = self.inference_module.inference(frame, params=self.input_params)
```
### 2.2 `frame` 參數規格
| 屬性 | 值 |
|------|-----|
| 型別 | `np.ndarray` |
| 形狀 | `(H, W, 3)` |
| dtype | `uint8` |
| 色彩空間 | RGBVideoThread 輸出)|
| 典型尺寸 | 640 x 480 |
### 2.3 `params` 字典完整 Keys
以下 keys 由 `InferenceController.select_tool()` 注入,加上 model `config.json``input_parameters`
| Key | 型別 | 必填 | 說明 |
|-----|------|------|------|
| `device_group` | kp.DeviceGroup \| None | 是 | kp SDK 連接的裝置群組 |
| `usb_port_id` | int | 是 | USB port 編號 |
| `scpu_path` | str | 否 | SCPU firmware 完整路徑 |
| `ncpu_path` | str | 否 | NCPU firmware 完整路徑 |
| `model` | str | 否 | .nef 模型完整路徑(若 config.json 有 model_file |
| `model_descriptor` | kp.ModelDescriptor \| None | 否 | 已上傳的模型描述(若有 model_file |
| `file_path` | str | 否 | image/voice 模式的上傳檔案路徑 |
| (自訂)| any | 否 | 來自 model config.json 的 `input_parameters` 欄位 |
### 2.4 回傳值格式
#### 格式 A無 Bounding Box分類/其他結果)
觸發 `QMessageBox` 顯示:
```python
return {
"result": "class_name",
# 任意 key-value 對
}
```
#### 格式 B單一 Bounding Box舊格式仍相容
直接繪製於畫面:
```python
return {
"bounding box": [x1, y1, x2, y2], # 整數,像素座標
"result": "class_label" # 可選
}
```
#### 格式 C多個 Bounding Box推薦格式
直接繪製於畫面:
```python
return {
"bounding boxes": [ # 注意 key 有空格
[x1, y1, x2, y2], # 整數,像素座標
[x1, y1, x2, y2],
...
],
"results": [ # 可選,與 bounding boxes 對應
"label1",
"label2",
...
]
}
```
#### 回傳 None
表示此幀跳過,不更新 Bounding Box不顯示 QMessageBox。
---
## 3. 裝置連接 API 流程kp SDK 呼叫順序)
### 3.1 DeviceController.connect_device() 流程
```python
# 1. 掃描(在 refresh_devices() 時已完成)
descriptors = kp.core.scan_devices()
# descriptors.device_descriptor_number: int
# descriptors.device_descriptor_list: list
# 2. 連接
device_group = kp.core.connect_devices(usb_port_ids=[usb_port_id])
# 3. 上傳 firmware
kp.core.load_firmware_from_file(
device_group=device_group,
scpu_fw_path=scpu_path, # 完整路徑
ncpu_fw_path=ncpu_path # 完整路徑
)
# 4. 上傳模型(在 InferenceController.select_tool() 中,非 DeviceController
model_descriptor = kp.core.load_model_from_file(
device_group=device_group,
file_path=model_file_path
)
```
### 3.2 CustomInferenceWorkerThread.initialize_device() 流程
```python
# 完整的連接 + firmware + model 上傳
device_group = kp.core.connect_devices(usb_port_ids=[port_id])
kp.core.set_timeout(device_group=device_group, milliseconds=5000)
kp.core.load_firmware_from_file(device_group, scpu_path, ncpu_path)
model_descriptor = kp.core.load_model_from_file(device_group, file_path=model_path)
```
### 3.3 推論 API 呼叫順序CustomInferenceWorkerThread
```python
# 建立推論描述符
descriptor = kp.GenericImageInferenceDescriptor(
model_id=model_descriptor.models[0].id,
inference_number=0,
input_node_image_list=[
kp.GenericInputNodeImage(
image=img_bgr565,
image_format=kp.ImageFormat.KP_IMAGE_FORMAT_RGB565,
resize_mode=kp.ResizeMode.KP_RESIZE_ENABLE,
padding_mode=kp.PaddingMode.KP_PADDING_CORNER,
normalize_mode=kp.NormalizeMode.KP_NORMALIZE_KNERON
)
]
)
# 非同步發送推論請求
kp.inference.generic_image_inference_send(device_group, descriptor)
# 接收推論結果(阻塞式)
result = kp.inference.generic_image_inference_receive(device_group)
# result.header.num_output_node: int
# result.header.hw_pre_proc_info_list: list
# 取得每個輸出節點的 float 值
for node_idx in range(result.header.num_output_node):
node_output = kp.inference.generic_inference_retrieve_float_node(
node_idx=node_idx,
generic_raw_result=result,
channels_ordering=kp.ChannelOrdering.KP_CHANNEL_ORDERING_CHW
)
# node_output.shape: (batch, channels, height, width)
# node_output.ndarray: numpy array
```
### 3.4 中斷連接
```python
kp.core.disconnect_devices(device_group=device_group)
```
---
## 4. 推論 Queue 設計
### 4.1 基本規格
| 屬性 | 值 |
|------|-----|
| 類型 | `queue.Queue` |
| maxsize | **5** |
| 生產者 | `MediaController.update_image()`VideoThread 幀)、`InferenceController.process_uploaded_image()`(圖片)、`InferenceController.select_tool()`(初始圖片) |
| 消費者 | `InferenceWorkerThread.run()``CustomInferenceWorkerThread.run()` |
### 4.2 滿了怎麼辦drop policy
當 queue 滿時,**新幀被靜默丟棄**
```python
# add_frame_to_queue() 的邏輯
if not self.inference_queue.full():
self.inference_queue.put(frame)
else:
print("Warning: inference queue is full") # 只有 printUI 不更新
```
**⚠️ 問題**:這是 `put_nowait` 語意,但實際上是 `if not full` 檢查後再 `put`,在多執行緒環境中存在微小的 race windowcheck-then-act
### 4.3 清空時機
以下情況會清空 queue
| 觸發 | 方法 | 說明 |
|------|------|------|
| 切換工具 | `InferenceController.select_tool()` 開頭 | 避免舊工具的幀被新 Worker 處理 |
| 切換自定義工具 | `InferenceController.select_custom_tool()` 開頭 | 同上 |
| 上傳圖片 | `InferenceController.process_uploaded_image()` 開頭 | 確保只處理最新上傳的圖片 |
### 4.4 Worker 從 Queue 取幀的行為
```python
frame = self.frame_queue.get(timeout=0.1)
# - 若 0.1 秒無幀,拋出 queue.Empty → continue
# - 不 blocking僅等 0.1 秒)
# - 不呼叫 task_done()queue 未使用 join 功能)
```
---
## 5. Signal/Slot 對應表
### 5.1 頁面導航 SignalAppController 層)
| Signal | 發出 Class | Slot | 說明 |
|--------|-----------|------|------|
| `open_utilities` | SelectionScreen | AppController.show_login_screen | 點擊 Utilities 卡片 |
| `open_demo_app` | SelectionScreen | AppController.show_demo_app | 點擊 Demo App 卡片 |
| `login_success` | LoginScreen | AppController.show_utilities_screen | 登入成功 |
| `back_to_selection` | LoginScreen | AppController.show_selection_screen | 點擊 Back |
| `back_to_selection` | UtilitiesScreen | AppController.show_selection_screen | 點擊 Back |
### 5.2 媒體與推論 SignalMainWindow 層)
| Signal | 型別 | 發出 Class | Slot | 說明 |
|--------|------|-----------|------|------|
| `change_pixmap_signal` | QImage | VideoThread | MediaController.update_image | 相機每幀 |
| `camera_error_signal` | str | VideoThread | **未連接** | ⚠️ 無 Slot 接收 |
| `inference_result_signal` | object | InferenceWorkerThread | MainWindow.handle_inference_result | 推論完成 |
| `inference_result_signal` | object | CustomInferenceWorkerThread | MainWindow.handle_inference_result | 推論完成 |
**⚠️ 注意**`VideoThread.camera_error_signal` 目前沒有任何 Slot 連接,相機錯誤不會通知 UI。
### 5.3 UtilitiesScreen 內部 Signal
| Signal | 發出 Class | 說明 |
|--------|-----------|------|
| `back_to_selection` | UtilitiesScreen | 點擊 Back 按鈕 |
---
## 6. config.json 完整 Schema 定義
### 6.1 全域 config.jsonSCRIPT_CONFIG
路徑:`%LOCALAPPDATA%/Kneron_Academy/utils/config.json`
`ConfigUtils.generate_global_config()` 自動產生,不應手動修改。
```json
{
"plugins": [
{
"mode": "string", // 模式目錄名稱(如 "object_detection"
"display_name": "string", // UI 顯示名稱(自動從 mode 名稱產生)
"models": [
{
"name": "string", // 模型目錄名稱(如 "yolov5_coco"
"display_name": "string", // UI 顯示名稱(來自 model config.json
"description": "string", // 描述(來自 model config.json
"compatible_devices": ["string"] // 如 ["KL520", "KL720"]
}
]
}
]
}
```
### 6.2 模型層級 config.json
路徑:`%LOCALAPPDATA%/Kneron_Academy/utils/{mode}/{model_name}/config.json`
由 Plugin 開發者提供。
```json
{
"display_name": "string", // 必填UI 顯示名稱
"description": "string", // 選填:模型描述
"model_file": "string", // 選填:.nef 檔名(若有則 InferenceController 自動上傳)
"input_info": {
"type": "video" | "image" | "voice", // 必填:輸入類型
"supported_formats": ["string"] // 選填:支援的格式(目前主要用於文件)
},
"input_parameters": {
// 選填:任意 key-value會被合併進 input_params 傳給 script.py
"threshold": 0.5,
// 可包含任何 script.py 需要的參數
},
"compatible_devices": ["string"] // 必填:如 ["KL520", "KL720"]
}
```
### 6.3 ConfigUtils.create_model_config_template() 產生的範本
```json
{
"display_name": "Model Name",
"description": "AI model for model_name",
"model_file": "model_name.nef",
"input_info": {
"type": "video",
"supported_formats": ["mp4", "avi"]
},
"input_parameters": {
"threshold": 0.5
},
"compatible_devices": ["KL520", "KL720"]
}
```
---
## 7. 已知限制與邊界條件
### 7.1 裝置相關限制
| 限制 | 描述 |
|------|------|
| 僅支援 KL520 / KL720 | `DongleModelMap` 只有這兩個映射;其他型號被標注為 "unknown" |
| 單一裝置 | `connect_devices(usb_port_ids=[single_id])`,不支援多裝置並行推論 |
| Firmware 必須預先放置 | `connect_device()` 不自動下載 firmware`%LOCALAPPDATA%/Kneron_Academy/firmware/{dongle}/` 下不存在,回傳 False |
| kp SDK timeout | `CustomInferenceWorkerThread.initialize_device()` 設定 timeout=5000ms`DeviceController.connect_device()` 未設定 timeout |
### 7.2 相機相關限制
| 限制 | 描述 |
|------|------|
| 僅支援 camera index 0 | `cv2.VideoCapture(0, cv2.CAP_DSHOW)` — 硬體寫死 |
| 最多嘗試 3 次 | `_max_attempts=3`;若 3 次後仍無法開啟Worker 靜默退出,無 UI 通知 |
| 解析度固定 640x480 | `cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)` 固定設定 |
| camera_error_signal 未連接 | 相機錯誤只有 print使用者不知道相機失敗 |
### 7.3 推論相關限制
| 限制 | 描述 |
|------|------|
| Custom Model 僅支援 YOLOv5 | `CustomInferenceWorkerThread` 的後處理 hardcode 了 YOLOv5 的 anchor 和 NMS 邏輯 |
| COCO 80 類 | 預設使用 COCO 80 類別;需自定義類別時必須傳入 `custom_labels` |
| Voice 模式未完整實作 | `input_info.type = "voice"` 的路徑只傳 `file_path`,無音訊處理邏輯在主程式 |
| Image 模式一次只能處理一張 | `once_mode=True` 後 Worker 自動停止,需重新建立才能推論下一張 |
| MSE 比較跨 frame 尺寸變化 | 若相機解析度切換MSE 比較因形狀不一致而 reset功能正確但可能不符預期 |
| Script module 快取問題 | `load_inference_module()` 每次切換工具都重新 `exec_module()`若同一模組切換多次Python module cache 可能有舊版本 |
### 7.4 Plugin 開發注意事項
| 事項 | 說明 |
|------|------|
| script.py 執行在 Worker Thread 中 | 不可在 script.py 中直接呼叫 PyQt UI 方法 |
| script.py 不應長時間阻塞 | 若推論超過 `min_interval`,下一幀會等待 queue 中的幀累積 |
| Bounding Box 座標系 | 座標為相對於 `frame`QImage 轉出的 numpy array的像素座標若輸入解析度是 640x480座標範圍為 0-639 / 0-479 |
| script.py 例外處理 | `InferenceWorkerThread``try/except` 會捕捉 `script.inference()` 的例外,設 result=None 並繼續;不會中斷 Worker |
### 7.5 打包相關邊界條件
| 條件 | 問題 |
|------|------|
| PyInstaller + kp SDK | kp 為 C Extension需在 `.spec` 中手動設定 `hiddenimports` |
| UXUI_ASSETS 路徑 | 開發模式下透過 `__file__` 計算;打包後需改用 `sys._MEIPASS` |
| `FIRMWARE_PATHS` 常數 | 使用相對路徑,打包後無效;實際使用的是 `FW_DIR` + dongle 名稱(正確) |
| script.py 動態載入 | Plugin 的 `script.py` 必須放在 `%LOCALAPPDATA%`(外部目錄),不能打包進 exe |

View File

@ -1,569 +0,0 @@
# Design Doc: KNEO AcademyInnovedus AI Playgroundv2.0
**作者**Architect Agent
**狀態**Draft從既有程式碼反向整理
**日期**2026-04-04
**產品版本**v2.0
---
## 1. 背景與目標
### 1.1 產品概述
KNEO Academy對外名稱Innovedus AI Playground是一套 Windows 桌面應用程式,讓擁有 Kneron NPU USB Dongle 的使用者能夠在本機端執行 Edge AI 推論,無需雲端服務、無需撰寫程式碼。
**核心使用情境**
- 業務展示:即插即用,開箱展示 AI 推論能力
- 研發驗證:快速測試 Kneron NPU 在特定任務的推論效果
- 客戶自訂:上傳自訂 `.nef` 模型進行推論測試
### 1.2 設計目標
- **即插即用**:連接 Kneron dongle 後,數秒內可開始推論
- **Plugin 化架構**:透過目錄結構 + `config.json` + `script.py` 新增模型,不需修改主程式
- **Thread 隔離**UI 執行緒與推論執行緒完全分離,確保畫面不卡頓
- **PyInstaller 相容**:可打包為單一可執行檔,方便分發
### 1.3 非目標Out of Scope
- 雲端推論或 API 服務
- 多裝置同時推論
- 行動平台支援
---
## 2. 整體架構概覽
### 2.1 設計模式
本應用程式採用 **MVCModel-View-Controller** 架構,搭配 Qt 的 **Signal/Slot** 機制實現跨執行緒通訊。
```
┌─────────────────────────────────────────────────────────────────┐
│ Views呈現層
│ SelectionScreen LoginScreen MainWindow UtilitiesScreen │
│ QWidget QWidget QWidget QWidget │
└────────────────────────────┬────────────────────────────────────┘
│ Signal/Slot
┌────────────────────────────▼────────────────────────────────────┐
│ AppControllermain.py
│ QStackedWidget — 頁面路由中樞 │
└──────┬──────────────────┬───────────────────┬───────────────────┘
│ │ │
┌──────▼──────┐ ┌────────▼────────┐ ┌──────▼──────────┐
│ Device │ │ Inference │ │ Media │
│ Controller │ │ Controller │ │ Controller │
│ (裝置管理) │ │ (推論管理) │ │ (相機/媒體) │
└──────┬──────┘ └───────┬─────────┘ └──────┬──────────┘
│ │ │
┌──────▼──────┐ ┌───────▼─────────┐ ┌──────▼──────────┐
│ device_ │ │ InferenceWorker │ │ VideoThread │
│ service.py │ │ Thread │ │ QThread
kp SDK │ │ QThread │ │ OpenCV 擷取) │
└─────────────┘ └────────┬────────┘ └─────────────────┘
│ 動態載入
┌────────▼────────┐
│ script.py │
Plugin 推論) │
└─────────────────┘
```
### 2.2 頁面導航架構
AppController 使用 `QStackedWidget` 作為根容器,所有頁面在啟動時一次性初始化,透過 `setCurrentWidget()` 切換顯示,不需重新建立物件。
```
AppController
└── QStackedWidgetstack
├── [index 0] SelectionScreen ← 預設顯示
├── [index 1] LoginScreen
├── [index 2] UtilitiesScreen
└── [index 3] MainWindow
```
**Signal 連接關係(頁面切換)**
| 發出者 | Signal | 接收者Slot | 效果 |
|--------|--------|--------------|------|
| SelectionScreen | `open_utilities` | `AppController.show_login_screen` | 跳至登入頁 |
| SelectionScreen | `open_demo_app` | `AppController.show_demo_app` | 跳至主視窗 |
| LoginScreen | `login_success` | `AppController.show_utilities_screen` | 登入成功,進入工具頁 |
| LoginScreen | `back_to_selection` | `AppController.show_selection_screen` | 返回首頁 |
| UtilitiesScreen | `back_to_selection` | `AppController.show_selection_screen` | 返回首頁 |
### 2.3 MainWindow 內部架構
MainWindow 是 AI Demo 推論的核心容器,內部持有三個 Controller 組成協作關係:
```
MainWindowQWidget
├── DeviceController — 管理 kp SDK 裝置連接
├── InferenceController — 管理推論 Worker Thread 與 Queue
│ └── inference_queuequeue.Queue, maxsize=5
└── MediaController — 管理相機擷取與畫面更新
└── VideoThreadQThread
```
---
## 3. 模組依賴圖
```
main.pyAppController
├── views/selection_screen.pySelectionScreen
│ └── config.py
├── views/login_screen.pyLoginScreen
│ └── config.py
├── views/utilities_screen.pyUtilitiesScreen
│ ├── config.py
│ ├── controllers/device_controller.pyDeviceController
│ └── services/device_service.pycheck_available_device
└── views/mainWindows.pyMainWindow
├── config.py
├── controllers/device_controller.pyDeviceController
│ ├── config.py
│ └── services/device_service.py
├── controllers/inference_controller.pyInferenceController
│ ├── config.py
│ ├── models/inference_worker.pyInferenceWorkerThread
│ │ ├── config.py
│ │ └── [動態載入] utils/{mode}/{model}/script.py
│ └── models/custom_inference_worker.pyCustomInferenceWorkerThread
├── controllers/media_controller.pyMediaController
│ ├── models/video_thread.pyVideoThread
│ └── utils/image_utils.py
├── services/file_service.pyFileService
└── utils/config_utils.pyConfigUtils
```
**注意**`UtilitiesScreen` 建立了**自己的** `DeviceController` 實例(與 `MainWindow` 的是不同物件),兩者不共享裝置狀態。
---
## 4. 資料流程圖
### 4.1 Video 即時推論流程
```
相機硬體
│ 每幀(~30fps
VideoThread.run()
│ QImage
│ change_pixmap_signal.emit(qt_image)
MediaController.update_image(qt_image)
├── 1. 繪製 Bounding Box → canvas_label.setPixmap(pixmap)
└── 2. qimage_to_numpy(qt_image) → frame_np
InferenceController.add_frame_to_queue(frame_np)
│ 若 queue 未滿maxsize=5
inference_queue.put(frame_np)
InferenceWorkerThread.run()
├── 1. MSE 比較(與前一幀)→ 差異不大時emit 快取結果
├── 2. 時間間隔檢查min_interval=2秒
└── 3. script.inference(frame, params) → result
│ inference_result_signal.emit(result)
MainWindow.handle_inference_result(result)
├── 若有 "bounding box"/"bounding boxes"
│ → 更新 current_bounding_boxes下一幀繪製
└── 若無 bounding box → QMessageBox 彈出顯示
```
### 4.2 Image 推論流程
```
使用者點擊「Upload」
FileService.upload_file()
├── 1. 暫停相機 Signaldisconnect change_pixmap_signal
├── 2. QFileDialog 選檔
├── 3. shutil.copy2() → %LOCALAPPDATA%/Kneron_Academy/uploads/
├── 4. 顯示圖片於 canvas_label
└── 5. InferenceController.process_uploaded_image(file_path)
_clear_inference_queue()
inference_queue.put(img)
InferenceWorkerThreadonce_mode=True
│ 只處理一幀後停止
script.inference(frame, params) → result
│ inference_result_signal.emit(result)
MainWindow.handle_inference_result(result)
```
### 4.3 Custom Model 推論流程
```
使用者提供:
- custom_model_path.nef 檔)
- custom_scpu_pathfw_scpu.bin
- custom_ncpu_pathfw_ncpu.bin
- custom_labels可選
InferenceController.select_custom_tool(tool_config)
CustomInferenceWorkerThreadQThread
├── initialize_device()(首次執行時)
│ ├── kp.core.connect_devices([port_id])
│ ├── kp.core.load_firmware_from_file(scpu, ncpu)
│ └── kp.core.load_model_from_file(model_path)
└── run_single_inference(frame)
├── preprocess_frame()resize to 640, BGR → BGR565
├── kp.GenericImageInferenceDescriptor
├── kp.inference.generic_image_inference_send()
├── kp.inference.generic_image_inference_receive()
├── kp.inference.generic_inference_retrieve_float_node()
└── post_process_yolo_v5() → ExampleYoloResult
│ inference_result_signal.emit(result_dict)
MainWindow.handle_inference_result()
```
**注意**`CustomInferenceWorkerThread` 在 Worker Thread 內部**自行連接/重置裝置**,與 `DeviceController` 管理的 `device_group` 是**不同的連接**。這是一個雙重連接問題(見第 8 節技術問題)。
---
## 5. Thread 架構
### 5.1 執行緒關係圖
```
Qt Main ThreadUI Thread
├── AppControllerQStackedWidget 管理)
├── MainWindowUI 事件處理)
│ ├── handle_inference_result() ← 由 Signal 呼叫(執行在主執行緒)
│ └── update_image() via MediaController ← 由 Signal 呼叫(執行在主執行緒)
├── VideoThreadQThread #1
│ └── 職責相機擷取、QImage 轉換、emit change_pixmap_signal
│ └── 內部threading.Thread用於相機開啟 timeout 機制)
├── InferenceWorkerThreadQThread #2
│ └── 職責:從 queue 取幀、MSE 比較、呼叫 script.inference()、emit 結果
└── CustomInferenceWorkerThreadQThread #3,替代 InferenceWorkerThread
└── 職責device init、kp 推論、YOLOv5 後處理、emit 結果
```
### 5.2 裝置掃描的執行緒
`check_available_device()` 使用 `threading.Thread`(非 QThread執行 `kp.core.scan_devices()`,並設 5 秒 timeout
```
check_available_device() in Main Thread
└── threading.Threaddaemon=True
└── kp.core.scan_devices()(阻塞式 SDK 呼叫)
thread.join(timeout=5.0)
```
同樣地,`VideoThread._open_camera_with_timeout()` 也使用 `threading.Thread` 開啟相機timeout 為 5 秒。
### 5.3 跨執行緒通訊
所有跨執行緒通訊均透過 Qt Signal/Slot 機制Qt 保證跨執行緒的 Signal 會在接收執行緒的 Event Loop 中排隊執行:
| Signal | 發出執行緒 | 接收執行緒Slot |
|--------|-----------|----------------|
| `VideoThread.change_pixmap_signal` | VideoThread | Main Thread`MediaController.update_image` |
| `InferenceWorkerThread.inference_result_signal` | InferenceWorkerThread | Main Thread`MainWindow.handle_inference_result` |
| `CustomInferenceWorkerThread.inference_result_signal` | CustomInferenceWorkerThread | Main Thread`MainWindow.handle_inference_result` |
---
## 6. Plugin 系統設計
### 6.1 架構概念
Plugin 系統讓 Kneron 或第三方可以透過放置目錄和設定檔來新增 AI 工具,完全不需修改主程式碼。
### 6.2 目錄結構
```
%LOCALAPPDATA%/Kneron_Academy/utils/
├── config.json ← 全域 Plugin 索引(自動產生)
├── {mode_name}/ ← 推論模式目錄(如 object_detection
│ └── {model_name}/ ← 模型目錄(如 yolov5_person
│ ├── config.json ← 模型設定
│ ├── script.py ← 推論腳本Plugin 核心)
│ └── {model_name}.nef ← Kneron 模型檔
```
### 6.3 Plugin 載入流程
```
應用程式啟動
ConfigUtils.generate_global_config()
├── 掃描 utils/ 下所有 mode 目錄(跳過 _ 開頭的目錄)
├── 掃描每個 mode 下所有 model 目錄
├── 讀取每個 model/config.json
└── 輸出 utils/config.jsonPlugin 索引)
使用者選擇工具
InferenceController.select_tool(tool_config)
InferenceWorkerThread.__init__()
└── load_inference_module(mode, model_name)
└── importlib.util.spec_from_file_location()
→ 動態 import utils/{mode}/{model}/script.py
```
### 6.4 `script.py` 介面規範
每個 Plugin 的 `script.py` 必須實作以下介面:
```python
def inference(frame: np.ndarray, params: dict) -> dict | None:
"""
Args:
frame: 影像幀numpy array形狀 (H, W, 3)RGB 格式
params: 推論參數字典(詳見 config.json schema
Returns:
dict 或 NoneNone 表示跳過此幀)
支援的回傳格式(在 handle_inference_result 中處理):
格式 A單一 Bounding Box
{
"bounding box": [x1, y1, x2, y2],
"result": "class_label"
}
格式 B多個 Bounding Box推薦
{
"bounding boxes": [[x1, y1, x2, y2], ...],
"results": ["label1", "label2", ...]
}
格式 C任意分類結果彈出 QMessageBox 顯示)
{
"key": "value",
...
}
"""
```
### 6.5 `params` 字典的內容
`InferenceController.select_tool()` 在建立 `InferenceWorkerThread` 前,會將以下資訊注入 `input_params`
| Key | 型別 | 來源 |
|-----|------|------|
| `device_group` | kp.DeviceGroup | DeviceController |
| `usb_port_id` | int | 已連接裝置 |
| `scpu_path` | str | firmware 路徑 |
| `ncpu_path` | str | firmware 路徑 |
| `model` | str | model .nef 完整路徑 |
| `model_descriptor` | kp.ModelDescriptor | 已上傳的模型描述 |
| `file_path` | str | 圖片/聲音模式的上傳檔案路徑 |
| (其他) | any | 來自 model config.json 的 `input_parameters` |
---
## 7. 資料存放設計
### 7.1 執行期資料目錄
全部存放於 Windows 的 `%LOCALAPPDATA%\Kneron_Academy\`
```
%LOCALAPPDATA%\Kneron_Academy\
├── utils\
│ ├── config.json ← Plugin 全域索引(啟動時自動產生)
│ ├── {mode}\
│ │ └── {model}\
│ │ ├── config.json ← 模型設定
│ │ ├── script.py ← 推論腳本
│ │ └── *.nef ← 模型檔
│ └── ...
├── uploads\ ← 使用者上傳的圖片(不自動清理)
│ └── *.jpg / *.png / *.wav / ...
└── firmware\
├── KL520\
│ ├── fw_scpu.bin
│ └── fw_ncpu.bin
└── KL720\
├── fw_scpu.bin
└── fw_ncpu.bin
```
### 7.2 靜態 UI 資源
打包在應用程式內(`uxui/` 目錄),路徑透過 `PROJECT_ROOT` 常數計算:
```python
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "")
```
**PyInstaller 注意**:打包後 `__file__` 的位置會改變,需確認 `UXUI_ASSETS` 路徑在打包後仍正確(詳見第 8.3 節)。
---
## 8. 打包架構PyInstaller
### 8.1 打包工具鏈
- **PyInstaller 6.12.0**:將 Python 應用打包為 Windows .exe
- **Inno Setup**`dist/test.iss`):製作 Windows 安裝包 (.exe installer)
- **PyArmor**(計畫中):混淆/加密 Python 原始碼
### 8.2 打包注意事項
| 項目 | 問題 | 解決方向 |
|------|------|---------|
| `kp` SDK | kp 是 C Extension需確認是否能被 PyInstaller 正確打包 | 需設定 `hiddenimports``binaries` |
| 動態 import | `importlib.util.spec_from_file_location()` 在打包後需從外部路徑載入 | `script.py` 必須放在 `%LOCALAPPDATA%`,不能打包進 exe |
| `UXUI_ASSETS` 路徑 | 打包後 `__file__` 指向臨時目錄 | 需在 `.spec` 中設定 `datas`,並使用 `sys._MEIPASS` 處理路徑 |
| OpenCV | OpenCV 需包含 DLL | 通常 PyInstaller 能自動偵測 |
### 8.3 目錄結構(打包後)
```
安裝目錄/
├── Innovedus AI Playground.exe ← 主執行檔
├── uxui/ ← 靜態資源(需隨 exe 一起安裝)
└── ...
%LOCALAPPDATA%\Kneron_Academy\ ← 使用者資料(安裝時建立)
├── utils/
├── uploads/
└── firmware/
```
---
## 9. 已知技術問題 / 技術債
### 9.1 雙重裝置連接(⚠️ 嚴重)
**問題**`CustomInferenceWorkerThread` 在 Worker Thread 內部調用 `kp.core.connect_devices()`,但 `DeviceController` 可能已經對同一個 `usb_port_id` 建立了連接(在 MainWindow 流程中)。
**影響**:可能導致 kp SDK 報告「裝置已被連接」的錯誤,或產生未定義行為。
**建議**`CustomInferenceWorkerThread` 應改為接受外部傳入的 `device_group`,而非自行連接。
### 9.2 UtilitiesScreen 的 DeviceController 孤立問題(⚠️ 中度)
**問題**`UtilitiesScreen` 建立了自己的 `DeviceController(self)` 實例,與 `MainWindow``DeviceController` 完全獨立,兩者各自管理自己的 `device_group`
**影響**:使用者在 UtilitiesScreen 連接裝置後,切換到 MainWindow 並不知道裝置已連接;反之亦然。
**建議**:將 `DeviceController` 提升到 `AppController` 層級,作為共享的單例。
### 9.3 推論 Queue 丟幀而不通知(⚠️ 中度)
**問題**`inference_queue``maxsize=5`,當 queue 滿時,`add_frame_to_queue()` 靜默丟棄幀(只印 print不通知 UI
**影響**:在高推論延遲時,使用者不知道有幀被丟棄,可能誤以為推論仍在即時進行。
**建議**:新增 UI 指示推論 queue 壓力如幀率顯示、lag 指示)。
### 9.4 LoginScreen 的驗證邏輯未實作(⚠️ 中度)
**問題**`LoginScreen.attempt_login()` 的實際 Server 驗證邏輯未實作,目前只要輸入任何非空帳密就會成功登入。
```python
# 目前實作(不安全)
if not username or not password:
self.show_error("Please enter both username and password")
return
self.login_success.emit() # 永遠成功
```
**建議**:需補齊 Server 端驗證 API 呼叫。
### 9.5 debug print 語句散落各處(低優先)
**問題**:各 Controller 和 Thread 中有大量 `print()` 呼叫作為 debug 輸出,打包後仍會執行(輸出被丟棄,但有效能成本)。
**建議**:改用 Python 的 `logging` 模組,並設定適當的 log level。
### 9.6 `custom_inference_worker.py` 中的 `kp` 全域引用問題(⚠️ 中度)
**問題**`_boxes_scale()``post_process_yolo_v5()` 函數的型別標注直接引用 `kp.HwPreProcInfo``kp.InferenceFloatNodeOutput`(如 `def _boxes_scale(boxes, hardware_preproc_info: kp.HwPreProcInfo)`),但 `kp` 在模組頂層未被 import。實際 kp import 是在函數內部的 `run_single_inference()` 中延遲進行的。
**影響**:型別標注在模組載入時會被解析(在 Python 3.10+ 以下),可能導致 `NameError`
**建議**:在頂層加入 `if TYPE_CHECKING: import kp`,或改用字串型別標注 `"kp.HwPreProcInfo"`
### 9.7 VideoThread 的 `threading.Thread` 記憶體洩漏風險(低優先)
**問題**`_open_camera_with_timeout()` 啟動了 `daemon=True``threading.Thread` 並等待最多 5 秒,但如果 thread 仍存活timeout其仍會繼續嘗試開啟相機可能導致相機資源被不正確佔用。
**建議**:使用 cv2 的 nonblocking 方式或設定相機 timeout 參數,避免 daemon thread 的不確定行為。
### 9.8 MSE 計算的效能問題(低優先)
**問題**`InferenceWorkerThread``CustomInferenceWorkerThread` 的 MSE 計算會把整個 frame 轉成 float32 進行運算:
```python
mse = np.mean((frame.astype(np.float32) - self.last_frame.astype(np.float32)) ** 2)
```
對於 640x480 的 3 通道影像,每次計算需要處理 ~921,600 個浮點數。
**建議**:可改為縮小解析度後再計算 MSE或使用 histogram 比較等更快速的方式。
---
## 10. 容量與效能估算
### 10.1 系統需求(桌面應用)
| 資源 | 需求 | 備註 |
|------|------|------|
| CPU | 雙核心以上 | 主要用於影像轉換和後處理 |
| RAM | 2GB 以上 | kp SDK + OpenCV + PyQt5 |
| USB | USB 3.0 | KL720 需要 USB 3.0 |
| GPU | 不需要 | 推論在 NPU 執行 |
| 磁碟 | 500MB 以上 | 安裝包 + 模型檔 |
### 10.2 推論速度特性
- **Queue maxsize**5 幀
- **VideoThread 輸出**~30fps640x480
- **InferenceWorkerThread min_interval**2 秒(標準模式)/ 0.5 秒Custom 模式)
- **MSE threshold**500低於此值視為相似幀使用快取結果
- **相機開啟 timeout**5 秒 × 最多 3 次嘗試
---
## 11. 安全性設計
### 11.1 目前狀態
| 項目 | 狀態 | 說明 |
|------|------|------|
| Server 登入驗證 | ❌ 未實作 | `attempt_login()` 永遠成功 |
| 程式碼保護 | ⚠️ 計畫中 | PyArmor 列在計畫中 |
| 自定義模型驗證 | ❌ 無 | 任何 .nef 檔都能上傳 |
| 網路通訊加密 | ❌ 未知 | Server 驗證端點未見 TLS 設定 |
### 11.2 Plugin 安全風險
`load_inference_module()` 使用 `importlib` 動態執行 `script.py`,等同於執行任意 Python 程式碼。若 `%LOCALAPPDATA%` 中的 `script.py` 被惡意替換,攻擊者可以完整控制推論行為。
**建議**:考慮對 `script.py` 進行簽章驗證,或限制其沙盒執行環境。

View File

@ -1,39 +0,0 @@
# 專案進度 — KNEO Academy
## 目的:既有專案接入 → 補齊文件
## 當前階段:文件補齊完成,待使用者確認下一步
## 當前狀態:待審核
## 最後更新2026-04-05
## 進度表
| 階段 | 狀態 | 完成時間 | 備註 |
|------|------|----------|------|
| 既有專案接入 | ✅ 已完成 | 2026-04-04 | 本地路徑 |
| 專案健檢 | ✅ 已完成 | 2026-04-04 | 見 00-onboarding/health-check.md |
| 確認目的與策略 | ✅ 已完成 | 2026-04-04 | 客戶產品,補齊 PRD + 架構文件 + 設計規格 |
| PRDPM Agent 反推) | ✅ 已完成 | 2026-04-04 | 見 02-prd/PRD.md 項目保留待確認 |
| 架構文件 + TDDArchitect Agent | ✅ 已完成 | 2026-04-04 | 見 04-architecture/design-doc.md + TDD.md |
| 設計規格Design Agent | ✅ 已完成 | 2026-04-05 | 見 03-design/design-spec.md |
| 使用者確認各文件 | ✅ 已完成 | 2026-04-05 | PRD + 架構文件已確認;設計規格待確認 |
## 當前待辦
- [ ] 使用者審核設計規格03-design/design-spec.md
- [ ] 決定下一步(新增功能 / 修 bug / 其他)
## 未解決問題
- PRD 中 ⚠️ 待確認項目(保留待日後確認)
- 技術問題Architect Agent 標注):
- 🔴 CustomInferenceWorkerThread 雙重裝置連接風險
- 🔴 LoginScreen 驗證未實作
- 🟡 UtilitiesScreen DeviceController 孤立
- 設計問題Design Agent 標注):
- 兩套設計語言(深色 vs 淺色)共存,視覺不統一
- back_arrow.png 缺失,返回按鈕空白
- 暫停按鈕使用語意錯誤的刪除圖示
## 重要決策紀錄
- 程式碼來源:本地路徑 C:\Users\sungs\Documents\abin\KNEO-Academy
- 專案類型客戶產品Python + PyQt5 桌面 AI 推論應用)
- 設計稿:無 Figma僅 uxui/ 資源
- 測試計畫:暫不需要
- PRD 待確認項目:保留現狀,不強迫確認

View File

@ -66,10 +66,6 @@ class AppController:
self.main_window = MainWindow()
self.stack.addWidget(self.main_window)
# Share MainWindow's DeviceController with UtilitiesScreen so both
# screens reflect the same device connection state.
self.utilities_screen.set_device_controller(self.main_window.device_controller)
def connect_signals(self):
"""

View File

@ -11,6 +11,7 @@ import cv2
import json
from PyQt5.QtWidgets import QMessageBox, QApplication
from PyQt5.QtCore import QTimer, Qt
import kp
from src.models.inference_worker import InferenceWorkerThread
from src.models.custom_inference_worker import CustomInferenceWorkerThread
@ -186,7 +187,6 @@ class InferenceController:
# Upload model to device
if device_group:
try:
import kp
print('[Uploading model]')
self.model_descriptor = kp.core.load_model_from_file(
device_group=device_group,
@ -364,9 +364,6 @@ class InferenceController:
input_params["custom_ncpu_path"] = tool_config.get("custom_ncpu_path")
input_params["custom_labels"] = tool_config.get("custom_labels")
# Pass existing device_group to avoid double connection in the worker
input_params["device_group"] = self.device_controller.get_device_group()
# Get device-related settings
selected_device = self.device_controller.get_selected_device()
if selected_device:

View File

@ -66,7 +66,6 @@ class MediaController:
print("Camera signal connected successfully")
except Exception as e:
print(f"Error connecting camera signal: {e}")
self.video_thread.camera_error_signal.connect(self.handle_camera_error)
# Start camera thread
self.video_thread.start()
@ -164,20 +163,6 @@ class MediaController:
import traceback
print(traceback.format_exc())
def handle_camera_error(self, error_msg):
"""
Handle camera error signal from VideoThread.
Args:
error_msg (str): Error message describing what went wrong.
"""
print(f"Camera error: {error_msg}")
if hasattr(self.main_window, 'canvas_label'):
self.main_window.canvas_label.setText(error_msg)
self.main_window.canvas_label.setAlignment(Qt.AlignCenter)
self.video_thread = None
self._signal_was_connected = False
def reconnect_camera_signal(self):
"""Reconnect the camera signal if it was previously disconnected"""
if self.video_thread is not None and self._signal_was_connected:

View File

@ -4,17 +4,16 @@ custom_inference_worker.py - Custom Inference Worker
This module provides a worker thread for running inference using user-uploaded
custom models. It uses YOLO V5 pre/post-processing logic for object detection.
"""
from __future__ import annotations
import os
import time
import queue
import cv2
import numpy as np
from typing import List, TYPE_CHECKING
from typing import List
from PyQt5.QtCore import QThread, pyqtSignal
if TYPE_CHECKING:
import kp
import kp
from kp.KPBaseClass.ValueBase import ValueRepresentBase
# COCO dataset class names (80 classes)
@ -32,7 +31,7 @@ COCO_CLASSES = [
]
class ExampleBoundingBox:
class ExampleBoundingBox(ValueRepresentBase):
"""Bounding box descriptor."""
def __init__(self,
@ -60,7 +59,7 @@ class ExampleBoundingBox:
}
class ExampleYoloResult:
class ExampleYoloResult(ValueRepresentBase):
"""YOLO output result descriptor."""
def __init__(self,
@ -355,10 +354,6 @@ class CustomInferenceWorkerThread(QThread):
"""
Initialize device, upload firmware and model.
If a device_group is already provided in input_params (connected by
DeviceController), reuse it and skip connect_devices to avoid double
connection conflicts with the Kneron SDK.
Returns:
bool: True if initialization successful, False otherwise.
"""
@ -379,19 +374,11 @@ class CustomInferenceWorkerThread(QThread):
print("Missing required file paths")
return False
import kp
# Reuse existing device_group if provided to avoid double connection
existing_device_group = self.input_params.get("device_group")
if existing_device_group is not None:
print('[Reusing existing device connection]')
self.device_group = existing_device_group
else:
print('[Connecting device]')
self.device_group = kp.core.connect_devices(usb_port_ids=[port_id])
print(' - Connection successful')
# Connect to device
print('[Connecting device]')
self.device_group = kp.core.connect_devices(usb_port_ids=[port_id])
kp.core.set_timeout(device_group=self.device_group, milliseconds=5000)
print(' - Connection successful')
# Upload firmware
print('[Uploading firmware]')
@ -434,7 +421,6 @@ class CustomInferenceWorkerThread(QThread):
img_processed, original_width, original_height = preprocess_frame(frame)
# 建立推論描述符
import kp
descriptor = kp.GenericImageInferenceDescriptor(
model_id=self.model_descriptor.models[0].id,
inference_number=0,
@ -546,20 +532,11 @@ class CustomInferenceWorkerThread(QThread):
self.quit()
def cleanup(self):
"""Clean up resources.
Only disconnects device if this worker created the connection itself
(i.e. no device_group was provided via input_params).
"""
"""Clean up resources and disconnect device."""
try:
if self.device_group is not None:
owned_by_worker = self.input_params.get("device_group") is None
if owned_by_worker:
import kp
kp.core.disconnect_devices(self.device_group)
print('[Device disconnected]')
else:
print('[Device connection owned by DeviceController, skipping disconnect]')
kp.core.disconnect_devices(self.device_group)
print('[Device disconnected]')
self.device_group = None
except Exception as e:
print(f"Error cleaning up resources: {e}")

View File

@ -125,10 +125,9 @@ class VideoThread(QThread):
height, width, channel = frame.shape
bytes_per_line = channel * width
qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888)
self.change_pixmap_signal.emit(qt_image.copy())
self.change_pixmap_signal.emit(qt_image)
else:
print("Unable to read camera frame, camera may be disconnected")
self.camera_error_signal.emit("相機連線中斷,嘗試重新連線...")
break
# Release camera resources
@ -142,7 +141,6 @@ class VideoThread(QThread):
if self._camera_open_attempts >= self._max_attempts:
print("Maximum attempts reached, unable to open camera")
self.camera_error_signal.emit("無法開啟相機,請確認相機是否連接")
def stop(self):
"""Stop the video capture thread."""

View File

@ -25,7 +25,7 @@ def create_media_panel(parent, media_controller, file_service):
media_controller.take_screenshot),
('upload file', os.path.join(assets_path, "Assets_svg/bt_function_upload_normal.svg").replace('\\', '/'),
file_service.upload_file),
('pause/resume', os.path.join(assets_path, "Assets_svg/bt_function_video_normal.svg").replace('\\', '/'),
('pause/resume', os.path.join(assets_path, "Assets_svg/btn_result_image_delete_hover.svg").replace('\\', '/'),
lambda: toggle_pause_button(parent, media_controller)),
('voice', os.path.join(assets_path, "Assets_svg/ic_recording_voice.svg").replace('\\', '/'),
lambda: media_controller.record_audio(None)),

View File

@ -14,6 +14,7 @@ from PyQt5.QtWidgets import (
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
from PyQt5.QtGui import QPixmap, QFont, QIcon, QColor
import os
import kp
from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, DongleModelMap
from src.controllers.device_controller import DeviceController
from src.services.device_service import check_available_device
@ -54,18 +55,6 @@ class UtilitiesScreen(QWidget):
self.device_controller = DeviceController(self)
self.current_page = "utilities" # Track current page: "utilities" or "purchased_items"
self.init_ui()
def set_device_controller(self, device_controller):
"""
Replace the local DeviceController with a shared instance.
Call this from AppController after all screens are created so that
UtilitiesScreen and MainWindow share the same device connection state.
Args:
device_controller: Shared DeviceController instance from MainWindow.
"""
self.device_controller = device_controller
def init_ui(self):
"""
@ -146,14 +135,14 @@ class UtilitiesScreen(QWidget):
header_layout.setContentsMargins(20, 0, 20, 0)
# Back button
back_button = QPushButton("", self)
back_button = QPushButton("", self)
back_button.setIcon(QIcon(os.path.join(UXUI_ASSETS, "Assets_png/back_arrow.png")))
back_button.setIconSize(QPixmap(os.path.join(UXUI_ASSETS, "Assets_png/back_arrow.png")).size())
back_button.setFixedSize(40, 40)
back_button.setStyleSheet("""
QPushButton {
background-color: transparent;
border: none;
color: white;
font-size: 20px;
}
QPushButton:hover {
background-color: rgba(255, 255, 255, 0.1);
@ -862,7 +851,6 @@ class UtilitiesScreen(QWidget):
firmware_version = "-"
try:
if device.is_connectable:
import kp
# Connect to device and get system info
device_group = kp.core.connect_devices(usb_port_ids=[port_id])
system_info = kp.core.get_system_info(
@ -992,7 +980,6 @@ class UtilitiesScreen(QWidget):
self.show_progress(f"Updating firmware for {device_model}...", 0)
# Connect to device
import kp
device_group = kp.core.connect_devices(usb_port_ids=[int(port_id)])
# Build firmware file paths
@ -1039,7 +1026,6 @@ class UtilitiesScreen(QWidget):
self.show_progress("Installing Kneron Device Drivers...", 0)
# List all product IDs
import kp
product_ids = [
kp.ProductId.KP_DEVICE_KL520,
kp.ProductId.KP_DEVICE_KL720_LEGACY,