diff --git a/edge-ai-platform/server/internal/driver/kneron/detector.go b/edge-ai-platform/server/internal/driver/kneron/detector.go index 69103bc..c7057be 100644 --- a/edge-ai-platform/server/internal/driver/kneron/detector.go +++ b/edge-ai-platform/server/internal/driver/kneron/detector.go @@ -13,17 +13,32 @@ import ( ) // ResolvePython finds the best Python interpreter for the given script path. -// Search order: script-local venv → parent venv → ~/.edge-ai-platform/venv → system python3. +// Search order: script-local venv → parent venv → platform config dir venv → system python3/python. func ResolvePython(scriptPath string) string { scriptDir := filepath.Dir(scriptPath) + parentDir := filepath.Dir(scriptDir) - candidates := []string{ - filepath.Join(scriptDir, "venv", "bin", "python3"), - filepath.Join(filepath.Dir(scriptDir), "venv", "bin", "python3"), + // Build candidate list with both Unix and Windows venv layouts + var candidates []string + for _, base := range []string{scriptDir, parentDir} { + candidates = append(candidates, + filepath.Join(base, "venv", "bin", "python3"), // Unix + filepath.Join(base, "venv", "Scripts", "python.exe"), // Windows + ) } if home, err := os.UserHomeDir(); err == nil { - candidates = append(candidates, filepath.Join(home, ".edge-ai-platform", "venv", "bin", "python3")) + candidates = append(candidates, + filepath.Join(home, ".edge-ai-platform", "venv", "bin", "python3"), + filepath.Join(home, ".edge-ai-platform", "venv", "Scripts", "python.exe"), + ) + } + + // On Windows, also check %LOCALAPPDATA%\EdgeAIPlatform\venv + if appData := os.Getenv("LOCALAPPDATA"); appData != "" { + candidates = append(candidates, + filepath.Join(appData, "EdgeAIPlatform", "venv", "Scripts", "python.exe"), + ) } for _, p := range candidates { @@ -32,6 +47,13 @@ func ResolvePython(scriptPath string) string { } } + // Fallback to system python + for _, name := range []string{"python3", "python"} { + if p, err := exec.LookPath(name); err == nil { + return p + } + } + return "python3" } @@ -70,6 +92,14 @@ func DetectDevices(scriptPath string) []driver.DeviceInfo { cmd := exec.Command(pythonBin, scriptPath) cmd.Stdin = nil + // Ensure libusb-1.0.dll can be found on Windows by adding the binary's + // directory to PATH (the installer places the DLL there). + scriptDir := filepath.Dir(scriptPath) + installDir := filepath.Dir(scriptDir) + cmd.Env = append(os.Environ(), + fmt.Sprintf("PATH=%s;%s;%s", installDir, scriptDir, os.Getenv("PATH")), + ) + stdinPipe, err := cmd.StdinPipe() if err != nil { return nil diff --git a/edge-ai-platform/server/internal/driver/kneron/kl720_driver.go b/edge-ai-platform/server/internal/driver/kneron/kl720_driver.go index 1442315..f8d079c 100644 --- a/edge-ai-platform/server/internal/driver/kneron/kl720_driver.go +++ b/edge-ai-platform/server/internal/driver/kneron/kl720_driver.go @@ -113,6 +113,14 @@ func (d *KneronDriver) startPython() error { } } + // On Windows, ensure libusb-1.0.dll can be found by adding the install + // directory to PATH (installer places the DLL there). + if runtime.GOOS == "windows" { + installDir := filepath.Dir(scriptDir) + cmd.Env = append(cmd.Env, + fmt.Sprintf("PATH=%s;%s;%s", installDir, scriptDir, os.Getenv("PATH"))) + } + stdinPipe, err := cmd.StdinPipe() if err != nil { return fmt.Errorf("failed to create stdin pipe: %w", err) diff --git a/edge-ai-platform/server/tray/assets/icon_running.ico b/edge-ai-platform/server/tray/assets/icon_running.ico new file mode 100644 index 0000000..1f6ac7c Binary files /dev/null and b/edge-ai-platform/server/tray/assets/icon_running.ico differ diff --git a/edge-ai-platform/server/tray/assets/icon_stopped.ico b/edge-ai-platform/server/tray/assets/icon_stopped.ico new file mode 100644 index 0000000..c5189e3 Binary files /dev/null and b/edge-ai-platform/server/tray/assets/icon_stopped.ico differ diff --git a/edge-ai-platform/server/tray/webgui.go b/edge-ai-platform/server/tray/webgui.go index 2545029..48e9176 100644 --- a/edge-ai-platform/server/tray/webgui.go +++ b/edge-ai-platform/server/tray/webgui.go @@ -3,6 +3,7 @@ package tray import ( + _ "embed" "fmt" "log" "os" @@ -13,6 +14,12 @@ import ( "unsafe" ) +//go:embed assets/icon_running.ico +var icoRunning []byte + +//go:embed assets/icon_stopped.ico +var icoStopped []byte + // Windows API constants const ( wmApp = 0x8000 @@ -21,7 +28,6 @@ const ( wmDestroy = 0x0002 wmLButtonUp = 0x0202 wmRButtonUp = 0x0205 - wmContextMenu = 0x007B wmClose = 0x0010 wmQueryEndSess = 0x0011 @@ -42,14 +48,9 @@ const ( mfEnabled = 0x00000000 mfGrayed = 0x00000001 - tpmRightAlign = 0x0008 - tpmBottomAlign = 0x0020 - - imageIcon = 1 - lrShared = 0x00008000 - idiApp = 32512 // default application icon - swHide = 0 + + createNoWindow = 0x08000000 ) // NOTIFYICONDATAW for Shell_NotifyIconW @@ -83,9 +84,12 @@ var ( procPostMessage = user32.NewProc("PostMessageW") procGetCursorPos = user32.NewProc("GetCursorPos") procLoadIcon = user32.NewProc("LoadIconW") + procCreateIconFromResource = user32.NewProc("CreateIconFromResource") + procDestroyIcon = user32.NewProc("DestroyIcon") procGetModuleHandle = kernel32.NewProc("GetModuleHandleW") procGetConsoleWindow = kernel32.NewProc("GetConsoleWindow") procShowWindow = user32.NewProc("ShowWindow") + procFreeConsole = kernel32.NewProc("FreeConsole") ) type point struct { @@ -120,13 +124,33 @@ type msg struct { var globalApp *nativeTrayApp type nativeTrayApp struct { - mu sync.Mutex - cmd *exec.Cmd - running bool - cfg *Config - hwnd uintptr - nid notifyIconData - hIcon uintptr + mu sync.Mutex + cmd *exec.Cmd + running bool + cfg *Config + hwnd uintptr + nid notifyIconData + hIconRun uintptr + hIconStop uintptr +} + +// loadIconFromICO creates an HICON from raw ICO data. +// ICO format: 6-byte header + 16-byte directory entry + BMP/PNG data. +// We skip the ICO header (6 + 16 = 22 bytes) and pass the BMP data +// to CreateIconFromResource. +func loadIconFromICO(data []byte) uintptr { + if len(data) < 22 { + return 0 + } + // Skip ICO header (6 bytes) + directory entry (16 bytes) to get BMP data + bmpData := data[22:] + icon, _, _ := procCreateIconFromResource.Call( + uintptr(unsafe.Pointer(&bmpData[0])), + uintptr(len(bmpData)), + 1, // fIcon = TRUE + 0x30000, // version 0x00030000 + ) + return icon } // RunWebGUI starts a native Windows system tray application. @@ -134,16 +158,25 @@ func RunWebGUI(cfg *Config) { app := &nativeTrayApp{cfg: cfg} globalApp = app - // Hide console window + // Hide and detach console window so closing it won't kill us if hwndConsole, _, _ := procGetConsoleWindow.Call(); hwndConsole != 0 { procShowWindow.Call(hwndConsole, swHide) } + procFreeConsole.Call() // Get module handle hInstance, _, _ := procGetModuleHandle.Call(0) - // Load default application icon - app.hIcon, _, _ = procLoadIcon.Call(0, uintptr(idiApp)) + // Load icons from embedded ICO data + app.hIconRun = loadIconFromICO(icoRunning) + app.hIconStop = loadIconFromICO(icoStopped) + // Fallback to system default if embedded icons fail + if app.hIconStop == 0 { + app.hIconStop, _, _ = procLoadIcon.Call(0, uintptr(32512)) // IDI_APPLICATION + } + if app.hIconRun == 0 { + app.hIconRun = app.hIconStop + } // Register window class className := syscall.StringToUTF16Ptr("EdgeAITrayClass") @@ -155,8 +188,8 @@ func RunWebGUI(cfg *Config) { } procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wcx))) - // Create message-only window - hwndMessageOnly := uintptr(0xFFFFFFFFFFFFFFFD) // HWND_MESSAGE = -3 + // Create message-only window (HWND_MESSAGE = -3) + hwndMessageOnly := uintptr(0xFFFFFFFFFFFFFFFD) app.hwnd, _, _ = procCreateWindowEx.Call( 0, uintptr(unsafe.Pointer(className)), @@ -165,21 +198,21 @@ func RunWebGUI(cfg *Config) { hwndMessageOnly, 0, hInstance, 0, ) - // Set up notification icon + // Set up notification icon (start with stopped icon) app.nid = notifyIconData{ CbSize: uint32(unsafe.Sizeof(notifyIconData{})), HWnd: app.hwnd, UID: 1, UFlags: nifMessage | nifIcon | nifTip, UCallbackMessage: wmTrayIcon, - HIcon: app.hIcon, + HIcon: app.hIconStop, } copy(app.nid.SzTip[:], syscall.StringToUTF16("Edge AI Platform - Starting...")) procShellNotifyIcon.Call(nimAdd, uintptr(unsafe.Pointer(&app.nid))) // Auto-start the server app.startServer() - app.updateTrayTip() + app.updateTray() // Handle OS signals quit := make(chan os.Signal, 1) @@ -233,7 +266,7 @@ func wndProc(hwnd uintptr, umsg uint32, wParam, lParam uintptr) uintptr { } else { app.startServer() } - app.updateTrayTip() + app.updateTray() case idmDashboard: app.openDashboard() case idmQuit: @@ -274,10 +307,8 @@ func (a *nativeTrayApp) showMenu() { procAppendMenu.Call(hMenu, uintptr(mfString|mfGrayed), 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(statusText)))) - // Separator procAppendMenu.Call(hMenu, uintptr(mfSeparator), 0, 0) - // Toggle button var toggleText string if running { toggleText = "Stop Server" @@ -287,7 +318,6 @@ func (a *nativeTrayApp) showMenu() { procAppendMenu.Call(hMenu, uintptr(mfString|mfEnabled), uintptr(idmToggle), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(toggleText)))) - // Open Dashboard (grayed if not running) dashFlags := mfString | mfEnabled if !running { dashFlags = mfString | mfGrayed @@ -295,34 +325,31 @@ func (a *nativeTrayApp) showMenu() { procAppendMenu.Call(hMenu, uintptr(dashFlags), uintptr(idmDashboard), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Open Dashboard")))) - // Separator + Quit procAppendMenu.Call(hMenu, uintptr(mfSeparator), 0, 0) procAppendMenu.Call(hMenu, uintptr(mfString|mfEnabled), uintptr(idmQuit), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Quit")))) - // Get cursor position and show menu var pt point procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt))) procSetForegroundWindow.Call(a.hwnd) procTrackPopupMenu.Call(hMenu, 0, uintptr(pt.X), uintptr(pt.Y), 0, a.hwnd, 0) procDestroyMenu.Call(hMenu) - - // Post empty message to dismiss menu properly procPostMessage.Call(a.hwnd, 0, 0, 0) } -func (a *nativeTrayApp) updateTrayTip() { +func (a *nativeTrayApp) updateTray() { a.mu.Lock() running := a.running a.mu.Unlock() - var tip string if running { - tip = fmt.Sprintf("Edge AI Platform - Running (:%d)", a.cfg.Server.Port) + a.nid.HIcon = a.hIconRun + tip := fmt.Sprintf("Edge AI Platform - Running (:%d)", a.cfg.Server.Port) + copy(a.nid.SzTip[:], syscall.StringToUTF16(tip)) } else { - tip = "Edge AI Platform - Stopped" + a.nid.HIcon = a.hIconStop + copy(a.nid.SzTip[:], syscall.StringToUTF16("Edge AI Platform - Stopped")) } - copy(a.nid.SzTip[:], syscall.StringToUTF16(tip)) procShellNotifyIcon.Call(nimModify, uintptr(unsafe.Pointer(&a.nid))) } @@ -352,8 +379,10 @@ func (a *nativeTrayApp) startServer() { } a.cmd = exec.Command(exe, args...) - a.cmd.Stdout = os.Stdout - a.cmd.Stderr = os.Stderr + // Hide the child process console window + a.cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: createNoWindow, + } if err := a.cmd.Start(); err != nil { log.Printf("[gui] failed to start server: %v", err) @@ -368,8 +397,7 @@ func (a *nativeTrayApp) startServer() { a.mu.Lock() a.running = false a.mu.Unlock() - // Update tray tooltip from the message loop thread - a.updateTrayTip() + a.updateTray() if err != nil { log.Printf("[gui] server process exited: %v", err) } else { @@ -408,7 +436,9 @@ func (a *nativeTrayApp) openDashboard() { url = fmt.Sprintf("http://%s:%d", host, a.cfg.Server.Port) } - exec.Command("cmd", "/c", "start", url).Start() + cmd := exec.Command("cmd", "/c", "start", url) + cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow} + cmd.Start() } func (a *nativeTrayApp) cleanup() { @@ -416,7 +446,6 @@ func (a *nativeTrayApp) cleanup() { procShellNotifyIcon.Call(nimDelete, uintptr(unsafe.Pointer(&a.nid))) } -// replacePrefix replaces old prefix with new if present. func replacePrefix(s, old, new string) string { if len(s) >= len(old) && s[:len(old)] == old { return new + s[len(old):] @@ -424,7 +453,6 @@ func replacePrefix(s, old, new string) string { return s } -// indexOf returns the index of substr in s, or -1 if not found. func indexOf(s, substr string) int { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr {