根因: 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>
132 lines
3.2 KiB
Go
132 lines
3.2 KiB
Go
package handlers
|
||
|
||
import (
|
||
"net/http"
|
||
"runtime"
|
||
"time"
|
||
|
||
"visiona-local/server/internal/deps"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type SystemHandler struct {
|
||
startTime time.Time
|
||
version string
|
||
buildTime string
|
||
pythonBin string // 由 main.go 傳入,InstallDriver 會用到
|
||
shutdownFn func()
|
||
depsCache []deps.Dependency
|
||
}
|
||
|
||
func NewSystemHandler(version, buildTime, pythonBin string, shutdownFn func()) *SystemHandler {
|
||
return &SystemHandler{
|
||
startTime: time.Now(),
|
||
version: version,
|
||
buildTime: buildTime,
|
||
pythonBin: pythonBin,
|
||
shutdownFn: shutdownFn,
|
||
depsCache: deps.CheckAll(),
|
||
}
|
||
}
|
||
|
||
func (h *SystemHandler) HealthCheck(c *gin.Context) {
|
||
c.JSON(200, gin.H{"status": "ok"})
|
||
}
|
||
|
||
func (h *SystemHandler) Info(c *gin.Context) {
|
||
c.JSON(200, gin.H{
|
||
"success": true,
|
||
"data": gin.H{
|
||
"version": h.version,
|
||
"platform": runtime.GOOS + "/" + runtime.GOARCH,
|
||
"uptime": time.Since(h.startTime).Seconds(),
|
||
"goVersion": runtime.Version(),
|
||
},
|
||
})
|
||
}
|
||
|
||
func (h *SystemHandler) Metrics(c *gin.Context) {
|
||
var ms runtime.MemStats
|
||
runtime.ReadMemStats(&ms)
|
||
|
||
c.JSON(200, gin.H{
|
||
"success": true,
|
||
"data": gin.H{
|
||
"version": h.version,
|
||
"buildTime": h.buildTime,
|
||
"platform": runtime.GOOS + "/" + runtime.GOARCH,
|
||
"goVersion": runtime.Version(),
|
||
"uptimeSeconds": time.Since(h.startTime).Seconds(),
|
||
"goroutines": runtime.NumGoroutine(),
|
||
"memHeapAllocMB": float64(ms.HeapAlloc) / 1024 / 1024,
|
||
"memSysMB": float64(ms.Sys) / 1024 / 1024,
|
||
"memHeapObjects": ms.HeapObjects,
|
||
"gcCycles": ms.NumGC,
|
||
"nextGcMB": float64(ms.NextGC) / 1024 / 1024,
|
||
},
|
||
})
|
||
}
|
||
|
||
func (h *SystemHandler) Deps(c *gin.Context) {
|
||
c.JSON(200, gin.H{
|
||
"success": true,
|
||
"data": gin.H{"deps": h.depsCache},
|
||
})
|
||
}
|
||
|
||
// InstallDriver 安裝 Kneron WinUSB driver(僅 Windows 有意義)。
|
||
// 呼叫 platform-specific 實作(見 system_driver_windows.go / system_driver_other.go)。
|
||
func (h *SystemHandler) InstallDriver(c *gin.Context) {
|
||
if runtime.GOOS != "windows" {
|
||
c.JSON(400, gin.H{
|
||
"success": false,
|
||
"error": gin.H{
|
||
"code": "DRIVER_NOT_NEEDED",
|
||
"message": runtime.GOOS + " 不需要安裝 WinUSB driver",
|
||
},
|
||
})
|
||
return
|
||
}
|
||
pyBin := h.pythonBin
|
||
if pyBin == "" {
|
||
c.JSON(500, gin.H{
|
||
"success": false,
|
||
"error": gin.H{
|
||
"code": "PYTHON_NOT_AVAILABLE",
|
||
"message": "Python interpreter 未就緒,請確認 bundled Python runtime 初始化完成",
|
||
},
|
||
})
|
||
return
|
||
}
|
||
if err := installKneronWinUSBDriverHandler(pyBin); err != nil {
|
||
c.JSON(500, gin.H{
|
||
"success": false,
|
||
"error": gin.H{
|
||
"code": "DRIVER_INSTALL_FAILED",
|
||
"message": err.Error(),
|
||
},
|
||
})
|
||
return
|
||
}
|
||
c.JSON(200, gin.H{
|
||
"success": true,
|
||
"data": gin.H{"message": "WinUSB driver 安裝完成,請重新插拔裝置或直接點擊連線。"},
|
||
})
|
||
}
|
||
|
||
func (h *SystemHandler) Restart(c *gin.Context) {
|
||
c.JSON(200, gin.H{"success": true, "data": gin.H{"message": "restarting"}})
|
||
if f, ok := c.Writer.(http.Flusher); ok {
|
||
f.Flush()
|
||
}
|
||
|
||
go func() {
|
||
time.Sleep(200 * time.Millisecond)
|
||
// shutdownFn signals the main goroutine to perform exec after server shutdown
|
||
if h.shutdownFn != nil {
|
||
h.shutdownFn()
|
||
}
|
||
}()
|
||
}
|