package hardware import ( "context" "errors" "fmt" "sync" "time" "github.com/rs/zerolog/log" "github.com/wailsapp/wails/v2/pkg/runtime" ) // PeripheralEvent is trigger by the finders when the scan is complete type PeripheralEvent string // PeripheralStatus is the peripheral status (DISCONNECTED => CONNECTING => DEACTIVATED => ACTIVATED) type PeripheralStatus 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" // PeripheralLoad is triggered when a peripheral is added to the project PeripheralLoad PeripheralEvent = "PERIPHERAL_LOAD" // PeripheralUnload is triggered when a peripheral is removed from the project PeripheralUnload PeripheralEvent = "PERIPHERAL_UNLOAD" // PeripheralStatusUpdated is triggered when a peripheral status has been updated (disconnected - connecting - deactivated - activated) PeripheralStatusUpdated PeripheralEvent = "PERIPHERAL_STATUS" // PeripheralEventEmitted is triggered when a peripheral event is emitted PeripheralEventEmitted PeripheralEvent = "PERIPHERAL_EVENT_EMITTED" // PeripheralStatusDisconnected : peripheral is now disconnected PeripheralStatusDisconnected PeripheralStatus = "PERIPHERAL_DISCONNECTED" // PeripheralStatusConnecting : peripheral is now connecting PeripheralStatusConnecting PeripheralStatus = "PERIPHERAL_CONNECTING" // PeripheralStatusDeactivated : peripheral is now deactivated PeripheralStatusDeactivated PeripheralStatus = "PERIPHERAL_DEACTIVATED" // PeripheralStatusActivated : peripheral is now activated PeripheralStatusActivated PeripheralStatus = "PERIPHERAL_ACTIVATED" ) // Manager is the class who manages the hardware type Manager struct { mu sync.Mutex wg sync.WaitGroup finders map[string]PeripheralFinder // The map of peripherals finders DetectedPeripherals map[string]Peripheral // The current list of peripherals SavedPeripherals map[string]PeripheralInfo // The list of stored peripherals } // NewManager creates a new hardware manager func NewManager() *Manager { log.Trace().Str("package", "hardware").Msg("Hardware instance created") return &Manager{ finders: make(map[string]PeripheralFinder), DetectedPeripherals: make(map[string]Peripheral, 0), SavedPeripherals: make(map[string]PeripheralInfo, 0), } } // RegisterPeripheral registers a new peripheral func (h *Manager) RegisterPeripheral(ctx context.Context, peripheralInfo PeripheralInfo) (string, error) { h.mu.Lock() defer h.mu.Unlock() // Create the peripheral from its finder (if needed) if finder, found := h.finders[peripheralInfo.ProtocolName]; found { var err error peripheralInfo, err = finder.Create(ctx, peripheralInfo) if err != nil { return "", err } } // Do not save if the peripheral doesn't have a S/N if peripheralInfo.SerialNumber == "" { return "", fmt.Errorf("serial number is empty for this peripheral") } h.SavedPeripherals[peripheralInfo.SerialNumber] = peripheralInfo runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralInfo, PeripheralStatusDisconnected) // If already detected, connect it if peripheral, ok := h.DetectedPeripherals[peripheralInfo.SerialNumber]; ok { h.wg.Add(1) go func() { defer h.wg.Done() err := peripheral.Connect(ctx) if err != nil { log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralInfo.SerialNumber).Msg("unable to connect the peripheral") return } // Peripheral connected, activate it err = peripheral.Activate(ctx) if err != nil { log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralInfo.SerialNumber).Msg("unable to activate the FTDI peripheral") return } }() } // Emits the event in the hardware runtime.EventsEmit(ctx, string(PeripheralLoad), peripheralInfo) return peripheralInfo.SerialNumber, nil } // UnregisterPeripheral unregisters an existing peripheral func (h *Manager) UnregisterPeripheral(ctx context.Context, peripheralInfo PeripheralInfo) error { h.mu.Lock() defer h.mu.Unlock() if peripheral, detected := h.DetectedPeripherals[peripheralInfo.SerialNumber]; detected { // Deactivating peripheral err := peripheral.Deactivate(ctx) if err != nil { log.Err(err).Str("sn", peripheralInfo.SerialNumber).Msg("unable to deactivate the peripheral") return nil } // Disconnecting peripheral err = peripheral.Disconnect(ctx) if err != nil { log.Err(err).Str("sn", peripheralInfo.SerialNumber).Msg("unable to disconnect the peripheral") return nil } // Remove the peripheral from its finder (if needed) if finder, found := h.finders[peripheralInfo.ProtocolName]; found { err = finder.Remove(ctx, peripheral) if err != nil { return err } } } delete(h.SavedPeripherals, peripheralInfo.SerialNumber) runtime.EventsEmit(ctx, string(PeripheralUnload), peripheralInfo) return nil } // GetPeripheralSettings gets the peripheral settings func (h *Manager) GetPeripheralSettings(peripheralSN string) (map[string]any, error) { // Return the specified peripheral peripheral, found := h.DetectedPeripherals[peripheralSN] if !found { // Peripheral not detected, return the last settings saved if savedPeripheral, isFound := h.SavedPeripherals[peripheralSN]; isFound { return savedPeripheral.Settings, nil } return nil, fmt.Errorf("unable to found the peripheral") } return peripheral.GetSettings(), nil } // SetPeripheralSettings sets the peripheral settings func (h *Manager) SetPeripheralSettings(ctx context.Context, peripheralSN string, settings map[string]any) error { peripheral, found := h.DetectedPeripherals[peripheralSN] if !found { return fmt.Errorf("unable to found the FTDI peripheral") } return peripheral.SetSettings(ctx, settings) } // Start starts to find new peripheral events func (h *Manager) Start(ctx context.Context) error { // Register all the finders to use as hardware scanners h.RegisterFinder(NewFTDIFinder(3 * time.Second)) h.RegisterFinder(NewOS2LFinder()) h.RegisterFinder(NewMIDIFinder(3 * time.Second)) for finderName, finder := range h.finders { // Initialize the finder err := finder.Initialize() if err != nil { log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to initialize finder") return err } // Set callback functions finder.OnArrival(h.OnPeripheralArrival) finder.OnRemoval(h.OnPeripheralRemoval) // Start the finder err = finder.Start(ctx) if err != nil { log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to start finder") return err } } return nil } // OnPeripheralArrival is called when a peripheral arrives in the system func (h *Manager) OnPeripheralArrival(ctx context.Context, peripheral Peripheral) { // Add the peripheral to the detected hardware h.DetectedPeripherals[peripheral.GetInfo().SerialNumber] = peripheral // If the peripheral is saved in the project, connect it if _, saved := h.SavedPeripherals[peripheral.GetInfo().SerialNumber]; saved { h.wg.Add(1) go func(p Peripheral) { defer h.wg.Done() err := p.Connect(ctx) if err != nil { log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to connect the FTDI peripheral") return } err = p.Activate(ctx) if err != nil { log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to activate the FTDI peripheral") return } }(peripheral) } // TODO: Update the Peripheral reference in the corresponding devices runtime.EventsEmit(ctx, string(PeripheralArrival), peripheral.GetInfo()) } // OnPeripheralRemoval is called when a peripheral exits the system func (h *Manager) OnPeripheralRemoval(ctx context.Context, peripheral Peripheral) { // Properly deactivating and disconnecting the peripheral h.wg.Add(1) go func(p Peripheral) { defer h.wg.Done() err := p.Deactivate(ctx) if err != nil { log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to deactivate peripheral after disconnection") } err = p.Disconnect(ctx) if err != nil { log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to disconnect the peripheral after disconnection") } }(peripheral) // Remove the peripheral from the hardware delete(h.DetectedPeripherals, peripheral.GetInfo().SerialNumber) // TODO: Update the Peripheral reference in the corresponding devices runtime.EventsEmit(ctx, string(PeripheralRemoval), peripheral.GetInfo()) } // GetFinder returns a register finder func (h *Manager) GetFinder(finderName string) (PeripheralFinder, error) { finder, exists := h.finders[finderName] if !exists { log.Error().Str("file", "hardware").Str("finderName", finderName).Msg("unable to get the finder") return nil, fmt.Errorf("unable to locate the '%s' finder", finderName) } log.Debug().Str("file", "hardware").Str("finderName", finderName).Msg("got finder") return finder, nil } // RegisterFinder registers a new peripherals finder func (h *Manager) RegisterFinder(finder PeripheralFinder) { h.finders[finder.GetName()] = finder log.Info().Str("file", "hardware").Str("finderName", finder.GetName()).Msg("finder registered") } // WaitStop stops the hardware manager func (h *Manager) WaitStop() error { log.Trace().Str("file", "hardware").Msg("closing the hardware manager") // Stop each finder var errs []error for name, f := range h.finders { if err := f.WaitStop(); err != nil { errs = append(errs, fmt.Errorf("%s: %w", name, err)) } } // Wait for all the peripherals to close log.Trace().Str("file", "MIDIFinder").Msg("closing all MIDI peripherals") for registeredPeripheralSN, registeredPeripheral := range h.DetectedPeripherals { err := registeredPeripheral.WaitStop() if err != nil { errs = append(errs, fmt.Errorf("%s: %w", registeredPeripheralSN, err)) } } // Wait for goroutines to finish h.wg.Wait() // Returning errors if len(errs) > 0 { return errors.Join(errs...) } log.Info().Str("file", "hardware").Msg("hardware manager stopped") return nil }