package hardware import ( "bufio" _ "embed" "fmt" "io" "github.com/rs/zerolog/log" "os" "os/exec" "sync" ) const ( activateCommandString = 0x01 deactivateCommandString = 0x02 setCommandString = 0x03 ) //go:embed third-party/ftdi/dmxSender.exe var dmxSender []byte // 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 wg sync.WaitGroup // Tasks management } // 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.CreateTemp("", "dmxSender*.exe") 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, 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() error { // Connect if no connection is already running log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("connecting FTDI peripheral...") if p.dmxSender == nil { log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("no instance of dmxSender for this FTDI") // Executing the command p.dmxSender = exec.Command(p.programName, fmt.Sprintf("%d", p.location)) log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("no instance of dmxSender for this FTDI") var err error p.stdout, err = p.dmxSender.StdoutPipe() if err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create the stdout pipe") return fmt.Errorf("unable to create the stdout pipe: %v", err) } p.stdin, err = p.dmxSender.StdinPipe() if err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create the stdin pipe") return fmt.Errorf("unable to create the stdin pipe: %v", err) } p.stderr, err = p.dmxSender.StderrPipe() if err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create the stderr pipe") return fmt.Errorf("unable to create the stderr pipe: %v", err) } go func() { scanner := bufio.NewScanner(p.stderr) for scanner.Scan() { // Traitez chaque ligne lue depuis 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") } }() p.wg.Add(1) go func() { defer p.wg.Done() err = p.dmxSender.Run() if err != nil { log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while execution of dmw 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") } }() } return nil } // Disconnect disconnects the FTDI peripheral func (p *FTDIPeripheral) Disconnect() 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).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() 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() 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(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", } }