package hardware import ( "context" "fmt" "syscall" "time" "unsafe" "github.com/rs/zerolog/log" "github.com/lxn/win" "github.com/wailsapp/wails/v2/pkg/runtime" "golang.org/x/sys/windows" ) // PeripheralEvent is trigger by the finders when the scan is complete type PeripheralEvent string const ( // PeripheralArrival is triggerd when a peripheral has been connected to the system PeripheralArrival PeripheralEvent = "PERIPHERAL_ARRIVAL" // PeripheralRemoval is triggered when a peripheral has been disconnected from the system PeripheralRemoval PeripheralEvent = "PERIPHERAL_REMOVAL" debounceDuration = 500 * time.Millisecond ) var ( debounceTimer *time.Timer ) // HardwareManager is the class who manages the hardware type HardwareManager struct { drivers map[string]PeripheralDriver // The map of peripherals finders peripherals []Peripheral // The current list of peripherals deviceChangedEvent chan struct{} // The event when the devices list changed ctx context.Context } // NewHardwareManager creates a new HardwareManager func NewHardwareManager() *HardwareManager { log.Trace().Str("package", "hardware").Msg("Hardware instance created") return &HardwareManager{ drivers: make(map[string]PeripheralDriver), peripherals: make([]Peripheral, 0), deviceChangedEvent: make(chan struct{}), } } // Start starts to find new peripheral events func (h *HardwareManager) Start(ctx context.Context) error { cb := windows.NewCallback(h.wndProc) log.Trace().Str("file", "hardware").Msg("wndProc callback set") inst := win.GetModuleHandle(nil) log.Trace().Str("file", "hardware").Msg("got windows API instance") cn, err := syscall.UTF16PtrFromString("DMXConnect peripheral watcher") if err != nil { log.Err(err).Str("file", "hardware").Msg("failed to convert window class name to UTF16") return fmt.Errorf("failed to convert window class name to UTF16: %w", err) } wc := win.WNDCLASSEX{ HInstance: inst, LpfnWndProc: cb, LpszClassName: cn, } log.Trace().Str("file", "hardware").Msg("windows API class created") wc.CbSize = uint32(unsafe.Sizeof(wc)) if win.RegisterClassEx(&wc) == 0 { log.Err(syscall.GetLastError()).Str("file", "hardware").Msg("failed to register window class") return fmt.Errorf("failed to register window class: %w", syscall.GetLastError()) } log.Trace().Str("file", "hardware").Msg("window class registered") wName, err := syscall.UTF16PtrFromString("usbevent.exe") if err != nil { log.Err(err).Str("file", "hardware").Msg("failed to convert window class name to UTF16") return fmt.Errorf("failed to convert window name to UTF16: %w", err) } wdw := win.CreateWindowEx( 0, wc.LpszClassName, wName, win.WS_MINIMIZE|win.WS_OVERLAPPEDWINDOW, win.CW_USEDEFAULT, win.CW_USEDEFAULT, 100, 100, 0, 0, wc.HInstance, nil) if wdw == 0 { log.Err(syscall.GetLastError()).Str("file", "hardware").Msg("failed to create window") return fmt.Errorf("failed to create window: %w", syscall.GetLastError()) } log.Trace().Str("file", "hardware").Msg("window created successfully") _ = win.ShowWindow(wdw, win.SW_HIDE) win.UpdateWindow(wdw) log.Trace().Str("file", "hardware").Msg("window shown and updated") // To continuously get the devices events from Windows go func() { defer log.Debug().Str("file", "hardware").Msg("peripheral watcher goroutine exited") for { select { case <-ctx.Done(): return default: var msg win.MSG got := win.GetMessage(&msg, win.HWND(windows.HWND(wdw)), 0, 0) if got == 0 { win.TranslateMessage(&msg) win.DispatchMessage(&msg) } } } }() // To handle the peripheral changed go func() { defer log.Debug().Str("file", "hardware").Msg("peripheral getter goroutine exited") for { select { case <-ctx.Done(): return case <-h.deviceChangedEvent: log.Debug().Str("file", "hardware").Msg("peripheral change event, triggering scan...") err := h.Scan(ctx) if err != nil { log.Err(err).Str("file", "hardware").Msg("unable to scan peripherals") } } } }() return nil } // GetDriver returns a register driver func (h *HardwareManager) GetDriver(driverName string) (PeripheralDriver, error) { driver, exists := h.drivers[driverName] if !exists { log.Error().Str("file", "hardware").Str("driverName", driverName).Msg("unable to get the driver") return nil, fmt.Errorf("Unable to locate the '%s' driver", driverName) } log.Debug().Str("file", "hardware").Str("driverName", driverName).Msg("got driver") return driver, nil } // RegisterDriver registers a new peripherals driver func (h *HardwareManager) RegisterDriver(driver PeripheralDriver) { h.drivers[driver.GetName()] = driver log.Info().Str("file", "hardware").Str("driverName", driver.GetName()).Msg("driver registered") } // GetPeripheral gets the peripheral object from the parent driver func (h *HardwareManager) GetPeripheral(driverName string, peripheralID string) (Peripheral, bool) { // Get the driver parentDriver, found := h.drivers[driverName] // If no driver found, return false if !found { log.Error().Str("file", "hardware").Str("driverName", driverName).Msg("unable to get the driver") return nil, false } log.Trace().Str("file", "hardware").Str("driverName", parentDriver.GetName()).Msg("driver got") // Contact the driver to get the device return parentDriver.GetPeripheral(peripheralID) } // Scan scans all the peripherals for the registered finders func (h *HardwareManager) Scan(ctx context.Context) error { if len(h.drivers) == 0 { log.Warn().Str("file", "hardware").Msg("no driver registered") return fmt.Errorf("no driver registered") } for _, driver := range h.drivers { driverCopy := driver go func() { err := driverCopy.Scan(ctx) if err != nil { log.Err(err).Str("file", "hardware").Str("driverName", driverCopy.GetName()).Msg("unable to scan peripheral") return } }() } return nil } func (h *HardwareManager) wndProc(hwnd windows.HWND, msg uint32, wParam, lParam uintptr) uintptr { switch msg { case win.WM_DEVICECHANGE: // Trigger the devices scan when the last DEVICE_CHANGE event is received if debounceTimer != nil { debounceTimer.Stop() log.Trace().Str("file", "hardware").Msg("scan debounce timer stopped") } debounceTimer = time.AfterFunc(debounceDuration, func() { log.Debug().Str("file", "hardware").Msg("peripheral changed") h.deviceChangedEvent <- struct{}{} }) } return win.DefWindowProc(win.HWND(hwnd), msg, wParam, lParam) } // peripheralsList emits a peripheral event func emitPeripheralsEvents(ctx context.Context, peripheralsList map[string]Peripheral, peripheralEvent PeripheralEvent) { for _, peripheral := range peripheralsList { runtime.EventsEmit(ctx, string(peripheralEvent), peripheral.GetInfo()) log.Trace().Str("file", "hardware").Str("event", string(peripheralEvent)).Msg("emit peripheral event") } } // comparePeripherals compares the peripherals to determine which has been inserted or removed func comparePeripherals(oldPeripherals map[string]Peripheral, newPeripherals map[string]Peripheral) (map[string]Peripheral, map[string]Peripheral) { // Duplicate the lists oldList := make(map[string]Peripheral) newList := make(map[string]Peripheral) for key, value := range oldPeripherals { oldList[key] = value } log.Trace().Str("file", "hardware").Any("oldList", oldList).Msg("peripheral oldList comparison") for key, value := range newPeripherals { newList[key] = value } log.Trace().Str("file", "hardware").Any("newList", newList).Msg("peripheral newList comparison") // Remove in these lists all the commons peripherals for key := range newList { if _, exists := oldList[key]; exists { delete(oldList, key) delete(newList, key) } } // Now the old list contains the removed peripherals, and the new list contains the added peripherals log.Trace().Str("file", "hardware").Any("oldList", oldList).Msg("peripheral oldList computed") log.Trace().Str("file", "hardware").Any("newList", newList).Msg("peripheral newList computed") return oldList, newList }