package hardware import ( "bufio" "context" _ "embed" "fmt" "os" "os/exec" "path/filepath" goRuntime "runtime" "strconv" "strings" "sync" "time" "github.com/rs/zerolog/log" "github.com/wailsapp/wails/v2/pkg/runtime" ) const ( ftdiFinderExecutableName = "FTDI_finder.exe" ftdiSenderExecutableName = "FTDI_sender.exe" ) // FTDIFinder represents how the protocol is defined type FTDIFinder struct { 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 goWait sync.WaitGroup // Check goroutines execution } // 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) { ftdiPeripheral, err := NewFTDIPeripheral(peripheralData) if err != nil { return "", fmt.Errorf("unable to create the FTDI peripheral: %v", err) } f.registeredPeripherals[peripheralData.SerialNumber] = *ftdiPeripheral log.Trace().Any("periph", &ftdiPeripheral).Str("file", "FTDIFinder").Str("peripheralName", peripheralData.Name).Msg("FTDI peripheral has been created") return peripheralData.SerialNumber, nil } // UnregisterPeripheral unregisters an existing peripheral func (f *FTDIFinder) UnregisterPeripheral(peripheralID string) error { peripheral, registered := f.registeredPeripherals[peripheralID] if registered { err := peripheral.Disconnect() if err != nil { return err } } delete(f.registeredPeripherals, peripheralID) return nil } //go:embed third-party/ftdi/detectFTDI.exe var finderExe []byte //go:embed third-party/ftdi/dmxSender.exe var senderExe []byte // 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) } // Create the FTDI executables err := createExecutable(ftdiFinderExecutableName, finderExe) if err != nil { return err } err = createExecutable(ftdiSenderExecutableName, senderExe) if err != nil { return err } log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder initialized") return nil } // createExecutable creates and writes an executable to the temporary directory of the system func createExecutable(fileName string, storedFile []byte) error { tempFile, err := os.Create(filepath.Join(os.TempDir(), fileName)) if err != nil { log.Err(err).Str("file", "FTDIFinder").Str("fileName", fileName).Msg("unable to create an FTDI executable") return err } log.Trace().Str("file", "FTDIFinder").Str("filePath", tempFile.Name()).Msg("FTDI executable created") // Write the embedded executable to the temp file if _, err := tempFile.Write(storedFile); err != nil { log.Err(err).Str("file", "FTDIFinder").Str("fileName", fileName).Msg("unable to write the content to an FTDI executable") return err } tempFile.Close() log.Trace().Str("file", "FTDIPeripheral").Str("fileName", fileName).Msg("FTDI executable written") return nil } // Start starts the finder and search for peripherals func (f *FTDIFinder) Start(ctx context.Context) error { f.goWait.Add(1) go func() { defer f.goWait.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() { f.scanChannel <- struct{}{} } // Stop stops the finder func (f *FTDIFinder) Stop() error { log.Trace().Str("file", "FTDIFinder").Msg("stopping the FTDI finder...") // Wait for goroutines to stop f.goWait.Wait() // Stop the ticker f.findTicker.Stop() // Delete the FTDI executable files fileToDelete := filepath.Join(os.TempDir(), ftdiFinderExecutableName) err := os.Remove(fileToDelete) if err != nil { log.Warn().Str("file", "FTDIFinder").Str("fileName", fileToDelete).AnErr("error", err).Msg("unable to remove the executable file") } fileToDelete = filepath.Join(os.TempDir(), ftdiSenderExecutableName) err = os.Remove(fileToDelete) if err != nil { log.Warn().Str("file", "FTDIFinder").Str("fileName", fileToDelete).AnErr("error", err).Msg("unable to remove the executable file") } log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder stopped") return nil } // 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 { detectionCtx, cancel := context.WithCancel(ctx) defer cancel() log.Trace().Str("file", "FTDIFinder").Msg("FTDI scan triggered") finder := exec.CommandContext(detectionCtx, filepath.Join(os.TempDir(), ftdiFinderExecutableName)) log.Trace().Str("file", "FTDIFinder").Msg("has executed the FIND executable") stdout, err := finder.StdoutPipe() if err != nil { return fmt.Errorf("unable to create the stdout pipe: %s", err) } defer stdout.Close() stderr, err := finder.StderrPipe() if err != nil { return fmt.Errorf("unable to create the stderr pipe: %s", err) } defer stderr.Close() err = finder.Start() if err != nil { return fmt.Errorf("unable to find FTDI peripherals: %s", err) } scannerErr := bufio.NewScanner(stderr) for scannerErr.Scan() { return fmt.Errorf("unable to find FTDI peripherals: %s", scannerErr.Text()) } temporaryPeripherals := make(map[string]PeripheralInfo) scanner := bufio.NewScanner(stdout) for scanner.Scan() { peripheralString := scanner.Text() // The program output is like '0:1:2' where 0 is the location, 1 is the S/N and 2 is the name peripheralInfo := strings.Split(peripheralString, ":") log.Trace().Str("file", "FTDIFinder").Str("scannedString", peripheralString).Str("peripheralName", peripheralInfo[2]).Str("peripheralSN", peripheralInfo[1]).Msg("new FTDI peripheral detected") // Convert the location to an integer location, err := strconv.Atoi(peripheralInfo[0]) if err != nil { log.Warn().Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("no location provided for this FTDI peripheral") location = -1 } // Add the peripheral info to the found list temporaryPeripherals[peripheralInfo[1]] = PeripheralInfo{ Name: peripheralInfo[2], SerialNumber: peripheralInfo[1], ProtocolName: "FTDI", } // If this peripheral is already registered, connect it and activate it peripheral, registered := f.registeredPeripherals[peripheralInfo[1]] if registered { runtime.EventsEmit(ctx, string(PeripheralStatus), peripheral.info, "connecting") time.Sleep(2 * time.Second) err := peripheral.Connect(ctx, location) if err != nil { log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralInfo[1]).Msg("unable to connect the peripheral") } runtime.EventsEmit(ctx, string(PeripheralStatus), peripheral.info, "deactivated") time.Sleep(2 * time.Second) err = peripheral.Activate(ctx) if err != nil { log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralInfo[1]).Msg("unable to activate the peripheral") } runtime.EventsEmit(ctx, string(PeripheralStatus), peripheral.info, "activated") } log.Trace().Any("periph", temporaryPeripherals).Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("successfully added the FTDI peripheral to the finder") } // Emit the peripherals changes to the front emitPeripheralsChanges(ctx, f.foundPeripherals, temporaryPeripherals) // Store the new peripherals list f.foundPeripherals = temporaryPeripherals return nil }