generated from thinkode/modelRepository
11-hardware-definition (#17)
- Detection of FTDI and MIDI peripherals - Creation od OS2L peripherals - Optimization Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
196
hardware/FTDIPeripheral.go
Normal file
196
hardware/FTDIPeripheral.go
Normal file
@@ -0,0 +1,196 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
return &FTDIPeripheral{
|
||||
name: name,
|
||||
dmxSender: nil,
|
||||
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",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user