package hardware import ( "context" "errors" "fmt" "sync" "github.com/rs/zerolog/log" "github.com/wailsapp/wails/v2/pkg/runtime" ) // EndpointEvent is trigger by the providers when the scan is complete type EndpointEvent string // EndpointStatus is the endpoint status (DISCONNECTED => CONNECTING => DEACTIVATED => ACTIVATED) type EndpointStatus string const ( // EndpointArrival is triggerd when a endpoint has been connected to the system EndpointArrival EndpointEvent = "PERIPHERAL_ARRIVAL" // EndpointRemoval is triggered when a endpoint has been disconnected from the system EndpointRemoval EndpointEvent = "PERIPHERAL_REMOVAL" // EndpointLoad is triggered when a endpoint is added to the project EndpointLoad EndpointEvent = "PERIPHERAL_LOAD" // EndpointUnload is triggered when a endpoint is removed from the project EndpointUnload EndpointEvent = "PERIPHERAL_UNLOAD" // EndpointStatusUpdated is triggered when a endpoint status has been updated (disconnected - connecting - deactivated - activated) EndpointStatusUpdated EndpointEvent = "PERIPHERAL_STATUS" // EndpointEventEmitted is triggered when a endpoint event is emitted EndpointEventEmitted EndpointEvent = "PERIPHERAL_EVENT_EMITTED" // EndpointStatusDisconnected : endpoint is now disconnected EndpointStatusDisconnected EndpointStatus = "PERIPHERAL_DISCONNECTED" // EndpointStatusConnecting : endpoint is now connecting EndpointStatusConnecting EndpointStatus = "PERIPHERAL_CONNECTING" // EndpointStatusDeactivated : endpoint is now deactivated EndpointStatusDeactivated EndpointStatus = "PERIPHERAL_DEACTIVATED" // EndpointStatusActivated : endpoint is now activated EndpointStatusActivated EndpointStatus = "PERIPHERAL_ACTIVATED" ) // MappingInfo is the configuration for each device type MappingInfo struct { DeviceInfo struct { Name string `yaml:"name"` Manufacturer string `yaml:"manufacturer"` Type string `yaml:"type"` } `yaml:"device"` Features map[string]any `yaml:"features"` } // Device represents the methods used to manage a device (logic element include in a Endpoint) type Device interface { Configure() error // Load the mapping for the device } // Endpoint represents the methods used to manage a endpoint (input or output hardware) type Endpoint interface { Connect(context.Context) error // Connect the endpoint // SetEventCallback(func(any)) // Callback is called when an event is emitted from the endpoint Disconnect(context.Context) error // Disconnect the endpoint Activate(context.Context) error // Activate the endpoint Deactivate(context.Context) error // Deactivate the endpoint // AddDevice(Device) error // Add a device to the endpoint // RemoveDevice(Device) error // Remove a device to the endpoint GetSettings() map[string]any // Get the endpoint settings SetSettings(context.Context, map[string]any) error // Set a endpoint setting SetDeviceProperty(context.Context, uint32, byte) error // Update a device property WaitStop() error // Properly close the endpoint GetInfo() EndpointInfo // Get the endpoint information } // EndpointInfo represents a endpoint information type EndpointInfo struct { Name string `yaml:"name"` // Name of the endpoint SerialNumber string `yaml:"sn"` // S/N of the endpoint ProtocolName string `yaml:"protocol"` // Protocol name of the endpoint Settings map[string]any `yaml:"settings"` // Endpoint settings } // EndpointProvider represents how compatible endpoint drivers are implemented type EndpointProvider interface { Initialize() error // Initializes the protocol Create(ctx context.Context, endpointInfo EndpointInfo) (EndpointInfo, error) // Manually create a endpoint Remove(ctx context.Context, endpoint Endpoint) error // Manually remove a endpoint OnArrival(cb func(context.Context, Endpoint)) // Callback function when a endpoint arrives OnRemoval(cb func(context.Context, Endpoint)) // Callback function when a endpoint goes away Start(context.Context) error // Start the detection WaitStop() error // Waiting for provider to close GetName() string // Get the name of the provider } // Manager is the class who manages the hardware type Manager struct { mu sync.Mutex wg sync.WaitGroup providers map[string]EndpointProvider // The map of endpoints providers DetectedEndpoints map[string]Endpoint // The current list of endpoints SavedEndpoints map[string]EndpointInfo // The list of stored endpoints } // NewManager creates a new hardware manager func NewManager() *Manager { log.Trace().Str("package", "hardware").Msg("Hardware instance created") return &Manager{ providers: make(map[string]EndpointProvider), DetectedEndpoints: make(map[string]Endpoint, 0), SavedEndpoints: make(map[string]EndpointInfo, 0), } } // RegisterEndpoint registers a new endpoint func (h *Manager) RegisterEndpoint(ctx context.Context, endpointInfo EndpointInfo) (string, error) { h.mu.Lock() defer h.mu.Unlock() // Create the endpoint from its provider (if needed) if provider, found := h.providers[endpointInfo.ProtocolName]; found { var err error endpointInfo, err = provider.Create(ctx, endpointInfo) if err != nil { return "", err } } // Do not save if the endpoint doesn't have a S/N if endpointInfo.SerialNumber == "" { return "", fmt.Errorf("serial number is empty for this endpoint") } h.SavedEndpoints[endpointInfo.SerialNumber] = endpointInfo runtime.EventsEmit(ctx, string(EndpointStatusUpdated), endpointInfo, EndpointStatusDisconnected) // If already detected, connect it if endpoint, ok := h.DetectedEndpoints[endpointInfo.SerialNumber]; ok { h.wg.Add(1) go func() { defer h.wg.Done() err := endpoint.Connect(ctx) if err != nil { log.Err(err).Str("file", "FTDIProvider").Str("endpointSN", endpointInfo.SerialNumber).Msg("unable to connect the endpoint") return } // Endpoint connected, activate it err = endpoint.Activate(ctx) if err != nil { log.Err(err).Str("file", "FTDIProvider").Str("endpointSN", endpointInfo.SerialNumber).Msg("unable to activate the FTDI endpoint") return } }() } // Emits the event in the hardware runtime.EventsEmit(ctx, string(EndpointLoad), endpointInfo) return endpointInfo.SerialNumber, nil } // UnregisterEndpoint unregisters an existing endpoint func (h *Manager) UnregisterEndpoint(ctx context.Context, endpointInfo EndpointInfo) error { h.mu.Lock() defer h.mu.Unlock() if endpoint, detected := h.DetectedEndpoints[endpointInfo.SerialNumber]; detected { // Deactivating endpoint err := endpoint.Deactivate(ctx) if err != nil { log.Err(err).Str("sn", endpointInfo.SerialNumber).Msg("unable to deactivate the endpoint") return nil } // Disconnecting endpoint err = endpoint.Disconnect(ctx) if err != nil { log.Err(err).Str("sn", endpointInfo.SerialNumber).Msg("unable to disconnect the endpoint") return nil } // Remove the endpoint from its provider (if needed) if provider, found := h.providers[endpointInfo.ProtocolName]; found { err = provider.Remove(ctx, endpoint) if err != nil { return err } } } delete(h.SavedEndpoints, endpointInfo.SerialNumber) runtime.EventsEmit(ctx, string(EndpointUnload), endpointInfo) return nil } // GetEndpointSettings gets the endpoint settings func (h *Manager) GetEndpointSettings(endpointSN string) (map[string]any, error) { // Return the specified endpoint endpoint, found := h.DetectedEndpoints[endpointSN] if !found { // Endpoint not detected, return the last settings saved if savedEndpoint, isFound := h.SavedEndpoints[endpointSN]; isFound { return savedEndpoint.Settings, nil } return nil, fmt.Errorf("unable to found the endpoint") } return endpoint.GetSettings(), nil } // SetEndpointSettings sets the endpoint settings func (h *Manager) SetEndpointSettings(ctx context.Context, endpointSN string, settings map[string]any) error { endpoint, found := h.DetectedEndpoints[endpointSN] if !found { return fmt.Errorf("unable to found the FTDI endpoint") } return endpoint.SetSettings(ctx, settings) } // Start starts to find new endpoint events func (h *Manager) Start(ctx context.Context) error { for providerName, provider := range h.providers { // Initialize the provider err := provider.Initialize() if err != nil { log.Err(err).Str("file", "hardware").Str("providerName", providerName).Msg("unable to initialize provider") return err } // Set callback functions provider.OnArrival(h.OnEndpointArrival) provider.OnRemoval(h.OnEndpointRemoval) // Start the provider err = provider.Start(ctx) if err != nil { log.Err(err).Str("file", "hardware").Str("providerName", providerName).Msg("unable to start provider") return err } } return nil } // OnEndpointArrival is called when a endpoint arrives in the system func (h *Manager) OnEndpointArrival(ctx context.Context, endpoint Endpoint) { // Add the endpoint to the detected hardware h.DetectedEndpoints[endpoint.GetInfo().SerialNumber] = endpoint // If the endpoint is saved in the project, connect it if _, saved := h.SavedEndpoints[endpoint.GetInfo().SerialNumber]; saved { h.wg.Add(1) go func(p Endpoint) { 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 endpoint") return } err = p.Activate(ctx) if err != nil { log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to activate the FTDI endpoint") return } }(endpoint) } // TODO: Update the Endpoint reference in the corresponding devices runtime.EventsEmit(ctx, string(EndpointArrival), endpoint.GetInfo()) } // OnEndpointRemoval is called when a endpoint exits the system func (h *Manager) OnEndpointRemoval(ctx context.Context, endpoint Endpoint) { // Properly deactivating and disconnecting the endpoint h.wg.Add(1) go func(p Endpoint) { defer h.wg.Done() err := p.Deactivate(ctx) if err != nil { log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to deactivate endpoint after disconnection") } err = p.Disconnect(ctx) if err != nil { log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to disconnect the endpoint after disconnection") } }(endpoint) // Remove the endpoint from the hardware delete(h.DetectedEndpoints, endpoint.GetInfo().SerialNumber) // TODO: Update the Endpoint reference in the corresponding devices runtime.EventsEmit(ctx, string(EndpointRemoval), endpoint.GetInfo()) } // GetProvider returns a register provider func (h *Manager) GetProvider(providerName string) (EndpointProvider, error) { provider, exists := h.providers[providerName] if !exists { log.Error().Str("file", "hardware").Str("providerName", providerName).Msg("unable to get the provider") return nil, fmt.Errorf("unable to locate the '%s' provider", providerName) } log.Debug().Str("file", "hardware").Str("providerName", providerName).Msg("got provider") return provider, nil } // RegisterProvider registers a new endpoints provider func (h *Manager) RegisterProvider(provider EndpointProvider) { h.providers[provider.GetName()] = provider log.Info().Str("file", "hardware").Str("providerName", provider.GetName()).Msg("provider registered") } // WaitStop stops the hardware manager func (h *Manager) WaitStop() error { log.Trace().Str("file", "hardware").Msg("closing the hardware manager") // Stop each provider var errs []error for name, f := range h.providers { if err := f.WaitStop(); err != nil { errs = append(errs, fmt.Errorf("%s: %w", name, err)) } } // Wait for all the endpoints to close log.Trace().Str("file", "MIDIProvider").Msg("closing all MIDI endpoints") for registeredEndpointSN, registeredEndpoint := range h.DetectedEndpoints { err := registeredEndpoint.WaitStop() if err != nil { errs = append(errs, fmt.Errorf("%s: %w", registeredEndpointSN, 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 }