Files
dmxconnect/hardware/FTDIPeripheral.go

201 lines
8.2 KiB
Go

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
}