2025-01-04 00:36:29 +01:00
package hardware
import (
"bufio"
"context"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
goRuntime "runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/rs/zerolog/log"
)
const (
ftdiFinderExecutableName = "FTDI_finder.exe"
ftdiSenderExecutableName = "FTDI_sender.exe"
)
// FTDIFinder represents how the protocol is defined
type FTDIFinder struct {
findTicker time . Ticker // Peripherals find ticker
peripherals map [ string ] Peripheral // The list of peripherals handled by this finder
scanChannel chan struct { } // The channel to trigger a scan event
goWait sync . WaitGroup // Check goroutines execution
}
// NewFTDIFinder creates a new FTDI finder
func NewFTDIFinder ( findPeriod time . Duration ) * FTDIFinder {
log . Trace ( ) . Str ( "file" , "FTDIFinder" ) . Msg ( "FTDI finder created" )
return & FTDIFinder {
findTicker : * time . NewTicker ( findPeriod ) ,
peripherals : make ( map [ string ] Peripheral ) ,
scanChannel : make ( chan struct { } ) ,
}
}
//go:embed third-party/ftdi/detectFTDI.exe
var finderExe [ ] byte
//go:embed third-party/ftdi/dmxSender.exe
var senderExe [ ] byte
// Initialize initializes the FTDI finder
func ( f * FTDIFinder ) Initialize ( ) error {
// Check platform
if goRuntime . GOOS != "windows" {
log . Error ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "platform" , goRuntime . GOOS ) . Msg ( "FTDI finder not compatible with your platform" )
return fmt . Errorf ( "the FTDI finder is not compatible with your platform yet (%s)" , goRuntime . GOOS )
}
// Create the FTDI executables
err := createExecutable ( ftdiFinderExecutableName , finderExe )
if err != nil {
return err
}
createExecutable ( ftdiSenderExecutableName , senderExe )
if err != nil {
return err
}
log . Trace ( ) . Str ( "file" , "FTDIFinder" ) . Msg ( "FTDI finder initialized" )
return nil
}
// createExecutable creates and writes an executable to the temporary directory of the system
func createExecutable ( fileName string , storedFile [ ] byte ) error {
tempFile , err := os . Create ( filepath . Join ( os . TempDir ( ) , fileName ) )
if err != nil {
log . Err ( err ) . Str ( "file" , "FTDIFinder" ) . Str ( "fileName" , fileName ) . Msg ( "unable to create an FTDI executable" )
return err
}
log . Trace ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "filePath" , tempFile . Name ( ) ) . Msg ( "FTDI executable created" )
// Write the embedded executable to the temp file
if _ , err := tempFile . Write ( storedFile ) ; err != nil {
log . Err ( err ) . Str ( "file" , "FTDIFinder" ) . Str ( "fileName" , fileName ) . Msg ( "unable to write the content to an FTDI executable" )
return err
}
tempFile . Close ( )
log . Trace ( ) . Str ( "file" , "FTDIPeripheral" ) . Str ( "fileName" , fileName ) . Msg ( "FTDI executable written" )
return nil
}
// Start starts the finder and search for peripherals
func ( f * FTDIFinder ) Start ( ctx context . Context ) error {
f . goWait . Add ( 1 )
go func ( ) {
defer f . goWait . Done ( )
for {
select {
case <- ctx . Done ( ) :
return
case <- f . findTicker . C :
// Scan the peripherals
err := f . scanPeripherals ( ctx )
if err != nil {
log . Err ( err ) . Str ( "file" , "FTDIFinder" ) . Msg ( "unable to scan FTDI peripherals" )
}
case <- f . scanChannel :
// Scan the peripherals
err := f . scanPeripherals ( ctx )
if err != nil {
log . Err ( err ) . Str ( "file" , "FTDIFinder" ) . Msg ( "unable to scan FTDI peripherals" )
}
}
}
} ( )
return nil
}
// ForceScan explicily asks for scanning peripherals
func ( f * FTDIFinder ) ForceScan ( ) {
f . scanChannel <- struct { } { }
}
// Stop stops the finder
func ( f * FTDIFinder ) Stop ( ) error {
log . Trace ( ) . Str ( "file" , "FTDIFinder" ) . Msg ( "stopping the FTDI finder..." )
// Wait for goroutines to stop
f . goWait . Wait ( )
// Stop the ticker
f . findTicker . Stop ( )
// Delete the FTDI executable files
fileToDelete := filepath . Join ( os . TempDir ( ) , ftdiFinderExecutableName )
err := os . Remove ( fileToDelete )
if err != nil {
log . Warn ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "fileName" , fileToDelete ) . AnErr ( "error" , err ) . Msg ( "unable to remove the executable file" )
}
fileToDelete = filepath . Join ( os . TempDir ( ) , ftdiSenderExecutableName )
err = os . Remove ( fileToDelete )
if err != nil {
log . Warn ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "fileName" , fileToDelete ) . AnErr ( "error" , err ) . Msg ( "unable to remove the executable file" )
}
log . Trace ( ) . Str ( "file" , "FTDIFinder" ) . Msg ( "FTDI finder stopped" )
return nil
}
// GetName returns the name of the driver
func ( f * FTDIFinder ) GetName ( ) string {
return "FTDI"
}
// GetPeripheral gets the peripheral that correspond to the specified ID
func ( f * FTDIFinder ) GetPeripheral ( peripheralID string ) ( Peripheral , bool ) {
// Return the specified peripheral
peripheral := f . peripherals [ peripheralID ]
if peripheral == nil {
log . Error ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralID" , peripheralID ) . Msg ( "unable to get this peripheral from the FTDI finder" )
return nil , false
}
log . Debug ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralID" , peripheralID ) . Msg ( "peripheral found by the FTDI finder" )
return peripheral , true
}
// scanPeripherals scans the FTDI peripherals
func ( f * FTDIFinder ) scanPeripherals ( ctx context . Context ) error {
detectionCtx , cancel := context . WithCancel ( ctx )
defer cancel ( )
log . Trace ( ) . Str ( "file" , "FTDIFinder" ) . Msg ( "FTDI scan triggered" )
ftdiPeripherals := make ( map [ string ] Peripheral )
finder := exec . CommandContext ( detectionCtx , filepath . Join ( os . TempDir ( ) , ftdiFinderExecutableName ) )
log . Trace ( ) . Str ( "file" , "FTDIFinder" ) . Msg ( "has executed the FIND executable" )
stdout , err := finder . StdoutPipe ( )
if err != nil {
return fmt . Errorf ( "unable to create the stdout pipe: %s" , err )
}
defer stdout . Close ( )
stderr , err := finder . StderrPipe ( )
if err != nil {
return fmt . Errorf ( "unable to create the stderr pipe: %s" , err )
}
defer stderr . Close ( )
err = finder . Start ( )
if err != nil {
return fmt . Errorf ( "unable to find FTDI peripherals: %s" , err )
}
scannerErr := bufio . NewScanner ( stderr )
for scannerErr . Scan ( ) {
return fmt . Errorf ( "unable to find FTDI peripherals: %s" , scannerErr . Text ( ) )
}
scanner := bufio . NewScanner ( stdout )
for scanner . Scan ( ) {
peripheralString := scanner . Text ( )
// The program output is like '0:1:2' where 0 is the location, 1 is the S/N and 2 is the name
peripheralInfo := strings . Split ( peripheralString , ":" )
2025-01-18 15:24:16 +01:00
log . Trace ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "scannedString" , peripheralString ) . Str ( "peripheralName" , peripheralInfo [ 2 ] ) . Str ( "peripheralSN" , peripheralInfo [ 1 ] ) . Msg ( "new FTDI peripheral detected" )
2025-01-04 00:36:29 +01:00
// Convert the location to an integer
location , err := strconv . Atoi ( peripheralInfo [ 0 ] )
if err != nil {
log . Warn ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralName" , peripheralInfo [ 2 ] ) . Msg ( "no location provided for this FTDI peripheral" )
location = - 1
}
// Add the peripheral to the temporary list
peripheral , err := NewFTDIPeripheral ( peripheralInfo [ 2 ] , peripheralInfo [ 1 ] , location )
if err != nil {
return fmt . Errorf ( "unable to create the FTDI peripheral: %v" , err )
}
2025-01-18 15:24:16 +01:00
log . Trace ( ) . Any ( "periph" , & peripheral ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralName" , peripheralInfo [ 2 ] ) . Msg ( "has been created" )
2025-01-04 00:36:29 +01:00
ftdiPeripherals [ peripheralInfo [ 1 ] ] = peripheral
2025-01-18 15:24:16 +01:00
log . Trace ( ) . Any ( "periph" , ftdiPeripherals ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralName" , peripheralInfo [ 2 ] ) . Msg ( "successfully added the FTDI peripheral to the finder" )
2025-01-04 00:36:29 +01:00
}
2025-01-18 15:24:16 +01:00
// Emit the peripherals changes to the front
emitPeripheralsChanges ( ctx , f . peripherals , ftdiPeripherals )
2025-01-04 00:36:29 +01:00
// Store the new peripherals list
f . peripherals = ftdiPeripherals
return nil
}
// CreatePeripheral is not implemented here
func ( f * FTDIFinder ) CreatePeripheral ( context . Context ) ( Peripheral , error ) {
return nil , nil
}
// DeletePeripheral is not implemented here
func ( f * FTDIFinder ) DeletePeripheral ( serialNumber string ) error {
return nil
}