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 { info PeripheralInfo // The peripheral basic data programName string // The temp file name of the executable settings map[string]interface{} // The settings of the peripheral 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 } // NewFTDIPeripheral creates a new FTDI peripheral func NewFTDIPeripheral(info PeripheralInfo) (*FTDIPeripheral, error) { log.Info().Str("file", "FTDIPeripheral").Str("name", info.Name).Str("s/n", info.SerialNumber).Msg("FTDI peripheral created") settings := make(map[string]interface{}) return &FTDIPeripheral{ info: info, dmxSender: nil, settings: settings, disconnectChan: make(chan struct{}), errorsChan: make(chan error, 1), }, nil } // Connect connects the FTDI peripheral func (p *FTDIPeripheral) Connect(ctx context.Context, location int) error { // Connect if no connection is already running log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.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.info.SerialNumber).Msg("dmxSender already initialized") return nil } // Initialize the exec.Command for running the process p.dmxSender = exec.Command(p.programName, fmt.Sprintf("%d", location)) log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.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.info.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.info.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.info.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.info.SerialNumber).Msg("error detected in dmx sender") } if err := scanner.Err(); err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.info.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.info.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.info.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.info.SerialNumber).Msg("dmx sender exited with code") } } else { log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("dmx sender exited successfully") } } }() log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.info.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.info.SerialNumber).Msg("disconnecting FTDI peripheral...") if p.dmxSender != nil { log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.info.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.info.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.info.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.info.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.info.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.info.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.info.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.info.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.info.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.info.SerialNumber).Msg("error while deactivating: not connected") return fmt.Errorf("unable to deactivate: not connected") } // SetSettings sets a specific setting for this peripheral func (p *FTDIPeripheral) SetSettings(settings map[string]interface{}) error { p.settings = settings return nil } // 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.info.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.info.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.info.SerialNumber).Msg("error while setting device property: not connected") return fmt.Errorf("unable to set device property: not connected") } // GetSettings gets the peripheral settings func (p *FTDIPeripheral) GetSettings() map[string]interface{} { return p.settings } // GetInfo gets all the peripheral information func (p *FTDIPeripheral) GetInfo() PeripheralInfo { return p.info }