visionA/local-tool/server/internal/api/handlers/system_driver_windows.go
jim800121chen 4902cb5531 feat(local-tool): Kneron WinUSB driver 透過 KneronPLUS SDK libwdi 安裝
根因:
  KP_ERROR_CONNECT_FAILED (error code 28) — Kneron USB 裝置預設沒綁定
  WinUSB driver,SDK 無法開 handle。原 .iss 的 pnputil /add-driver 做法
  需要 .cat 簽章(Windows 10/11 driver signing enforcement),我們沒有。

參考 edge-ai-platform/installer/platform_windows.go 的 installKneronDriverViaSDK:
  KneronPLUS SDK 內建 libwdi wrapper — kp.core.install_driver_for_windows(pid)
  libwdi 會自動用臨時自簽憑證,不需要 .cat 檔,只需要 UAC 提權。

實作:
- server/internal/api/handlers/system_driver_windows.go(新):
  組 Python script → kp.core.install_driver_for_windows 對 KL520/KL720/KL720_LEGACY →
  PowerShell Start-Process -Verb RunAs 提權執行 → 結果寫 temp 檔讀回
- server/internal/api/handlers/system_driver_other.go(新):非 Windows stub
- system_handler.go: NewSystemHandler 新增 pythonBin 參數 + InstallDriver handler
  先判斷 runtime.GOOS==windows 才執行
- router.go: 新增 POST /system/install-driver
- main.go: 解析 pythonBin(VISIONA_PYTHON env var → cfg.PythonBin)傳入

前端:
- frontend/src/app/devices/page.tsx:Windows only 多一個「安裝 USB Driver」按鈕
  (用 navigator.userAgent 判斷平台)
- frontend/src/stores/device-store.ts:connect 失敗訊息偵測 winusb / error 28 /
  KP_ERROR_CONNECT_FAILED 特徵字串,回一條明確的繁中提示引導使用者去點按鈕

Wails app 端:
- visiona-local/app.go: 新增 InstallKneronDriver() binding 作為備用入口(目前前端沒用到,
  因為前端跑在 http://127.0.0.1 不是 wails://,但保留給未來 splash 階段觸發用)
- visiona-local/platform_{windows,darwin,linux}.go: installKneronWinUSBDriver 平台實作
  / 跨平台 stub

.iss:
- 移除 pnputil /add-driver 的 [Run] entry(它是 silent-fail 的 dead code,
  因為沒 .cat 簽章)
- 新增註解說明:driver 安裝改由 app 端呼叫 libwdi 完成

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 05:25:32 +08:00

86 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build windows
package handlers
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)
// installKneronWinUSBDriverHandler 呼叫 KneronPLUS SDK 的 libwdi wrapper 安裝 WinUSB driver。
//
// 實作參考 edge-ai-platform installer/platform_windows.go 的 installKneronDriverViaSDK
// - 組 Python script 呼叫 kp.core.install_driver_for_windows(pid) 對三種 PID
// - PowerShell Start-Process -Verb RunAs 提權libwdi 要求 admin
// - 結果寫到 temp file 讓 Go 讀回來
func installKneronWinUSBDriverHandler(pythonBin string) error {
if _, err := os.Stat(pythonBin); err != nil {
return fmt.Errorf("python interpreter not found at %s: %w", pythonBin, err)
}
resultPath := filepath.Join(os.TempDir(), "visiona-local-driver-result.txt")
_ = os.Remove(resultPath)
// venv 根目錄
venvRoot := filepath.Dir(filepath.Dir(pythonBin))
pyScript := fmt.Sprintf(`
import sys, os
result_path = r'%s'
try:
kp_lib = os.path.join(r'%s', 'Lib', 'site-packages', 'kp', 'lib')
if os.path.isdir(kp_lib):
os.environ['PATH'] = kp_lib + ';' + os.environ.get('PATH', '')
os.add_dll_directory(kp_lib)
import kp
results = []
for pid in [kp.ProductId.KP_DEVICE_KL520, kp.ProductId.KP_DEVICE_KL720, kp.ProductId.KP_DEVICE_KL720_LEGACY]:
try:
kp.core.install_driver_for_windows(pid)
results.append(f"OK: {pid.name}")
except Exception as e:
results.append(f"SKIP: {pid.name}: {e}")
with open(result_path, 'w') as f:
f.write('\n'.join(results) + '\nDONE\n')
except ImportError as e:
with open(result_path, 'w') as f:
f.write(f'ERROR: kp module not available: {e}\n')
except Exception as e:
with open(result_path, 'w') as f:
f.write(f'ERROR: {e}\n')
`, resultPath, venvRoot)
scriptPath := filepath.Join(os.TempDir(), "visiona-local-install-usb-driver.py")
if err := os.WriteFile(scriptPath, []byte(pyScript), 0o644); err != nil {
return fmt.Errorf("write driver install script: %w", err)
}
defer os.Remove(scriptPath)
elevateCmd := fmt.Sprintf(
`Start-Process -FilePath '%s' -ArgumentList '"%s"' -Verb RunAs -Wait -WindowStyle Hidden`,
pythonBin, scriptPath,
)
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", elevateCmd)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("driver 安裝需要系統管理員權限UAC 可能被拒絕):%s", strings.TrimSpace(string(out)))
}
resultData, err := os.ReadFile(resultPath)
_ = os.Remove(resultPath)
if err != nil {
return fmt.Errorf("driver 安裝已執行但無法讀取結果,請在裝置管理員確認或用 Zadig 手動安裝 https://zadig.akeo.ie/")
}
result := strings.TrimSpace(string(resultData))
if strings.Contains(result, "ERROR:") {
return fmt.Errorf("driver 安裝失敗:%s", result)
}
return nil
}