package hardware import ( "bufio" "context" _ "embed" "fmt" "os" "os/exec" "path/filepath" goRuntime "runtime" "strconv" "strings" "sync" "time" "github.com/rs/zerolog/log" ) 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 peripherals map[string]Peripheral // The list of peripherals handled by this finder 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), peripherals: make(map[string]Peripheral), scanChannel: make(chan struct{}), } } //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 } 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" } // GetPeripheral gets the peripheral that correspond to the specified ID func (f *FTDIFinder) GetPeripheral(peripheralID string) (Peripheral, bool) { // Return the specified peripheral peripheral := f.peripherals[peripheralID] if peripheral == nil { log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") return nil, false } log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") return peripheral, true } // 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") ftdiPeripherals := make(map[string]Peripheral) 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()) } 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.Debug().Str("file", "FTDIFinder").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 to the temporary list peripheral, err := NewFTDIPeripheral(peripheralInfo[2], peripheralInfo[1], location) if err != nil { return fmt.Errorf("unable to create the FTDI peripheral: %v", err) } ftdiPeripherals[peripheralInfo[1]] = peripheral log.Trace().Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("successfully added the FTDI peripheral to the finder") } // Compare with the current peripherals to detect arrivals/removals removedList, addedList := comparePeripherals(f.peripherals, ftdiPeripherals) // Emit the events emitPeripheralsEvents(ctx, removedList, PeripheralRemoval) log.Info().Str("file", "FTDIFinder").Msg("FTDI remove list emitted to the front") emitPeripheralsEvents(ctx, addedList, PeripheralArrival) log.Info().Str("file", "FTDIFinder").Msg("FTDI add list emitted to the front") // Store the new peripherals list f.peripherals = ftdiPeripherals return nil } // CreatePeripheral is not implemented here func (f *FTDIFinder) CreatePeripheral(context.Context) (Peripheral, error) { return nil, nil } // DeletePeripheral is not implemented here func (f *FTDIFinder) DeletePeripheral(serialNumber string) error { return nil }