2025-01-18 14:53:29 +00:00
package hardware
import (
"bufio"
"context"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
goRuntime "runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/rs/zerolog/log"
2025-08-31 11:15:38 +02:00
"github.com/wailsapp/wails/v2/pkg/runtime"
2025-01-18 14:53:29 +00:00
)
const (
ftdiFinderExecutableName = "FTDI_finder.exe"
)
// FTDIFinder represents how the protocol is defined
type FTDIFinder struct {
2025-01-26 12:01:31 +01:00
findTicker time . Ticker // Peripherals find ticker
foundPeripherals map [ string ] PeripheralInfo // The list of peripherals handled by this finder
registeredPeripherals map [ string ] FTDIPeripheral // The list of found peripherals
scanChannel chan struct { } // The channel to trigger a scan event
goWait sync . WaitGroup // Check goroutines execution
2025-01-18 14:53:29 +00:00
}
// NewFTDIFinder creates a new FTDI finder
func NewFTDIFinder ( findPeriod time . Duration ) * FTDIFinder {
log . Trace ( ) . Str ( "file" , "FTDIFinder" ) . Msg ( "FTDI finder created" )
return & FTDIFinder {
2025-01-26 12:01:31 +01:00
findTicker : * time . NewTicker ( findPeriod ) ,
foundPeripherals : make ( map [ string ] PeripheralInfo ) ,
registeredPeripherals : make ( map [ string ] FTDIPeripheral ) ,
scanChannel : make ( chan struct { } ) ,
2025-01-18 14:53:29 +00:00
}
}
2025-01-26 12:01:31 +01:00
// RegisterPeripheral registers a new peripheral
func ( f * FTDIFinder ) RegisterPeripheral ( ctx context . Context , peripheralData PeripheralInfo ) ( string , error ) {
ftdiPeripheral , err := NewFTDIPeripheral ( peripheralData )
if err != nil {
return "" , fmt . Errorf ( "unable to create the FTDI peripheral: %v" , err )
}
f . registeredPeripherals [ peripheralData . SerialNumber ] = * ftdiPeripheral
log . Trace ( ) . Any ( "periph" , & ftdiPeripheral ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralName" , peripheralData . Name ) . Msg ( "FTDI peripheral has been created" )
return peripheralData . SerialNumber , nil
}
// UnregisterPeripheral unregisters an existing peripheral
2025-08-31 11:15:38 +02:00
func ( f * FTDIFinder ) UnregisterPeripheral ( peripheralID string ) error {
2025-01-26 12:01:31 +01:00
peripheral , registered := f . registeredPeripherals [ peripheralID ]
if registered {
2025-08-31 11:15:38 +02:00
err := peripheral . Disconnect ( )
2025-01-26 12:01:31 +01:00
if err != nil {
return err
}
}
delete ( f . registeredPeripherals , peripheralID )
return nil
}
2025-01-18 14:53:29 +00:00
//go:embed third-party/ftdi/detectFTDI.exe
var finderExe [ ] 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
}
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" )
}
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"
}
2025-01-26 12:01:31 +01:00
// GetPeripheralSettings gets the peripheral settings
func ( f * FTDIFinder ) GetPeripheralSettings ( peripheralID string ) ( map [ string ] interface { } , error ) {
2025-01-18 14:53:29 +00:00
// Return the specified peripheral
2025-01-26 12:01:31 +01:00
peripheral , found := f . registeredPeripherals [ peripheralID ]
if ! found {
2025-01-18 14:53:29 +00:00
log . Error ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralID" , peripheralID ) . Msg ( "unable to get this peripheral from the FTDI finder" )
2025-01-26 12:01:31 +01:00
return nil , fmt . Errorf ( "unable to found the peripheral" )
2025-01-18 14:53:29 +00:00
}
log . Debug ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralID" , peripheralID ) . Msg ( "peripheral found by the FTDI finder" )
2025-01-26 12:01:31 +01:00
return peripheral . GetSettings ( ) , nil
}
// SetPeripheralSettings sets the peripheral settings
func ( f * FTDIFinder ) SetPeripheralSettings ( peripheralID string , settings map [ string ] interface { } ) error {
// Return the specified peripheral
peripheral , found := f . registeredPeripherals [ peripheralID ]
if ! found {
log . Error ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralID" , peripheralID ) . Msg ( "unable to get this peripheral from the FTDI finder" )
return fmt . Errorf ( "unable to found the peripheral" )
}
log . Debug ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralID" , peripheralID ) . Msg ( "peripheral found by the FTDI finder" )
return peripheral . SetSettings ( settings )
2025-01-18 14:53:29 +00:00
}
// 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" )
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 ( ) )
}
2025-01-26 12:01:31 +01:00
temporaryPeripherals := make ( map [ string ] PeripheralInfo )
2025-01-18 14:53:29 +00:00
scanner := bufio . NewScanner ( stdout )
for scanner . Scan ( ) {
peripheralString := scanner . Text ( )
2025-10-19 19:56:05 +02:00
// The program output is like '0:1:2:3' where 0 is the location, 1 is the S/N, 2 is the name and 3 is the open flag [O/C]
2025-01-18 14:53:29 +00:00
peripheralInfo := strings . Split ( peripheralString , ":" )
2025-10-19 19:56:05 +02:00
log . Trace ( ) . Str ( "file" , "FTDIFinder" ) . Str ( "scannedString" , peripheralString ) . Str ( "peripheralOpenFlag" , peripheralInfo [ 3 ] ) . Str ( "peripheralName" , peripheralInfo [ 2 ] ) . Str ( "peripheralSN" , peripheralInfo [ 1 ] ) . Msg ( "new FTDI peripheral detected" )
2025-01-18 14:53:29 +00: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
}
2025-01-26 12:01:31 +01:00
// Add the peripheral info to the found list
temporaryPeripherals [ peripheralInfo [ 1 ] ] = PeripheralInfo {
Name : peripheralInfo [ 2 ] ,
SerialNumber : peripheralInfo [ 1 ] ,
2025-10-19 19:56:05 +02:00
IsOpen : peripheralInfo [ 3 ] == "O" ,
2025-01-26 12:01:31 +01:00
ProtocolName : "FTDI" ,
2025-01-18 14:53:29 +00:00
}
2025-08-31 11:15:38 +02:00
// If this peripheral is already registered, connect it and activate it
2025-01-26 12:01:31 +01:00
peripheral , registered := f . registeredPeripherals [ peripheralInfo [ 1 ] ]
if registered {
2025-08-31 11:15:38 +02:00
runtime . EventsEmit ( ctx , string ( PeripheralStatus ) , peripheral . info , "connecting" )
2025-01-26 12:01:31 +01:00
err := peripheral . Connect ( ctx , location )
if err != nil {
log . Err ( err ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralSN" , peripheralInfo [ 1 ] ) . Msg ( "unable to connect the peripheral" )
}
2025-08-31 11:15:38 +02:00
runtime . EventsEmit ( ctx , string ( PeripheralStatus ) , peripheral . info , "deactivated" )
time . Sleep ( 2 * time . Second )
err = peripheral . Activate ( ctx )
if err != nil {
log . Err ( err ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralSN" , peripheralInfo [ 1 ] ) . Msg ( "unable to activate the peripheral" )
}
runtime . EventsEmit ( ctx , string ( PeripheralStatus ) , peripheral . info , "activated" )
2025-01-26 12:01:31 +01:00
}
log . Trace ( ) . Any ( "periph" , temporaryPeripherals ) . Str ( "file" , "FTDIFinder" ) . Str ( "peripheralName" , peripheralInfo [ 2 ] ) . Msg ( "successfully added the FTDI peripheral to the finder" )
2025-01-18 14:53:29 +00:00
}
// Emit the peripherals changes to the front
2025-01-26 12:01:31 +01:00
emitPeripheralsChanges ( ctx , f . foundPeripherals , temporaryPeripherals )
2025-01-18 14:53:29 +00:00
// Store the new peripherals list
2025-01-26 12:01:31 +01:00
f . foundPeripherals = temporaryPeripherals
2025-01-18 14:53:29 +00:00
return nil
}