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>
This commit is contained in:
parent
f8533a6b04
commit
4902cb5531
@ -1,16 +1,24 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useDeviceStore } from '@/stores/device-store';
|
import { useDeviceStore } from '@/stores/device-store';
|
||||||
import { useDeviceEvents } from '@/hooks/use-device-events';
|
import { useDeviceEvents } from '@/hooks/use-device-events';
|
||||||
import { DeviceList } from '@/components/devices/device-list';
|
import { DeviceList } from '@/components/devices/device-list';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw, Usb } from 'lucide-react';
|
||||||
import { useTranslation } from '@/lib/i18n';
|
import { useTranslation } from '@/lib/i18n';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
function isWindows(): boolean {
|
||||||
|
if (typeof navigator === 'undefined') return false;
|
||||||
|
return /Windows/i.test(navigator.userAgent) || /Win32|Win64/i.test(navigator.platform);
|
||||||
|
}
|
||||||
|
|
||||||
export default function DevicesPage() {
|
export default function DevicesPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { devices, loading, scanning, fetchDevices, scanDevices } = useDeviceStore();
|
const { devices, loading, scanning, fetchDevices, scanDevices } = useDeviceStore();
|
||||||
|
const [installingDriver, setInstallingDriver] = useState(false);
|
||||||
|
|
||||||
useDeviceEvents();
|
useDeviceEvents();
|
||||||
|
|
||||||
@ -18,6 +26,22 @@ export default function DevicesPage() {
|
|||||||
fetchDevices();
|
fetchDevices();
|
||||||
}, [fetchDevices]);
|
}, [fetchDevices]);
|
||||||
|
|
||||||
|
async function handleInstallDriver() {
|
||||||
|
setInstallingDriver(true);
|
||||||
|
try {
|
||||||
|
const res = await api.post<{ message: string }>('/system/install-driver');
|
||||||
|
if (res.success) {
|
||||||
|
toast.success(res.data?.message || 'WinUSB driver 安裝完成,請重新點擊連線。');
|
||||||
|
} else {
|
||||||
|
toast.error(res.error?.message || 'driver 安裝失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setInstallingDriver(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -25,11 +49,24 @@ export default function DevicesPage() {
|
|||||||
<h1 className="text-2xl font-bold">{t('devices.title')}</h1>
|
<h1 className="text-2xl font-bold">{t('devices.title')}</h1>
|
||||||
<p className="text-muted-foreground">{t('devices.subtitle')}</p>
|
<p className="text-muted-foreground">{t('devices.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isWindows() && (
|
||||||
|
<Button
|
||||||
|
onClick={handleInstallDriver}
|
||||||
|
disabled={installingDriver}
|
||||||
|
variant="outline"
|
||||||
|
title="若連線失敗出現 WinUSB 相關錯誤,點此安裝 driver(需要系統管理員權限)"
|
||||||
|
>
|
||||||
|
<Usb className={`mr-2 h-4 w-4 ${installingDriver ? 'animate-pulse' : ''}`} />
|
||||||
|
{installingDriver ? '安裝中...' : '安裝 USB Driver'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button onClick={scanDevices} disabled={scanning} variant="outline" data-tour-id="scan-devices-btn">
|
<Button onClick={scanDevices} disabled={scanning} variant="outline" data-tour-id="scan-devices-btn">
|
||||||
<RefreshCw className={`mr-2 h-4 w-4 ${scanning ? 'animate-spin' : ''}`} />
|
<RefreshCw className={`mr-2 h-4 w-4 ${scanning ? 'animate-spin' : ''}`} />
|
||||||
{scanning ? t('devices.scanning') : t('devices.scan')}
|
{scanning ? t('devices.scanning') : t('devices.scan')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<DeviceList devices={devices} loading={loading} />
|
<DeviceList devices={devices} loading={loading} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -71,9 +71,21 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
|||||||
showSuccess(getTranslation().t('errors.deviceConnected'));
|
showSuccess(getTranslation().t('errors.deviceConnected'));
|
||||||
const name = get().devices.find((d) => d.id === id)?.name || id;
|
const name = get().devices.find((d) => d.id === id)?.name || id;
|
||||||
useActivityStore.getState().addActivity('device_connect', `Device connected: ${name}`);
|
useActivityStore.getState().addActivity('device_connect', `Device connected: ${name}`);
|
||||||
|
} else {
|
||||||
|
const msg = res.error?.message || '';
|
||||||
|
// 偵測 Kneron WinUSB driver 未安裝的特徵錯誤訊息(error 28 / KP_ERROR_CONNECT_FAILED)
|
||||||
|
// 若命中,給使用者明確指引去點「安裝 USB Driver」按鈕,而不只是顯示原始錯誤
|
||||||
|
if (/winusb|error.{0,5}code.{0,5}28|KP_ERROR_CONNECT_FAILED/i.test(msg)) {
|
||||||
|
showApiError({
|
||||||
|
code: 'DRIVER_NOT_INSTALLED',
|
||||||
|
message:
|
||||||
|
'連線失敗:Kneron WinUSB driver 尚未安裝。\n' +
|
||||||
|
'請點擊右上角「安裝 USB Driver」按鈕(需要系統管理員權限),安裝完成後重新點選連線。',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
showApiError(res.error);
|
showApiError(res.error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
get().fetchDevices();
|
get().fetchDevices();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -92,11 +92,13 @@ Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
|
|||||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
; 安裝 WinUSB driver(pnputil 為 Windows 內建)
|
; 注意:WinUSB driver 安裝**不在 installer 階段做**。
|
||||||
Filename: "{sys}\pnputil.exe"; \
|
; 原因:inf-based 安裝需要 .cat + Microsoft attestation signing,未簽章的 .inf 會被
|
||||||
Parameters: "/add-driver ""{app}\drivers\kneron_winusb.inf"" /install"; \
|
; Windows 10/11 的 driver signing enforcement 擋掉。
|
||||||
StatusMsg: "正在安裝 Kneron WinUSB driver..."; \
|
; 改為:Wails app 首次啟動時(或使用者手動點「安裝 driver」按鈕時)透過
|
||||||
Flags: runhidden waituntilterminated
|
; KneronPLUS SDK 的 kp.core.install_driver_for_windows() 呼叫 libwdi 完成,
|
||||||
|
; libwdi 會自動處理臨時自簽憑證,不需要 .cat 檔。
|
||||||
|
; 需要 UAC elevation,Wails app 會彈提權對話框。
|
||||||
|
|
||||||
; 安裝完畢選擇性啟動 app
|
; 安裝完畢選擇性啟動 app
|
||||||
Filename: "{app}\{#MyAppExeName}"; \
|
Filename: "{app}\{#MyAppExeName}"; \
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// installKneronWinUSBDriverHandler 在非 Windows 平台不做事(stub)。
|
||||||
|
// InstallDriver handler 會先判斷 runtime.GOOS 在這之前回錯,所以這個 stub 不應被呼叫。
|
||||||
|
func installKneronWinUSBDriverHandler(_ string) error {
|
||||||
|
return fmt.Errorf("only Windows needs WinUSB driver installation")
|
||||||
|
}
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
//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
|
||||||
|
}
|
||||||
@ -14,15 +14,17 @@ type SystemHandler struct {
|
|||||||
startTime time.Time
|
startTime time.Time
|
||||||
version string
|
version string
|
||||||
buildTime string
|
buildTime string
|
||||||
|
pythonBin string // 由 main.go 傳入,InstallDriver 會用到
|
||||||
shutdownFn func()
|
shutdownFn func()
|
||||||
depsCache []deps.Dependency
|
depsCache []deps.Dependency
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSystemHandler(version, buildTime string, shutdownFn func()) *SystemHandler {
|
func NewSystemHandler(version, buildTime, pythonBin string, shutdownFn func()) *SystemHandler {
|
||||||
return &SystemHandler{
|
return &SystemHandler{
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
version: version,
|
version: version,
|
||||||
buildTime: buildTime,
|
buildTime: buildTime,
|
||||||
|
pythonBin: pythonBin,
|
||||||
shutdownFn: shutdownFn,
|
shutdownFn: shutdownFn,
|
||||||
depsCache: deps.CheckAll(),
|
depsCache: deps.CheckAll(),
|
||||||
}
|
}
|
||||||
@ -73,6 +75,46 @@ func (h *SystemHandler) Deps(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func (h *SystemHandler) Restart(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{"success": true, "data": gin.H{"message": "restarting"}})
|
c.JSON(200, gin.H{"success": true, "data": gin.H{"message": "restarting"}})
|
||||||
if f, ok := c.Writer.(http.Flusher); ok {
|
if f, ok := c.Writer.(http.Flusher); ok {
|
||||||
|
|||||||
@ -49,6 +49,7 @@ func NewRouter(
|
|||||||
api.GET("/system/metrics", systemHandler.Metrics)
|
api.GET("/system/metrics", systemHandler.Metrics)
|
||||||
api.GET("/system/deps", systemHandler.Deps)
|
api.GET("/system/deps", systemHandler.Deps)
|
||||||
api.POST("/system/restart", systemHandler.Restart)
|
api.POST("/system/restart", systemHandler.Restart)
|
||||||
|
api.POST("/system/install-driver", systemHandler.InstallDriver)
|
||||||
|
|
||||||
// Models
|
// Models
|
||||||
api.GET("/models", modelHandler.ListModels)
|
api.GET("/models", modelHandler.ListModels)
|
||||||
|
|||||||
@ -172,8 +172,15 @@ func main() {
|
|||||||
shutdownFn()
|
shutdownFn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve python bin (used by InstallDriver handler on Windows).
|
||||||
|
// Priority: VISIONA_PYTHON env var (set by Wails shell) → cfg.PythonBin (--python flag)
|
||||||
|
pythonBinForSystem := os.Getenv("VISIONA_PYTHON")
|
||||||
|
if pythonBinForSystem == "" {
|
||||||
|
pythonBinForSystem = cfg.PythonBin
|
||||||
|
}
|
||||||
|
|
||||||
// Create system handler with injected version and restart function
|
// Create system handler with injected version and restart function
|
||||||
systemHandler := handlers.NewSystemHandler(Version, BuildTime, restartFn)
|
systemHandler := handlers.NewSystemHandler(Version, BuildTime, pythonBinForSystem, restartFn)
|
||||||
|
|
||||||
// Create router
|
// Create router
|
||||||
r := api.NewRouter(modelRepo, modelStore, deviceMgr, cameraMgr, inferenceSvc, wsHub, staticFS, logBroadcaster, systemHandler)
|
r := api.NewRouter(modelRepo, modelStore, deviceMgr, cameraMgr, inferenceSvc, wsHub, staticFS, logBroadcaster, systemHandler)
|
||||||
|
|||||||
@ -310,6 +310,31 @@ func (a *App) OpenBrowser(url string) error {
|
|||||||
return openBrowser(url)
|
return openBrowser(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InstallKneronDriver 呼叫 KneronPLUS SDK 的 libwdi wrapper 安裝 WinUSB driver。
|
||||||
|
//
|
||||||
|
// Windows 下需要 UAC 提權;macOS / Linux 下會直接回錯誤訊息(用不到)。
|
||||||
|
// 前端在使用者點「安裝 Kneron USB driver」按鈕、或偵測到 connect 失敗 + error 28 時呼叫。
|
||||||
|
//
|
||||||
|
// 呼叫前 Python venv 必須已就緒(ensureBundledPython 完成 + kp wheel 已裝)。
|
||||||
|
// 此 binding 會自動嘗試重新解析 python 路徑。
|
||||||
|
func (a *App) InstallKneronDriver() error {
|
||||||
|
a.mu.Lock()
|
||||||
|
pyBin := a.pythonBin
|
||||||
|
a.mu.Unlock()
|
||||||
|
|
||||||
|
if pyBin == "" {
|
||||||
|
// 可能使用者手動觸發、server 還沒啟動或 ensurePythonRuntime 失敗
|
||||||
|
// 嘗試重新 resolve
|
||||||
|
resolved, _, err := a.ensurePythonRuntime(a.pythonMode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("無法取得 Python interpreter:%w", err)
|
||||||
|
}
|
||||||
|
pyBin = resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
return installKneronWinUSBDriver(pyBin)
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Server 子行程管理
|
// Server 子行程管理
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -11,6 +12,11 @@ import (
|
|||||||
// configureSysProcAttr 在 macOS 上不需要特殊設定。
|
// configureSysProcAttr 在 macOS 上不需要特殊設定。
|
||||||
func configureSysProcAttr(_ *exec.Cmd) {}
|
func configureSysProcAttr(_ *exec.Cmd) {}
|
||||||
|
|
||||||
|
// installKneronWinUSBDriver 在 macOS 上是 no-op(macOS 不需要 driver,kernel 會直接給 kext/IOKit 權限)。
|
||||||
|
func installKneronWinUSBDriver(_ string) error {
|
||||||
|
return fmt.Errorf("macOS 不需要安裝 WinUSB driver — Kneron SDK 會直接使用 IOKit")
|
||||||
|
}
|
||||||
|
|
||||||
// platformDataDir 回傳 macOS 的應用程式資料目錄。
|
// platformDataDir 回傳 macOS 的應用程式資料目錄。
|
||||||
// ~/Library/Application Support/visiona-local
|
// ~/Library/Application Support/visiona-local
|
||||||
func platformDataDir() string {
|
func platformDataDir() string {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -11,6 +12,12 @@ import (
|
|||||||
// configureSysProcAttr 在 Linux 上不需要特殊設定。
|
// configureSysProcAttr 在 Linux 上不需要特殊設定。
|
||||||
func configureSysProcAttr(_ *exec.Cmd) {}
|
func configureSysProcAttr(_ *exec.Cmd) {}
|
||||||
|
|
||||||
|
// installKneronWinUSBDriver 在 Linux 上不需要 WinUSB — 改用 udev rule(installer 已放到 /etc/udev/rules.d)
|
||||||
|
// 這個 stub 只為了跨平台 Go build 能通過。
|
||||||
|
func installKneronWinUSBDriver(_ string) error {
|
||||||
|
return fmt.Errorf("Linux 不需要安裝 WinUSB driver — 請確認 /etc/udev/rules.d/99-kneron.rules 已放置並執行 udevadm reload")
|
||||||
|
}
|
||||||
|
|
||||||
// platformDataDir 回傳 Linux 的應用程式資料目錄(遵循 XDG)。
|
// platformDataDir 回傳 Linux 的應用程式資料目錄(遵循 XDG)。
|
||||||
// $XDG_DATA_HOME/visiona-local 或 ~/.local/share/visiona-local
|
// $XDG_DATA_HOME/visiona-local 或 ~/.local/share/visiona-local
|
||||||
func platformDataDir() string {
|
func platformDataDir() string {
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,3 +32,88 @@ func platformDataDir() string {
|
|||||||
}
|
}
|
||||||
return filepath.Join(appdata, appName)
|
return filepath.Join(appdata, appName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// installKneronWinUSBDriver 呼叫 KneronPLUS SDK 的 libwdi wrapper 安裝 WinUSB driver。
|
||||||
|
//
|
||||||
|
// 為什麼需要:
|
||||||
|
// - Kneron USB 裝置預設沒有綁定 WinUSB driver,KneronPLUS SDK 無法打開 handle → connect 失敗(error 28)
|
||||||
|
// - inf-based pnputil 安裝需要 .cat 簽章,我們沒有;libwdi 會自己用臨時自簽憑證解決
|
||||||
|
//
|
||||||
|
// 實作參考 edge-ai-platform installer/platform_windows.go 的 installKneronDriverViaSDK:
|
||||||
|
// - 組一段 Python script 呼叫 kp.core.install_driver_for_windows(pid) 對 KL520/KL720/KL720_LEGACY 三種 PID
|
||||||
|
// - 用 PowerShell Start-Process -Verb RunAs 提權執行(libwdi 要求 admin)
|
||||||
|
// - 結果寫到 temp 檔讓 Go 這邊讀回來
|
||||||
|
//
|
||||||
|
// 呼叫者:前端「安裝 Kneron USB driver」按鈕 → App.InstallKneronDriver() binding → 這個函式
|
||||||
|
// 需要:venv 已建好(ensureBundledPython 完成)且 kp 模組已 import
|
||||||
|
func installKneronWinUSBDriver(pythonBin string) error {
|
||||||
|
if pythonBin == "" {
|
||||||
|
return fmt.Errorf("python interpreter not available — 請確認 bundled Python runtime 已完成初始化")
|
||||||
|
}
|
||||||
|
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 根目錄(python.exe 在 venv\Scripts\python.exe 底下)
|
||||||
|
venvRoot := filepath.Dir(filepath.Dir(pythonBin))
|
||||||
|
|
||||||
|
pyScript := fmt.Sprintf(`
|
||||||
|
import sys, os
|
||||||
|
result_path = r'%s'
|
||||||
|
try:
|
||||||
|
# 讓 kp 模組能找到 native DLL
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 用 PowerShell Start-Process -Verb RunAs 觸發 UAC,libwdi 要求 admin
|
||||||
|
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)
|
||||||
|
configureSysProcAttr(cmd)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("driver 安裝需要系統管理員權限:%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(可改用 Zadig 手動安裝 https://zadig.akeo.ie/)", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user