根因: 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>
86 lines
2.9 KiB
Go
86 lines
2.9 KiB
Go
//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
|
||
}
|