package hardware import ( "context" _ "embed" "errors" "fmt" goRuntime "runtime" "sync" "time" "unsafe" "github.com/rs/zerolog/log" ) /* #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 findTicker *time.Ticker // Peripherals find ticker foundPeripherals map[string]PeripheralInfo // The list of peripherals handled by this finder registeredPeripherals map[string]*FTDIPeripheral // The list of found peripherals scanChannel chan struct{} // The channel to trigger a scan event } // NewFTDIFinder creates a new FTDI finder func NewFTDIFinder(findPeriod time.Duration) *FTDIFinder { log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder created") return &FTDIFinder{ findTicker: time.NewTicker(findPeriod), foundPeripherals: make(map[string]PeripheralInfo), registeredPeripherals: make(map[string]*FTDIPeripheral), scanChannel: make(chan struct{}), } } // RegisterPeripheral registers a new peripheral func (f *FTDIFinder) RegisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) (string, error) { // Create a new FTDI peripheral ftdiPeripheral, err := NewFTDIPeripheral(peripheralData) if err != nil { return "", fmt.Errorf("unable to create the FTDI peripheral: %v", err) } // Register it in the finder f.registeredPeripherals[peripheralData.SerialNumber] = ftdiPeripheral log.Trace().Any("periph", &ftdiPeripheral).Str("file", "FTDIFinder").Str("peripheralName", peripheralData.Name).Msg("FTDI peripheral has been created") // Peripheral created, connect it err = ftdiPeripheral.Connect(ctx) if err != nil { log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to connect the peripheral") } // Peripheral connected, activate it err = ftdiPeripheral.Activate(ctx) if err != nil { log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to activate the peripheral") } // Peripheral activated return peripheralData.SerialNumber, nil } // UnregisterPeripheral unregisters an existing peripheral func (f *FTDIFinder) UnregisterPeripheral(ctx context.Context, peripheralID string) error { peripheral, registered := f.registeredPeripherals[peripheralID] if registered { // Deactivating peripheral err := peripheral.Deactivate(ctx) if err != nil { return err } // Disconnecting peripheral err = peripheral.Disconnect() if err != nil { return err } } delete(f.registeredPeripherals, peripheralID) 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() { defer f.wg.Done() for { select { case <-ctx.Done(): return case <-f.findTicker.C: // Scan the peripherals err := f.scanPeripherals(ctx) if err != nil { log.Err(err).Str("file", "FTDIFinder").Msg("unable to scan FTDI peripherals") } case <-f.scanChannel: // 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 explicily 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 } // SetPeripheralSettings sets the peripheral settings func (f *FTDIFinder) SetPeripheralSettings(peripheralID string, settings 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 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) } // 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)) temporaryPeripherals := 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 temporaryPeripherals[sn] = PeripheralInfo{ SerialNumber: sn, Name: desc, IsOpen: isOpen, ProtocolName: "FTDI", } // Free C memory C.free_ftdi_device(&d) } log.Info().Any("peripherals", temporaryPeripherals).Msg("available FTDI peripherals") // Emit the peripherals changes to the front emitPeripheralsChanges(ctx, f.foundPeripherals, temporaryPeripherals) // Store the new peripherals list f.foundPeripherals = temporaryPeripherals return nil } // WaitStop stops the finder func (f *FTDIFinder) WaitStop() error { log.Trace().Str("file", "FTDIFinder").Msg("stopping the FTDI finder...") // Stop the ticker f.findTicker.Stop() // 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.registeredPeripherals { 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 }