L 級新功能、PRD/Design/TDD/ADR 三方協作 + 互審 + M9-6 SDK 雙驗證、總計 ~9000 行文件。
範圍:
- A 階段(MVP、5 人天):KL520 + KL720 自動升級 KDP1 → KDP2
- B 階段(10.5 人天):手動降版面向一般使用者 + KL630 / KL730 擴展
- 合計 15.5 人天、安裝包 +7MB(保守 bundle 策略)
關鍵決策:
- 翻案 R5-Q9(progress.md 第二輪使用者決策「韌體燒錄 flash → B 砍掉」)
- 跨平台用 KneronPLUS Python C API、不用 DFUT.exe
- 多版本目錄結構選 C metadata(firmware/<chip>/{version}/ + CURRENT_VERSION)
- Kneron firmware redistribution 授權與 R5-B4 預置模型同性質、發佈前評估
文件產出:
- PRD v2.2(PRD-v2.md 495 行 + features/feature-firmware-management.md 599 行)
- Design v2.2(firmware-management.md 948 行 + control-panel.md §6a graceful shutdown)
- TDD v2.2(v2/firmware-management.md 823 行 + ADR-001 218 行)
- 8 份 research(含 M9-6 弱驗證 + 強驗證、~3200 行)
- 3 份三方互審報告(PM/Design/Architect cross-review)
M9-6 強驗證重大發現(影響 B 階段):
- KL730 product_id 實際是 0x732(不是 0x0730)
- KL630/KL730 firmware 是 embedded Linux rootfs(不是 .bin、不同代設計)
- KneronPLUS Python 沒 update_kdp_firmware_from_files 公開 API、warrenchen 走 ctypes
- 不影響 A 階段、B 階段 M9-8 需 spike
下一步:派 backend M9-1 起跑(bridge.py handle_firmware_upgrade)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
552 lines
24 KiB
Markdown
552 lines
24 KiB
Markdown
# 手動降版面向一般使用者:流程設計與 Design 需求清單
|
||
|
||
> 對應 research index §42
|
||
> 範圍:B2 階段——把 FW 降版功能暴露給一般使用者(不只 dev mode / 內部測試)、所需 driver / bridge / API endpoint 細節 + 給 Design Agent 的 UX 補強需求
|
||
> 撰寫日期:2026-05-24
|
||
> 限制:架構面、不出 code、UX 細節留給 Design Agent
|
||
> 路徑使用相對路徑(相對於 `/Users/jimchen/visionA/local-tool/`)
|
||
|
||
---
|
||
|
||
## 0. TL;DR
|
||
|
||
1. **使用者決策(2026-05-24)翻案前一份研究 §3 的「降版僅內部測試」假設**——降版面向一般使用者
|
||
2. 一般使用者面向後、責任分工:
|
||
- **Architect(我,本檔)**:標記技術需求、定義 safety guards、API 簽名、儲存結構、driver 與 bridge.py 介面
|
||
- **Design Agent(另派)**:警告語、二次確認流程、版本 dropdown 視覺、降版進行中互動、失敗復原 UX
|
||
- **Frontend Agent**:實作 UI、串 API、i18n
|
||
- **Backend Agent**:實作 driver method、bridge.py handler、API endpoint
|
||
3. 「面向一般使用者」≠ 「無腦讓使用者隨便降」——必須有**多層 safety net**:
|
||
- UI 警告 + 二次確認(design 領域、本檔只標需求)
|
||
- Driver 層 guard(拒絕跨晶片誤匹配、拒絕「降版」到比 current 還新的版本)
|
||
- bridge.py 層 guard(checksum 驗證 firmware 完整性、version 字串嚴格 match)
|
||
4. 暴露範圍:Settings → 「韌體管理」面板(不在 Devices 頁主流程、避免誤觸)
|
||
|
||
---
|
||
|
||
## 1. 使用情境分析
|
||
|
||
### 1.1 一般使用者為什麼會降版
|
||
|
||
| 情境 | 頻率 | 描述 | 處理建議 |
|
||
|------|------|------|---------|
|
||
| 「我的舊 model 在新 FW 上跑不出結果」 | 中 | 內建升版到 v2.2 後、舊 NEF model(v2.1 編的)相容性問題 | 提供降版回 v2.1 選項 |
|
||
| 「跟某個 third-party tool 不相容」 | 低 | 例如某些 model debugging tool 只認 KDP1 | 提供降版回 KDP1 選項(KL520) |
|
||
| 「我升錯了、想回到原本狀態」 | 低 | 使用者誤觸升級、想 revert | 提供「降版回上次的版本」選項 |
|
||
| 「測試環境需要特定版本」 | 中 | 開發者場景(但你說也要 face 一般使用者)| 提供降版到任意 bundled 版本 |
|
||
| 「就是想試試看」 | 中 | 好奇心驅動 | 用警告語勸阻、但不阻止 |
|
||
|
||
→ 「跟一般使用者解釋降版」必須包裝成「**FW 版本切換**」、不要用「**降版**」這個帶負面含義詞——但內部技術文件仍叫 downgrade(程式碼/log/Architect 文件用語)。
|
||
|
||
### 1.2 UI 觸發位置(建議給 Design Agent 評估)
|
||
|
||
| 位置 | 優點 | 缺點 | 建議 |
|
||
|------|------|------|------|
|
||
| Devices 頁主卡片 | 醒目、與 FW badge 同位置 | 太容易誤觸、新手使用者不需要 | **不建議** |
|
||
| Devices 頁 → Device Detail Modal | 進階入口、需要點開 | 仍偏 device-centric | **次佳** |
|
||
| Settings → 韌體管理 | 進階入口、與其他 settings 同位置 | 較深入路徑 | **推薦** |
|
||
| Settings → 進階(advanced) | 最深入口 | 但使用者面向後不應藏太深 | 折衷 |
|
||
|
||
**架構建議**:**Settings → 韌體管理** 是主入口、Devices 頁 device card 顯示 FW badge 但不放降版按鈕。
|
||
|
||
---
|
||
|
||
## 2. Driver / Bridge / API 詳細設計
|
||
|
||
### 2.1 driver interface 擴展
|
||
|
||
承前一份 30-integration-plan §1 M9-2 已定義的 `UpgradeFirmware()`、本檔加:
|
||
|
||
```go
|
||
// 偽碼、不出 code(給 architect 補 TDD 用)
|
||
type DeviceDriver interface {
|
||
// ... 既有 methods 與 UpgradeFirmware() ...
|
||
|
||
// B2 階段新增
|
||
DowngradeFirmware(version string, progressCh chan<- FirmwareProgress) error
|
||
ListFirmwareVersions() ([]FirmwareVersion, error)
|
||
GetCurrentFirmwareVersion() (FirmwareVersion, error)
|
||
}
|
||
|
||
type FirmwareVersion struct {
|
||
Version string `json:"version"` // "v2.2.0" / "v2.1.0" / "kdp1"
|
||
DisplayName string `json:"displayName"` // "v2.2.0 (current)" / "v2.1.0 (older)"
|
||
IsCurrent bool `json:"isCurrent"`
|
||
IsBundled bool `json:"isBundled"` // 是否在安裝包內、總是 true(不做線上更新)
|
||
ReleaseDate string `json:"releaseDate,omitempty"` // ISO 8601、optional
|
||
Notes string `json:"notes,omitempty"` // 給使用者看的說明
|
||
}
|
||
|
||
type FirmwareProgress struct {
|
||
// ... 既有欄位 ...
|
||
Direction string `json:"direction"` // "upgrade" / "downgrade"
|
||
}
|
||
```
|
||
|
||
### 2.2 bridge.py handler 設計
|
||
|
||
#### 2.2.1 `handle_firmware_list_versions`
|
||
|
||
```python
|
||
# 偽碼
|
||
def handle_firmware_list_versions(params):
|
||
"""List bundled firmware versions for a chip.
|
||
|
||
Params:
|
||
chip: str ("KL520" | "KL720" | "KL630" | "KL730", required)
|
||
|
||
Returns:
|
||
{"versions": [{"version":"v2.2.0", "isCurrent":true, "isBundled":true, ...}, ...]}
|
||
or {"error": "..."}
|
||
"""
|
||
chip = params.get("chip", "").upper()
|
||
if chip not in ("KL520", "KL720", "KL630", "KL730"):
|
||
return {"error": f"unknown chip: {chip}"}
|
||
|
||
base = os.path.dirname(os.path.abspath(__file__))
|
||
fw_dir = os.path.join(base, "firmware", chip)
|
||
if not os.path.isdir(fw_dir):
|
||
return {"error": f"firmware dir not found for {chip}"}
|
||
|
||
# 讀 current/VERSION
|
||
current_version = _read_version_file(os.path.join(fw_dir, "current", "VERSION"))
|
||
|
||
versions = []
|
||
for entry in os.listdir(fw_dir):
|
||
entry_path = os.path.join(fw_dir, entry)
|
||
if not os.path.isdir(entry_path):
|
||
continue
|
||
if entry == "current":
|
||
continue # current 是 alias、不重複列
|
||
version_file = os.path.join(entry_path, "VERSION")
|
||
if not os.path.exists(version_file):
|
||
continue
|
||
v = _read_version_file(version_file)
|
||
versions.append({
|
||
"version": v,
|
||
"displayName": _format_display_name(v, current_version),
|
||
"isCurrent": v == current_version,
|
||
"isBundled": True,
|
||
"directory": entry, # 給 bridge.py 內部用、不外露給 API
|
||
})
|
||
|
||
return {"versions": versions, "current": current_version}
|
||
```
|
||
|
||
#### 2.2.2 `handle_firmware_downgrade`
|
||
|
||
```python
|
||
# 偽碼
|
||
def handle_firmware_downgrade(params):
|
||
"""Downgrade firmware to a specific bundled version.
|
||
|
||
Params:
|
||
port: int (USB port id, required)
|
||
chip: str ("KL520" | "KL720" | "KL630" | "KL730", required)
|
||
version: str (target version, e.g. "v2.1.0" / "kdp1", required)
|
||
|
||
Returns:
|
||
{"status":"downgraded", "before_version":"v2.2.0", "after_version":"v2.1.0",
|
||
"duration_ms":31000, "stage":"done"}
|
||
or {"error":"...", "stage":"validate|connect|download|verify"}
|
||
"""
|
||
chip = params.get("chip", "")
|
||
target_version = params.get("version", "")
|
||
target_port = params.get("port", "")
|
||
|
||
# Stage 1: validate
|
||
# - chip 必須是支援的
|
||
# - version 必須在 bundled list 內
|
||
# - target_version 不能等於 current(那是 no-op)
|
||
# - target_version 不能比 current 還新(那是 upgrade、走另一支)
|
||
if not _validate_downgrade_request(chip, target_version):
|
||
return {"error": "invalid downgrade request", "stage": "validate"}
|
||
|
||
# Stage 2: 解析 firmware paths
|
||
fw_paths = _resolve_firmware_paths_versioned(chip, target_version)
|
||
if fw_paths is None:
|
||
return {"error": f"firmware not found: {chip}/{target_version}", "stage": "validate"}
|
||
|
||
# Stage 3: connect
|
||
# 跟 firmware_upgrade 一樣的 connect_with_magic 流程
|
||
# ...
|
||
|
||
# Stage 4: 跑 SDK API
|
||
# 視 chip:
|
||
# - KL520 KDP2 → KDP1: kp.core.update_kdp_firmware_from_files(loader, ..., auto_reboot=True) + load_firmware
|
||
# - KL520 v2.2 → v2.1: 同上但用不同 .bin
|
||
# - KL720: 同上(KL720 是 flash-based、要寫 flash)
|
||
# - KL630/KL730: 用 update_*_from_tar API(M9-6 驗證後填)
|
||
# ...
|
||
|
||
# Stage 5: verify
|
||
# disconnect → sleep 3s → rescan → 看 firmware 字串 / kn_number 是否符合預期
|
||
# ...
|
||
|
||
return {
|
||
"status": "downgraded",
|
||
"before_version": before_version,
|
||
"after_version": after_version,
|
||
"duration_ms": duration,
|
||
"stage": "done",
|
||
}
|
||
```
|
||
|
||
### 2.3 API endpoint 設計
|
||
|
||
| Endpoint | Method | Request Body | Response |
|
||
|----------|--------|-------------|----------|
|
||
| `GET /api/devices/:id/firmware/versions` | GET | — | `{success:true, data:{versions:[...], current:"v2.2.0"}}` |
|
||
| `POST /api/devices/:id/firmware/downgrade` | POST | `{version:"v2.1.0", confirmToken:"DOWNGRADE"}` | `202 {success:true, data:{taskId:"..."}}` |
|
||
| WebSocket room `firmware:<deviceId>` | — | — | progress events(同 upgrade 流) |
|
||
|
||
**`confirmToken` 設計**:
|
||
- API 層要求 body 含 `confirmToken: "DOWNGRADE"`(字面字串)
|
||
- 沒帶 / 帶錯 → API 直接 400
|
||
- **目的**:防止 CSRF、防止前端 bug 誤觸發、強制 UI 二次確認流程
|
||
- Frontend 必須先讓使用者輸入字面字串 / 點兩次按鈕、才把 token 加進 request
|
||
|
||
### 2.4 driver 層 safety guards
|
||
|
||
`KneronDriver.DowngradeFirmware(version)` 內必做:
|
||
|
||
1. **不能跨晶片**:呼叫前驗 `version` 在 `ListFirmwareVersions()` 結果內、不接受任意字串
|
||
2. **不能升版偽裝**:比較 `version` 與 `GetCurrentFirmwareVersion()` 結果、若目標版本 >= current → 拒絕(要走升版 API)
|
||
3. **不能 no-op**:若 `version == current` → 拒絕(節省時間 + 避免 device 不必要 reset)
|
||
4. **status guard**:device 必須是 `StatusDetected` 或 `StatusConnected`、不能在 `StatusInferencing` / `StatusFlashing` / `StatusUpgrading` 期間降版(會卡 mutex)
|
||
|
||
```go
|
||
// 偽碼
|
||
func (d *KneronDriver) DowngradeFirmware(version string, progressCh chan<- driver.FirmwareProgress) error {
|
||
d.mu.Lock()
|
||
if d.info.Status == driver.StatusInferencing || d.info.Status == driver.StatusFlashing {
|
||
d.mu.Unlock()
|
||
return fmt.Errorf("device busy: %s", d.info.Status)
|
||
}
|
||
chip := d.chipType
|
||
current := d.info.FirmwareVer
|
||
d.mu.Unlock()
|
||
|
||
// Validate version exists
|
||
versions, err := d.ListFirmwareVersions()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
var targetVer *driver.FirmwareVersion
|
||
for _, v := range versions {
|
||
if v.Version == version {
|
||
targetVer = &v
|
||
break
|
||
}
|
||
}
|
||
if targetVer == nil {
|
||
return fmt.Errorf("version %s not found in bundled firmware for %s", version, chip)
|
||
}
|
||
|
||
// Validate it's actually a downgrade(順序比較邏輯 chip-specific)
|
||
if !isOlderVersion(version, current, chip) {
|
||
return fmt.Errorf("target version %s is not older than current %s", version, current)
|
||
}
|
||
|
||
// 進實際降版流程
|
||
// ... 跟 UpgradeFirmware 類似、call bridge.py firmware_downgrade ...
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 多版本 firmware 儲存結構(B2 細化)
|
||
|
||
### 3.1 完整目錄結構
|
||
|
||
```
|
||
server/scripts/firmware/
|
||
├── KL520/
|
||
│ ├── current/ ← 預設 firmware(A 階段位置 = MVP 既有)
|
||
│ │ ├── fw_scpu.bin ← KDP2 v2.2.0
|
||
│ │ ├── fw_ncpu.bin
|
||
│ │ ├── fw_loader.bin ← KDP1→KDP2 升級用(A 階段補進來)
|
||
│ │ └── VERSION ← "v2.2.0"
|
||
│ ├── v2.2.0/ ← 跟 current 同內容、用於版本切換 reference
|
||
│ │ ├── fw_scpu.bin
|
||
│ │ ├── fw_ncpu.bin
|
||
│ │ ├── fw_loader.bin
|
||
│ │ └── VERSION
|
||
│ ├── v2.1.0/ ← 舊版本(如果決定 bundle)
|
||
│ │ ├── fw_scpu.bin
|
||
│ │ ├── fw_ncpu.bin
|
||
│ │ ├── fw_loader.bin
|
||
│ │ └── VERSION
|
||
│ └── kdp1/ ← KDP1 降版
|
||
│ ├── fw_scpu.bin
|
||
│ ├── fw_ncpu.bin
|
||
│ └── VERSION ← "KDP1"(特殊標記、不是 semver)
|
||
├── KL720/
|
||
│ ├── current/ ← v2.2.0
|
||
│ ├── v2.2.0/
|
||
│ └── v2.1.0/ ← 視是否 bundle
|
||
├── KL630/
|
||
│ ├── current/ ← SDK-v2.5.7
|
||
│ │ ├── kp_firmware.tar
|
||
│ │ ├── kp_loader.tar
|
||
│ │ ├── extracted/ ← build time 解壓(如選策略 Y)
|
||
│ │ │ ├── fw_scpu.bin
|
||
│ │ │ └── fw_ncpu.bin
|
||
│ │ └── VERSION ← "SDK-v2.5.7"
|
||
│ └── SDK-v2.4.0/ ← 視是否 bundle 舊 SDK
|
||
│ └── ...
|
||
└── KL730/
|
||
├── current/ ← SDK-v1.3.0
|
||
│ ├── kp_firmware.tar
|
||
│ ├── kp_loader.tar
|
||
│ ├── extracted/
|
||
│ └── VERSION
|
||
└── ...
|
||
```
|
||
|
||
### 3.2 `current/` 是什麼
|
||
|
||
**選項 A:symbolic link** → `current/` 是 symlink 指向 `v2.2.0/`
|
||
- 優點:節省空間(不重複 binary)
|
||
- 缺點:Windows symbolic link 需要 admin 權限、`tar`/zip 壓縮對 symlink 處理各 OS 不同
|
||
- **不推薦**(跨平台 symlink 太脆弱)
|
||
|
||
**選項 B:實體副本** → `current/` 是 `v2.2.0/` 的 file copy
|
||
- 優點:跨平台、簡單
|
||
- 缺點:每個 chip 多佔 1 份(KL520 ~100KB、KL720 ~250KB、KL630/KL730 ~3-4MB)、合計 ~7-8MB 額外
|
||
- **推薦**
|
||
|
||
**選項 C:取消 `current/`、用 metadata 記錄當前版本**
|
||
- 結構簡化:`firmware/<chip>/{v2.2.0,v2.1.0,kdp1}/` + `firmware/<chip>/CURRENT_VERSION`(單行檔 = "v2.2.0")
|
||
- 優點:節省空間、structure 清晰
|
||
- 缺點:bridge.py 多一次 file read 才知道用哪個版本(trivial 成本)
|
||
- **可選**(架構乾淨度 vs 簡單度的 trade-off)
|
||
|
||
**建議選 C**——架構最乾淨、空間最省、跨平台無 symlink 風險。
|
||
|
||
### 3.3 採選項 C 的具體結構
|
||
|
||
```
|
||
server/scripts/firmware/
|
||
├── KL520/
|
||
│ ├── CURRENT_VERSION ← 單行檔:"v2.2.0"
|
||
│ ├── v2.2.0/
|
||
│ │ ├── fw_scpu.bin
|
||
│ │ ├── fw_ncpu.bin
|
||
│ │ ├── fw_loader.bin
|
||
│ │ └── VERSION ← "v2.2.0"
|
||
│ ├── v2.1.0/
|
||
│ │ └── ...
|
||
│ └── kdp1/
|
||
│ └── ...
|
||
├── KL720/
|
||
│ └── ...同結構...
|
||
├── KL630/
|
||
│ └── ...同結構(.tar / extracted)...
|
||
└── KL730/
|
||
└── ...
|
||
```
|
||
|
||
`_resolve_firmware_paths_versioned(chip, version=None)`:
|
||
- `version=None` → 讀 `CURRENT_VERSION` 拿當前版本 → 找 `<chip>/<version>/`
|
||
- `version="v2.1.0"` → 直接找 `<chip>/v2.1.0/`
|
||
|
||
**A 階段(MVP)相容性**:A 階段 `_resolve_firmware_paths(chip)` 找 `<chip>/fw_scpu.bin`、B2 階段重整目錄時、要把 A 階段檔案搬進 `<chip>/v2.2.0/` 並加 `CURRENT_VERSION`。**A 階段的 caller 必須升級成 `_resolve_firmware_paths_versioned`**(B2 migration step)。
|
||
|
||
---
|
||
|
||
## 4. Bundle 多版本對安裝包大小的精確估算
|
||
|
||
### 4.1 預估 bundle 策略
|
||
|
||
| Chip | current | 額外 bundled | 合計(每 chip)|
|
||
|------|---------|------------|--------------|
|
||
| KL520 | v2.2.0 (~100KB) | v2.1.0 (~100KB) + kdp1 (~90KB) | ~290KB |
|
||
| KL720 | v2.2.0 (~250KB) | v2.1.0 (~250KB)(如果 bundle) | ~500KB |
|
||
| KL630 | SDK-v2.5.7 (~3MB) | SDK-v2.4.0 (~3MB)(如果 bundle)+ extracted (~3MB) | ~9MB |
|
||
| KL730 | SDK-v1.3.0 (~4MB) | + extracted (~4MB) | ~8MB |
|
||
|
||
**合計 B2 階段所有 bundled firmware**:~18MB(多版本全部 bundle)
|
||
|
||
### 4.2 與既有 dmg 163MB 比
|
||
|
||
| 階段 | 累計 | 衝擊 |
|
||
|------|------|------|
|
||
| 既有 | 163MB | baseline |
|
||
| A 階段(MVP)| 163MB + 10KB | <0.01% |
|
||
| B 階段(單版本 KL630/KL730 + 降版用 KL520_kdp)| 163MB + 6-8MB ≈ 170MB | +4-5% |
|
||
| B2 階段(每 chip 多 bundle 1 舊版) | 163MB + 14-18MB ≈ 178-181MB | +9-11% |
|
||
|
||
→ **使用者「+5MB 接受」對 B 階段成立、但 B2 多版本超出**。
|
||
|
||
### 4.3 取捨建議
|
||
|
||
| 策略 | 大小 | 取捨 |
|
||
|------|------|------|
|
||
| **保守**:每 chip 只 bundle current + 1 個降版選項(KL520 v2.1.0、其他不 bundle 舊版) | +7MB | 滿足使用者「+5MB 接受」邊緣、降版選擇變少 |
|
||
| **完整**:每 chip current + 2 個舊版 | +18MB | 超出 +5MB 估算、但選擇豐富 |
|
||
| **極簡**:只 bundle current + KDP1 降版(KL520 only) | +5MB | 嚴格 +5MB、其他 chip 沒降版選擇 |
|
||
|
||
**建議選保守策略**——KL520 提供「kdp1」「v2.1.0」兩個降版、KL720/KL630/KL730 提供 current + 1 個舊版(如有可用)。
|
||
|
||
**重要前提**:因 SDK release 不一定提供舊版 firmware、KL630/KL730 「舊版」可能取不到(M9-6 待驗證)。
|
||
|
||
---
|
||
|
||
## 5. Design Agent 需求清單(給 Orchestrator 派 Design 用)
|
||
|
||
> **注意**:以下是 architect 標記的「**功能性需求**」、Design Agent 負責設計具體 wireframe / 視覺 / 文案。Architect 不畫設計稿、不寫文案。
|
||
|
||
### 5.1 Settings → 韌體管理面板(新頁面)
|
||
|
||
**功能需求**:
|
||
- F1. 列出所有偵測到的 dongle、每張 dongle 一張卡片
|
||
- F2. 卡片顯示:dongle 名稱("KL520 #1")、kn_number、當前 FW 版本、bundled current 版本(讓使用者看出是否最新)
|
||
- F3. 卡片內 actions:
|
||
- 「升級到最新」按鈕(如果 current 與 bundled 不同)
|
||
- 「切換 FW 版本」按鈕(永遠顯示、暴露多版本選擇)
|
||
- F4. 「切換 FW 版本」展開後:版本 dropdown + 詳細說明(每個版本一段話)+ 警告語 + 「降版」按鈕
|
||
- F5. 二次確認 modal(見 §5.3)
|
||
- F6. 降版進行中 progress UI(progress bar + 階段提示 + 不可中斷警告)
|
||
- F7. 降版完成後 toast 通知 + 自動 rescan + 卡片更新到新版本
|
||
|
||
### 5.2 Devices 頁面 FW badge(補強既有 A 階段)
|
||
|
||
**功能需求**:
|
||
- 既有:紅/黃/綠 badge 顯示 FW 健康度
|
||
- 新增:badge 旁加「⚙️」icon、點擊 deep-link 到 Settings → 韌體管理對應卡片
|
||
- 不在 Devices 頁放降版按鈕(避免誤觸)
|
||
|
||
### 5.3 二次確認 modal(最關鍵的 design 細節)
|
||
|
||
**功能需求**(**design 要全部達成**):
|
||
- D1. 警告語必須包含:
|
||
- 「降版可能導致現有 model 無法運作」
|
||
- 「降版過程不可中斷、否則裝置可能損壞」
|
||
- 「降版完成後可能需要重新插拔裝置」
|
||
- 「請確認版本相容性、降版至 KDP1 的舊版會限制可用功能」
|
||
- D2. **使用者必須輸入字面字串「DOWNGRADE」確認**(防誤觸)
|
||
- 不接受其他大小寫
|
||
- 輸入欄為空 / 不對時、確認按鈕 disabled
|
||
- D3. 顯示 before/after 版本比對表(當前 vs 目標)
|
||
- D4. 顯示預估時間(KL520 ~30s、KL720 ~180s、KL630/KL730 估計 ~60s)
|
||
- D5. modal 不可被點外部關閉(必須點「取消」明確關閉)
|
||
- D6. 「確認降版」按鈕視覺要 destructive(紅色 / 警告色)
|
||
- D7. 確認後 modal 不關閉、直接切到「進行中」狀態(避免使用者誤以為什麼都沒發生)
|
||
|
||
### 5.4 「進行中」UI
|
||
|
||
**功能需求**:
|
||
- E1. progress bar(雖然底層是 stage-based、給使用者看百分比更直觀)
|
||
- E2. 階段文字(連線中 / 載入引導程式 / 寫入韌體 / 驗證 / 完成)
|
||
- E3. **不顯示「取消」按鈕**(降版不可中斷、有按鈕會誘惑使用者點)
|
||
- E4. 顯示「請勿拔除裝置」persistent banner(不可關閉)
|
||
- E5. 如果失敗、顯示 friendly error 訊息 + 「重新插拔裝置後重試」指引
|
||
|
||
### 5.5 失敗復原 UX
|
||
|
||
**功能需求**:
|
||
- R1. 區分失敗類型:
|
||
- 「升級時 device disconnect」→ 訊息:「裝置已斷開、請重新插入後重試」
|
||
- 「Firmware 損毀 / 寫入失敗」→ 訊息:「韌體寫入失敗、請聯絡客服」(罕見、可能 brick)
|
||
- 「Timeout」→ 訊息:「升級時間過長、可能成功也可能未完成、請拔插裝置再 scan」
|
||
- R2. 失敗後不自動關閉 modal、給使用者讀完訊息再關
|
||
- R3. 提供「重試」按鈕(如果是可重試的 error)
|
||
- R4. 提供「複製錯誤訊息」按鈕(給技術支援用)
|
||
|
||
### 5.6 i18n keys(給 frontend 估算)
|
||
|
||
預估 新增中英雙語 keys:
|
||
- Settings 韌體管理頁標題、說明:~3 個
|
||
- 卡片內各欄位、按鈕:~8 個
|
||
- 二次確認 modal:~12 個
|
||
- 進行中 UI:~8 個
|
||
- 失敗訊息:~10 個
|
||
- toast / banner:~5 個
|
||
- **合計 ~46 個新 i18n keys**
|
||
|
||
---
|
||
|
||
## 6. 一般使用者誤觸降版 brick 風險評估
|
||
|
||
### 6.1 風險矩陣
|
||
|
||
| 誤操作 | 機率 | 嚴重性 | 已緩解 | 殘餘風險 |
|
||
|--------|------|-------|--------|---------|
|
||
| 點錯按鈕(沒看清楚就降版)| 中 | 中 | 二次確認 + 輸入「DOWNGRADE」字串 | 低 |
|
||
| 降版到不相容版本(KL520 KDP2 → KDP1,且使用者依賴 KDP2-only 功能)| 中 | 中 | 警告語明示 KDP1 限制 | 中(使用者沒讀完警告)|
|
||
| 降版中拔 USB | 低 | 高(可能 brick)| persistent banner + 不可關 modal | 低 |
|
||
| 降版中關 app | 低 | 中 | server graceful shutdown 機制(既有)| 低 |
|
||
| 升級當降版(誤把新版降到舊版、反向)| 低 | 低 | driver guard:不允許目標版本 >= current | 已消除 |
|
||
| 跨晶片誤匹配 | 極低 | 高 | driver guard:version 必須在 `ListFirmwareVersions(chip)` 內 | 已消除 |
|
||
|
||
### 6.2 殘餘風險最大的:使用者不讀警告就降版
|
||
|
||
**對策**:
|
||
- 二次確認字串「DOWNGRADE」(不是 「OK」/「Yes」、強制使用者打字)
|
||
- 視覺破壞性 button color(紅色)
|
||
- 「進行中」期間 banner persist(不可關)
|
||
|
||
但**無法完全消除**——一般使用者場景下、總有人會無視警告。
|
||
|
||
### 6.3 Plan B:給技術支援用的救磚 SOP
|
||
|
||
如果使用者真的把 dongle 弄 brick、需要:
|
||
- DFUT.exe(Windows-only Qt 工具、可從 Kneron 拿)
|
||
- 燒回 KDP2 standard firmware
|
||
|
||
**不打包 DFUT.exe 進 visionA-local**(因為跨平台限制 + 30MB 額外大小 + 安全性考量)、但**提供 SOP 文件**(內部 wiki / docs/troubleshooting/brick-recovery.md)讓技術支援能幫忙。
|
||
|
||
---
|
||
|
||
## 7. 法律 / 合規 考量(待釐清)
|
||
|
||
承前一份 R-FW-5:「打包 Kneron 官方 firmware 是否合法」。
|
||
|
||
**B2 階段新增變數**:
|
||
- 多版本 bundle 含舊版 firmware(v2.1.0、KDP1)→ 法律問題加重?
|
||
- 暴露給一般使用者降版 → Kneron 是否允許?(KDP1 是被棄用版本、Kneron 不一定希望使用者降回去)
|
||
|
||
**建議**:
|
||
- B 階段啟動前、與 Kneron 取得 firmware re-distribution **明確書面授權**
|
||
- 授權內容必須包含:current + 舊版(v2.1.0 等)+ KDP1
|
||
- 如果 Kneron 不允許降版到 KDP1、調整 bundle 範圍
|
||
|
||
---
|
||
|
||
## 8. 給 Orchestrator 的決策點清單
|
||
|
||
1. **使用者已決策手動降版面向一般使用者** → 本檔 §5 design 需求成立
|
||
2. **使用者已決策 FW 內嵌進安裝包、+5MB 接受** → 本檔 §4.3「保守」策略對齊
|
||
3. **使用者已決策不做線上更新通道** → 不實作 OTA、所有 firmware bundle
|
||
4. **待使用者裁決**:
|
||
- 多版本目錄結構選 A/B/C?(建議 C、§3.2)
|
||
- bundle 哪些舊版?(建議「保守」策略、§4.3)
|
||
- KneronPLUS wheel 升級 vs 不升級?(M9-6 驗證後決定)
|
||
- 與 Kneron 取得 firmware redistribution 授權的時程?
|
||
|
||
---
|
||
|
||
## 9. 與其他研究檔的關係
|
||
|
||
| 連結 | 引用 |
|
||
|------|------|
|
||
| `30-integration-plan.md` §6 階段 B 評估提示 | 本檔詳化 B2 階段細節 |
|
||
| `40-b-phase-kl630-kl730-extension.md` §8 多版本 firmware 並存 | 本檔 §3 詳化儲存結構 |
|
||
| `40-b-phase-kl630-kl730-extension.md` R-FW-11/R-FW-12 | 本檔 §5/§6 提供緩解措施 |
|
||
| `41-tar-firmware-handling.md` §3 大小估算 | 本檔 §4 整合計算 |
|
||
|
||
---
|
||
|
||
## 10. 工時影響補充
|
||
|
||
B 階段拆三層之後、本檔內容對應 M9-11 + M9-12 兩個 milestone:
|
||
|
||
| Milestone | 本檔 § | 工時 |
|
||
|-----------|--------|------|
|
||
| M9-11 多版本後端 | §2.1 ~ §2.4 + §3 + §4 | 1.5 人天 |
|
||
| M9-12 降版 UI | §5 + §6 + §7 | Frontend 1 人天 + Design 1 人天 = 2 人天 |
|
||
| **合計** | | **3.5 人天** |
|
||
|
||
(已含在 §40 B 階段 M9-6 ~ M9-13 工時表內、本檔不重複加總)
|