package hardware import ( "bufio" "context" _ "embed" "fmt" "io" "github.com/rs/zerolog/log" "os" "os/exec" ) const ( activateCommandString = 0x01 deactivateCommandString = 0x02 setCommandString = 0x03 ) // FTDIPeripheral contains the data of an FTDI peripheral type FTDIPeripheral struct { name string // The name of the peripheral serialNumber string // The S/N of the FTDI peripheral location int // The location of the peripheral universesNumber int // The number of DMX universes handled by this peripheral programName string // The temp file name of the executable dmxSender *exec.Cmd // The command to pilot the DMX sender program stdin io.WriteCloser // For writing in the DMX sender stdout io.ReadCloser // For reading from the DMX sender stderr io.ReadCloser // For reading the errors disconnectChan chan struct{} // Channel to cancel the connection errorsChan chan error // Channel to get the errors } //go:embed third-party/ftdi/dmxSender.exe var dmxSender []byte // NewFTDIPeripheral creates a new FTDI peripheral func NewFTDIPeripheral(name string, serialNumber string, location int) (*FTDIPeripheral, error) { log.Info().Str("file", "FTDIPeripheral").Str("name", name).Str("s/n", serialNumber).Int("location", location).Msg("FTDI peripheral created") // Create a temporary file tempFile, err := os.Create(fmt.Sprintf("dmxSender-%s.exe", serialNumber)) if err != nil { return nil, err } log.Trace().Str("file", "FTDIPeripheral").Str("s/n", serialNumber).Msg("FTDI sender temp created") // Write the embedded executable to the temp file if _, err := tempFile.Write(dmxSender); err != nil { return nil, err } log.Trace().Str("file", "FTDIPeripheral").Str("s/n", serialNumber).Msg("FTDI sender written") tempFile.Close() return &FTDIPeripheral{ name: name, dmxSender: nil, programName: tempFile.Name(), serialNumber: serialNumber, location: location, universesNumber: 1, disconnectChan: make(chan struct{}), errorsChan: make(chan error, 1), }, nil } // Connect connects the FTDI peripheral func (p *FTDIPeripheral) Connect(ctx context.Context) error { // Connect if no connection is already running log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("connecting FTDI peripheral...") // Check if the connection has already been established if p.dmxSender != nil { log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender already initialized") return nil } // Initialize the exec.Command for running the process p.dmxSender = exec.Command(p.programName, fmt.Sprintf("%d", p.location)) log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender instance created") // Create the pipes for stdin, stdout, and stderr asynchronously without blocking var err error if p.stdout, err = p.dmxSender.StdoutPipe(); err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create stdout pipe") return fmt.Errorf("unable to create stdout pipe: %v", err) } if p.stdin, err = p.dmxSender.StdinPipe(); err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create stdin pipe") return fmt.Errorf("unable to create stdin pipe: %v", err) } if p.stderr, err = p.dmxSender.StderrPipe(); err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create stderr pipe") return fmt.Errorf("unable to create stderr pipe: %v", err) } // Launch a goroutine to read stderr asynchronously go func() { scanner := bufio.NewScanner(p.stderr) for scanner.Scan() { // Process each line read from stderr log.Err(fmt.Errorf(scanner.Text())).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error detected in dmx sender") } if err := scanner.Err(); err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error reading from stderr") } }() // Launch the command asynchronously in another goroutine go func() { // Run the command, respecting the context cancellation err := p.dmxSender.Run() select { case <-ctx.Done(): // If the context is canceled, handle it gracefully log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender was canceled by context") return default: // Handle command exit normally if err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while execution of dmx sender") if exitError, ok := err.(*exec.ExitError); ok { log.Warn().Str("file", "FTDIPeripheral").Int("exitCode", exitError.ExitCode()).Str("s/n", p.serialNumber).Msg("dmx sender exited with code") } } else { log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmx sender exited successfully") } } }() log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender process started successfully") return nil } // Disconnect disconnects the FTDI peripheral func (p *FTDIPeripheral) Disconnect(ctx context.Context) error { log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("disconnecting FTDI peripheral...") if p.dmxSender != nil { log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI") _, err := io.WriteString(p.stdin, string([]byte{0x04, 0x00, 0x00, 0x00})) if err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender") return fmt.Errorf("unable to disconnect: %v", err) } p.stdin.Close() p.stdout.Close() p.dmxSender = nil err = os.Remove(p.programName) if err != nil { log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Str("senderPath", p.programName).Msg("unable to delete the dmx sender temporary file") return fmt.Errorf("unable to delete the temporary file: %v", err) } return nil } log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while disconnecting: not connected") return fmt.Errorf("unable to disconnect: not connected") } // Activate activates the FTDI peripheral func (p *FTDIPeripheral) Activate(ctx context.Context) error { if p.dmxSender != nil { log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI") _, err := io.WriteString(p.stdin, string([]byte{0x01, 0x00, 0x00, 0x00})) if err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender") return fmt.Errorf("unable to activate: %v", err) } return nil } log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while activating: not connected") return fmt.Errorf("unable to activate: not connected") } // Deactivate deactivates the FTDI peripheral func (p *FTDIPeripheral) Deactivate(ctx context.Context) error { if p.dmxSender != nil { log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI") _, err := io.WriteString(p.stdin, string([]byte{0x02, 0x00, 0x00, 0x00})) if err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender") return fmt.Errorf("unable to deactivate: %v", err) } return nil } log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while deactivating: not connected") return fmt.Errorf("unable to deactivate: not connected") } // SetDeviceProperty sends a command to the specified device func (p *FTDIPeripheral) SetDeviceProperty(ctx context.Context, uint32, channelNumber uint32, channelValue byte) error { if p.dmxSender != nil { log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI") commandString := []byte{0x03, 0x01, 0x00, 0xff, 0x03, 0x02, 0x00, channelValue} _, err := io.WriteString(p.stdin, string(commandString)) if err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender") return fmt.Errorf("unable to set device property: %v", err) } return nil } log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while setting device property: not connected") return fmt.Errorf("unable to set device property: not connected") } // GetInfo gets all the peripheral information func (p *FTDIPeripheral) GetInfo() PeripheralInfo { return PeripheralInfo{ Name: p.name, SerialNumber: p.serialNumber, ProtocolName: "FTDI", } }