2025-01-18 14:53:29 +00:00
package hardware
import (
"bufio"
"context"
_ "embed"
"fmt"
"io"
2025-08-31 11:15:38 +02:00
"path/filepath"
2025-01-18 14:53:29 +00:00
"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 {
2025-01-26 12:01:31 +01:00
info PeripheralInfo // The peripheral basic data
2025-01-25 17:43:45 +00:00
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
2025-01-18 14:53:29 +00:00
}
// NewFTDIPeripheral creates a new FTDI peripheral
2025-01-26 12:01:31 +01:00
func NewFTDIPeripheral ( info PeripheralInfo ) ( * FTDIPeripheral , error ) {
log . Info ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "name" , info . Name ) . Str ( "s/n" , info . SerialNumber ) . Msg ( "FTDI peripheral created" )
2025-01-25 17:43:45 +00:00
settings := make ( map [ string ] interface { } )
2025-01-18 14:53:29 +00:00
return & FTDIPeripheral {
2025-08-31 11:15:38 +02:00
programName : filepath . Join ( os . TempDir ( ) , ftdiSenderExecutableName ) ,
2025-01-26 12:01:31 +01:00
info : info ,
2025-01-25 17:43:45 +00:00
dmxSender : nil ,
settings : settings ,
disconnectChan : make ( chan struct { } ) ,
errorsChan : make ( chan error , 1 ) ,
2025-01-18 14:53:29 +00:00
} , nil
}
// Connect connects the FTDI peripheral
2025-01-26 12:01:31 +01:00
func ( p * FTDIPeripheral ) Connect ( ctx context . Context , location int ) error {
2025-01-18 14:53:29 +00:00
// Connect if no connection is already running
2025-01-26 12:01:31 +01:00
log . Trace ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "connecting FTDI peripheral..." )
2025-01-18 14:53:29 +00:00
// Check if the connection has already been established
if p . dmxSender != nil {
2025-01-26 12:01:31 +01:00
log . Warn ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "dmxSender already initialized" )
2025-01-18 14:53:29 +00:00
return nil
}
// Initialize the exec.Command for running the process
2025-01-26 12:01:31 +01:00
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" )
2025-01-18 14:53:29 +00:00
// Create the pipes for stdin, stdout, and stderr asynchronously without blocking
var err error
if p . stdout , err = p . dmxSender . StdoutPipe ( ) ; err != nil {
2025-01-26 12:01:31 +01:00
log . Err ( err ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "unable to create stdout pipe" )
2025-01-18 14:53:29 +00:00
return fmt . Errorf ( "unable to create stdout pipe: %v" , err )
}
if p . stdin , err = p . dmxSender . StdinPipe ( ) ; err != nil {
2025-01-26 12:01:31 +01:00
log . Err ( err ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "unable to create stdin pipe" )
2025-01-18 14:53:29 +00:00
return fmt . Errorf ( "unable to create stdin pipe: %v" , err )
}
if p . stderr , err = p . dmxSender . StderrPipe ( ) ; err != nil {
2025-01-26 12:01:31 +01:00
log . Err ( err ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "unable to create stderr pipe" )
2025-01-18 14:53:29 +00:00
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
2025-01-26 12:01:31 +01:00
log . Err ( fmt . Errorf ( scanner . Text ( ) ) ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "error detected in dmx sender" )
2025-01-18 14:53:29 +00:00
}
if err := scanner . Err ( ) ; err != nil {
2025-01-26 12:01:31 +01:00
log . Err ( err ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "error reading from stderr" )
2025-01-18 14:53:29 +00:00
}
} ( )
// 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
2025-08-31 11:15:38 +02:00
err = p . Disconnect ( )
if err != nil {
log . Err ( err ) . Str ( "file" , "FTDIPeripheral" ) . Msg ( "unable to disconnect the peripheral" )
}
2025-01-26 12:01:31 +01:00
log . Warn ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "dmxSender was canceled by context" )
2025-01-18 14:53:29 +00:00
return
default :
// Handle command exit normally
if err != nil {
2025-01-26 12:01:31 +01:00
log . Err ( err ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "error while execution of dmx sender" )
2025-01-18 14:53:29 +00:00
if exitError , ok := err . ( * exec . ExitError ) ; ok {
2025-01-26 12:01:31 +01:00
log . Warn ( ) . Str ( "file" , "FTDIPeripheral" ) . Int ( "exitCode" , exitError . ExitCode ( ) ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "dmx sender exited with code" )
2025-01-18 14:53:29 +00:00
}
} else {
2025-01-26 12:01:31 +01:00
log . Debug ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "dmx sender exited successfully" )
2025-01-18 14:53:29 +00:00
}
}
} ( )
2025-01-26 12:01:31 +01:00
log . Debug ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "dmxSender process started successfully" )
2025-01-18 14:53:29 +00:00
return nil
}
// Disconnect disconnects the FTDI peripheral
2025-08-31 11:15:38 +02:00
func ( p * FTDIPeripheral ) Disconnect ( ) error {
2025-01-26 12:01:31 +01:00
log . Trace ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "disconnecting FTDI peripheral..." )
2025-01-18 14:53:29 +00:00
if p . dmxSender != nil {
2025-01-26 12:01:31 +01:00
log . Debug ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "dmxsender is defined for this FTDI" )
2025-01-18 14:53:29 +00:00
_ , err := io . WriteString ( p . stdin , string ( [ ] byte { 0x04 , 0x00 , 0x00 , 0x00 } ) )
if err != nil {
2025-01-26 12:01:31 +01:00
log . Err ( err ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "unable to write command to sender" )
2025-01-18 14:53:29 +00:00
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 {
2025-01-26 12:01:31 +01:00
log . Warn ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Str ( "senderPath" , p . programName ) . Msg ( "unable to delete the dmx sender temporary file" )
2025-01-18 14:53:29 +00:00
return fmt . Errorf ( "unable to delete the temporary file: %v" , err )
}
return nil
}
2025-01-26 12:01:31 +01:00
log . Warn ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "error while disconnecting: not connected" )
2025-01-18 14:53:29 +00:00
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 {
2025-01-26 12:01:31 +01:00
log . Debug ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "dmxsender is defined for this FTDI" )
2025-01-18 14:53:29 +00:00
_ , err := io . WriteString ( p . stdin , string ( [ ] byte { 0x01 , 0x00 , 0x00 , 0x00 } ) )
if err != nil {
2025-01-26 12:01:31 +01:00
log . Err ( err ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "unable to write command to sender" )
2025-01-18 14:53:29 +00:00
return fmt . Errorf ( "unable to activate: %v" , err )
}
return nil
}
2025-01-26 12:01:31 +01:00
log . Warn ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "error while activating: not connected" )
2025-01-18 14:53:29 +00:00
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 {
2025-01-26 12:01:31 +01:00
log . Debug ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "dmxsender is defined for this FTDI" )
2025-01-18 14:53:29 +00:00
_ , err := io . WriteString ( p . stdin , string ( [ ] byte { 0x02 , 0x00 , 0x00 , 0x00 } ) )
if err != nil {
2025-01-26 12:01:31 +01:00
log . Err ( err ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "unable to write command to sender" )
2025-01-18 14:53:29 +00:00
return fmt . Errorf ( "unable to deactivate: %v" , err )
}
return nil
}
2025-01-26 12:01:31 +01:00
log . Warn ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "error while deactivating: not connected" )
2025-01-18 14:53:29 +00:00
return fmt . Errorf ( "unable to deactivate: not connected" )
}
2025-01-26 12:01:31 +01:00
// SetSettings sets a specific setting for this peripheral
func ( p * FTDIPeripheral ) SetSettings ( settings map [ string ] interface { } ) error {
2025-01-25 17:43:45 +00:00
p . settings = settings
return nil
}
2025-01-18 14:53:29 +00:00
// SetDeviceProperty sends a command to the specified device
2025-08-31 11:15:38 +02:00
func ( p * FTDIPeripheral ) SetDeviceProperty ( ctx context . Context , channelNumber uint32 , channelValue byte ) error {
2025-01-18 14:53:29 +00:00
if p . dmxSender != nil {
2025-01-26 12:01:31 +01:00
log . Debug ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "dmxsender is defined for this FTDI" )
2025-08-31 11:15:38 +02:00
commandString := [ ] byte { 0x03 , byte ( channelNumber & 0xFF ) , byte ( ( channelNumber >> 8 ) & 0xFF ) , channelValue }
2025-01-18 14:53:29 +00:00
_ , err := io . WriteString ( p . stdin , string ( commandString ) )
if err != nil {
2025-01-26 12:01:31 +01:00
log . Err ( err ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "unable to write command to sender" )
2025-01-18 14:53:29 +00:00
return fmt . Errorf ( "unable to set device property: %v" , err )
}
return nil
}
2025-01-26 12:01:31 +01:00
log . Warn ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "s/n" , p . info . SerialNumber ) . Msg ( "error while setting device property: not connected" )
2025-01-18 14:53:29 +00:00
return fmt . Errorf ( "unable to set device property: not connected" )
}
2025-01-25 17:43:45 +00:00
// GetSettings gets the peripheral settings
func ( p * FTDIPeripheral ) GetSettings ( ) map [ string ] interface { } {
return p . settings
}
2025-01-18 14:53:29 +00:00
// GetInfo gets all the peripheral information
func ( p * FTDIPeripheral ) GetInfo ( ) PeripheralInfo {
2025-01-26 12:01:31 +01:00
return p . info
2025-01-18 14:53:29 +00:00
}