package hardware import ( "context" "errors" "fmt" goRuntime "runtime" "sync" "time" "unsafe" "github.com/rs/zerolog/log" "github.com/wailsapp/wails/v2/pkg/runtime" ) /* #include #cgo LDFLAGS: -L${SRCDIR}/../build/bin -ldetectFTDI #include "cpp/include/detectFTDIBridge.h" */ import "C" // FTDIFinder manages all the FTDI peripherals type FTDIFinder struct { wg sync.WaitGroup mu sync.Mutex saved map[string]PeripheralInfo // Peripherals saved in the project detected map[string]*FTDIPeripheral // Detected peripherals scanEvery time.Duration // Scans peripherals periodically onArrival func(p PeripheralInfo) // When a peripheral arrives onRemoval func(p PeripheralInfo) // When a peripheral goes away } // NewFTDIFinder creates a new FTDI finder func NewFTDIFinder(scanEvery time.Duration) *FTDIFinder { log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder created") return &FTDIFinder{ scanEvery: scanEvery, saved: make(map[string]PeripheralInfo), detected: make(map[string]*FTDIPeripheral), } } // OnArrival is the callback function when a new peripheral arrives func (f *FTDIFinder) OnArrival(cb func(p PeripheralInfo)) { f.onArrival = cb } // OnRemoval i the callback when a peripheral goes away func (f *FTDIFinder) OnRemoval(cb func(p PeripheralInfo)) { f.onRemoval = cb } // RegisterPeripheral registers a new peripheral func (f *FTDIFinder) RegisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) (string, error) { f.mu.Lock() defer f.mu.Unlock() f.saved[peripheralData.SerialNumber] = peripheralData // If already detected, connect it if peripheral, ok := f.detected[peripheralData.SerialNumber]; ok { f.wg.Add(1) go func() { defer f.wg.Done() runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusConnecting) err := peripheral.Connect(ctx) if err != nil { runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to connect the peripheral") return } runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDeactivated) // Peripheral connected, activate it err = peripheral.Activate(ctx) if err != nil { log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to activate the FTDI peripheral") return } runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusActivated) }() } // Emits the event in the hardware runtime.EventsEmit(ctx, "LOAD_PERIPHERAL", peripheralData) return peripheralData.SerialNumber, nil } // UnregisterPeripheral unregisters an existing peripheral func (f *FTDIFinder) UnregisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) error { f.mu.Lock() defer f.mu.Unlock() if peripheral, detected := f.detected[peripheralData.SerialNumber]; detected { // Deactivating peripheral err := peripheral.Deactivate(ctx) if err != nil { log.Err(err).Str("sn", peripheralData.SerialNumber).Msg("unable to deactivate the peripheral") return nil } runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDeactivated) // Disconnecting peripheral err = peripheral.Disconnect() if err != nil { log.Err(err).Str("sn", peripheralData.SerialNumber).Msg("unable to disconnect the peripheral") return nil } runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) } delete(f.saved, peripheralData.SerialNumber) runtime.EventsEmit(ctx, "UNLOAD_PERIPHERAL", peripheralData) return nil } // Initialize initializes the FTDI finder func (f *FTDIFinder) Initialize() error { // Check platform if goRuntime.GOOS != "windows" { log.Error().Str("file", "FTDIFinder").Str("platform", goRuntime.GOOS).Msg("FTDI finder not compatible with your platform") return fmt.Errorf("the FTDI finder is not compatible with your platform yet (%s)", goRuntime.GOOS) } log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder initialized") return nil } // Start starts the finder and search for peripherals func (f *FTDIFinder) Start(ctx context.Context) error { f.wg.Add(1) go func() { ticker := time.NewTicker(f.scanEvery) defer ticker.Stop() defer f.wg.Done() for { select { case <-ctx.Done(): return case <-ticker.C: // Scan the peripherals err := f.scanPeripherals(ctx) if err != nil { log.Err(err).Str("file", "FTDIFinder").Msg("unable to scan FTDI peripherals") } } } }() return nil } // ForceScan explicitly asks for scanning peripherals func (f *FTDIFinder) ForceScan() { // select { // case f.scanChannel <- struct{}{}: // default: // // Ignore if the channel is full or if it is closed // } } // GetName returns the name of the driver func (f *FTDIFinder) GetName() string { return "FTDI" } // GetPeripheralSettings gets the peripheral settings func (f *FTDIFinder) GetPeripheralSettings(peripheralID string) (map[string]interface{}, error) { // Return the specified peripheral // peripheral, found := f.registeredPeripherals[peripheralID] // if !found { // log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") // return nil, fmt.Errorf("unable to found the peripheral") // } // log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") // return peripheral.GetSettings(), nil return make(map[string]interface{}), nil } // SetPeripheralSettings sets the peripheral settings func (f *FTDIFinder) SetPeripheralSettings(ctx context.Context, peripheralID string, settings map[string]any) error { // Return the specified peripheral // peripheral, found := f.registeredPeripherals[peripheralID] // if !found { // log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") // return fmt.Errorf("unable to found the peripheral") // } // log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") // return peripheral.SetSettings(settings) return nil } // scanPeripherals scans the FTDI peripherals func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { log.Trace().Str("file", "FTDIFinder").Msg("FTDI scan triggered") count := int(C.get_peripherals_number()) log.Info().Int("number", count).Msg("number of FTDI devices connected") // Allocating C array size := C.size_t(count) * C.size_t(unsafe.Sizeof(C.FTDIPeripheralC{})) devicesPtr := C.malloc(size) defer C.free(devicesPtr) devices := (*[1 << 20]C.FTDIPeripheralC)(devicesPtr)[:count:count] C.get_ftdi_devices((*C.FTDIPeripheralC)(devicesPtr), C.int(count)) currentMap := make(map[string]PeripheralInfo) for i := 0; i < count; i++ { d := devices[i] sn := C.GoString(d.serialNumber) desc := C.GoString(d.description) // isOpen := d.isOpen != 0 currentMap[sn] = PeripheralInfo{ SerialNumber: sn, Name: desc, // IsOpen: isOpen, ProtocolName: "FTDI", } // Free C memory C.free_ftdi_device(&d) } log.Info().Any("peripherals", currentMap).Msg("available FTDI peripherals") // Detect arrivals for sn, peripheralData := range currentMap { if _, known := f.detected[sn]; !known { peripheral := NewFTDIPeripheral(peripheralData) f.detected[sn] = peripheral if f.onArrival != nil { f.onArrival(peripheralData) } log.Info().Str("sn", sn).Str("name", peripheralData.Name).Msg("[FTDI] New peripheral detected") // Disconnected by default runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) // If the peripheral is saved in the project => connect if _, saved := f.saved[sn]; saved { f.wg.Add(1) go func(p PeripheralInfo) { defer f.wg.Done() runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p, PeripheralStatusConnecting) err := peripheral.Connect(ctx) if err != nil { runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p, PeripheralStatusDisconnected) log.Err(err).Str("sn", p.SerialNumber).Msg("unable to connect the FTDI peripheral") return } runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p, PeripheralStatusDeactivated) err = peripheral.Activate(ctx) if err != nil { log.Err(err).Str("sn", p.SerialNumber).Msg("unable to activate the FTDI peripheral") return } runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p, PeripheralStatusActivated) }(peripheralData) } } } // Detect removals for sn, oldPeripheral := range f.detected { if _, still := currentMap[sn]; !still { // Properly clean the DMX device err := oldPeripheral.Deactivate(ctx) if err != nil { log.Err(err).Str("sn", sn).Msg("unable to deactivate the FTDI peripheral after disconnection") } runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), oldPeripheral.GetInfo(), PeripheralStatusDeactivated) err = oldPeripheral.Disconnect() if err != nil { log.Err(err).Str("sn", sn).Msg("unable to disconnect the FTDI peripheral after disconnection") } runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), oldPeripheral.GetInfo(), PeripheralStatusDisconnected) // Delete it from the detected list delete(f.detected, sn) log.Info().Str("sn", sn).Str("name", oldPeripheral.GetInfo().Name).Msg("[FTDI] peripheral removed") // Execute the removal callback if f.onRemoval != nil { f.onRemoval(oldPeripheral.GetInfo()) } } } return nil } // WaitStop stops the finder func (f *FTDIFinder) WaitStop() error { log.Trace().Str("file", "FTDIFinder").Msg("stopping the FTDI finder...") // Close the channel // close(f.scanChannel) // Wait for all the peripherals to close log.Trace().Str("file", "FTDIFinder").Msg("closing all FTDI peripherals") var errs []error for registeredPeripheralSN, registeredPeripheral := range f.detected { err := registeredPeripheral.WaitStop() if err != nil { errs = append(errs, fmt.Errorf("%s: %w", registeredPeripheralSN, err)) } } // Wait for goroutines to stop f.wg.Wait() // Returning errors if len(errs) > 0 { return errors.Join(errs...) } log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder stopped") return nil }