Files
dmxconnect/hardware/FTDIFinder.go

235 lines
7.6 KiB
Go
Raw Normal View History

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, ":")
log.Debug().Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Str("peripheralSN", peripheralInfo[1]).Msg("new FTDI peripheral detected")
// 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)
}
ftdiPeripherals[peripheralInfo[1]] = peripheral
log.Trace().Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("successfully added the FTDI peripheral to the finder")
}
// Compare with the current peripherals to detect arrivals/removals
removedList, addedList := comparePeripherals(f.peripherals, ftdiPeripherals)
// Emit the events
emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
log.Info().Str("file", "FTDIFinder").Msg("FTDI remove list emitted to the front")
emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
log.Info().Str("file", "FTDIFinder").Msg("FTDI add list emitted to the front")
// 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
}