diff --git a/app.go b/app.go index 719657d..6061afa 100644 --- a/app.go +++ b/app.go @@ -1,10 +1,11 @@ package main import ( - "changeme/hardware" "context" + "dmxconnect/hardware" "fmt" "io" + "time" "github.com/rs/zerolog/log" @@ -16,6 +17,7 @@ import ( // App struct type App struct { ctx context.Context + cancelFunc context.CancelFunc hardwareManager *hardware.HardwareManager // For managing all the hardware wmiMutex sync.Mutex // Avoid some WMI operations at the same time projectInfo ProjectInfo // The project information structure @@ -26,9 +28,9 @@ type App struct { func NewApp() *App { // Create a new hadware manager hardwareManager := hardware.NewHardwareManager() - hardwareManager.RegisterDriver(hardware.NewMIDIDriver()) - hardwareManager.RegisterDriver(hardware.NewFTDIDriver()) - hardwareManager.RegisterDriver(hardware.NewOS2LDriver()) + hardwareManager.RegisterDriver(hardware.NewMIDIFinder(5 * time.Second)) + hardwareManager.RegisterDriver(hardware.NewFTDIFinder(5 * time.Second)) + // hardwareManager.RegisterDriver(hardware.NewOS2LDriver()) return &App{ hardwareManager: hardwareManager, projectSave: "", @@ -41,8 +43,8 @@ func NewApp() *App { // startup is called when the app starts. The context is saved // so we can call the runtime methods func (a *App) onStartup(ctx context.Context) { - a.ctx = ctx - err := a.hardwareManager.Start(ctx) + a.ctx, a.cancelFunc = context.WithCancel(ctx) + err := a.hardwareManager.Start(a.ctx) if err != nil { log.Err(err).Str("file", "app").Msg("unable to start the hardware manager") return @@ -52,18 +54,25 @@ func (a *App) onStartup(ctx context.Context) { // onReady is called when the DOM is ready // We get the current peripherals connected func (a *App) onReady(ctx context.Context) { - log.Debug().Str("file", "peripherals").Msg("getting peripherals...") - err := a.hardwareManager.Scan(a.ctx) - if err != nil { - log.Err(err).Str("file", "app").Msg("unable to get the peripherals") - } + // log.Debug().Str("file", "peripherals").Msg("getting peripherals...") + // err := a.hardwareManager.Scan() + // if err != nil { + // log.Err(err).Str("file", "app").Msg("unable to get the peripherals") + // } return } // onShutdown is called when the app is closing // We stop all the pending processes func (a *App) onShutdown(ctx context.Context) { - log.Warn().Str("file", "app").Msg("app is closing") + // Close the application properly + log.Trace().Str("file", "app").Msg("app is closing") + // Explicitly close the context + a.cancelFunc() + err := a.hardwareManager.Stop() + if err != nil { + log.Err(err).Str("file", "app").Msg("unable to stop the hardware manager") + } return } diff --git a/go.mod b/go.mod index cb0683f..5101354 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module changeme +module dmxconnect go 1.21 diff --git a/hardware/FTDIDriver.go b/hardware/FTDIDriver.go deleted file mode 100644 index bfa4244..0000000 --- a/hardware/FTDIDriver.go +++ /dev/null @@ -1,152 +0,0 @@ -package hardware - -import ( - "bufio" - "context" - _ "embed" - "fmt" - "os" - "os/exec" - goRuntime "runtime" - "strconv" - "strings" - "time" - - "github.com/rs/zerolog/log" -) - -const ( - scanDelay = 4 * time.Second // Waiting delay before scanning the FTDI devices -) - -// FTDIDriver represents how the protocol is defined -type FTDIDriver struct { - peripherals map[string]Peripheral -} - -// NewFTDIDriver creates a new FTDI finder -func NewFTDIDriver() *FTDIDriver { - log.Trace().Str("file", "FTDIDriver").Msg("FTDI driver created") - return &FTDIDriver{ - peripherals: make(map[string]Peripheral), - } -} - -//go:embed third-party/ftdi/detectFTDI.exe -var findFTDI []byte - -// Initialize initializes the FTDI driver -func (d *FTDIDriver) Initialize() error { - // Check platform - if goRuntime.GOOS != "windows" { - log.Error().Str("file", "FTDIDriver").Str("platform", goRuntime.GOOS).Msg("FTDI driver not compatible with your platform") - return fmt.Errorf(" The FTDI driver is not compatible with your platform yet (%s)", goRuntime.GOOS) - } - log.Trace().Str("file", "FTDIDriver").Msg("FTDI driver initialized") - return nil -} - -// GetName returns the name of the driver -func (d *FTDIDriver) GetName() string { - return "FTDI" -} - -// GetPeripheral gets the peripheral that correspond to the specified ID -func (d *FTDIDriver) GetPeripheral(peripheralID string) (Peripheral, bool) { - // Return the specified peripheral - peripheral := d.peripherals[peripheralID] - if peripheral == nil { - log.Error().Str("file", "FTDIDriver").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI driver") - return nil, false - } - log.Debug().Str("file", "FTDIDriver").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI driver") - return peripheral, true -} - -// Scan scans the FTDI peripherals -func (d *FTDIDriver) Scan(ctx context.Context) error { - log.Trace().Str("file", "FTDIDriver").Msg("FTDI scan triggered") - time.Sleep(scanDelay) - - // Create a temporary file - tempFile, err := os.CreateTemp("", "findFTDI*.exe") - if err != nil { - return err - } - defer os.Remove(tempFile.Name()) - log.Trace().Str("file", "FTDIDriver").Msg("has created the FIND executable temp") - - // Write the embedded executable to the temp file - if _, err := tempFile.Write(findFTDI); err != nil { - return err - } - tempFile.Close() - log.Trace().Str("file", "FTDIDriver").Msg("has written the FIND executable") - - ftdiPeripherals := make(map[string]Peripheral) - - finder := exec.Command(tempFile.Name()) - log.Trace().Str("file", "FTDIDriver").Msg("has executed the FIND executable") - - stdout, err := finder.StdoutPipe() - if err != nil { - return fmt.Errorf("unable to create the stdout pipe: %s", err) - } - - stderr, err := finder.StderrPipe() - if err != nil { - return fmt.Errorf("unable to create the stderr pipe: %s", err) - } - - err = finder.Start() - if err != nil { - return fmt.Errorf("unable to find FTDI devices: %s", err) - } - - scannerErr := bufio.NewScanner(stderr) - for scannerErr.Scan() { - return fmt.Errorf("unable to find FTDI devices: %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", "FTDIDriver").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", "FTDIDriver").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", "FTDIDriver").Str("peripheralName", peripheralInfo[2]).Msg("successfully added the FTDI peripheral to the driver") - } - // Compare with the current peripherals to detect arrivals/removals - removedList, addedList := comparePeripherals(d.peripherals, ftdiPeripherals) - // Emit the events - emitPeripheralsEvents(ctx, removedList, PeripheralRemoval) - log.Info().Str("file", "FTDIDriver").Msg("FTDI remove list emitted to the front") - emitPeripheralsEvents(ctx, addedList, PeripheralArrival) - log.Info().Str("file", "FTDIDriver").Msg("FTDI add list emitted to the front") - // Store the new peripherals list - d.peripherals = ftdiPeripherals - return nil -} - -// CreatePeripheral is not implemented here -func (d *FTDIDriver) CreatePeripheral(context.Context) (Peripheral, error) { - return nil, nil -} - -// RemovePeripheral is not implemented here -func (d *FTDIDriver) RemovePeripheral(serialNumber string) error { - return nil -} diff --git a/hardware/FTDIFinder.go b/hardware/FTDIFinder.go new file mode 100644 index 0000000..279dad2 --- /dev/null +++ b/hardware/FTDIFinder.go @@ -0,0 +1,234 @@ +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 +} diff --git a/hardware/FTDIPeripheral.go b/hardware/FTDIPeripheral.go index c4455b0..aa91558 100644 --- a/hardware/FTDIPeripheral.go +++ b/hardware/FTDIPeripheral.go @@ -34,31 +34,12 @@ type FTDIPeripheral struct { errorsChan chan error // Channel to get the errors } -//go:embed third-party/ftdi/dmxSender.exe -var dmxSender []byte - // 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") - // Create a temporary file - tempFile, err := os.Create(fmt.Sprintf("dmxSender-%s.exe", serialNumber)) - if err != nil { - return nil, err - } - log.Trace().Str("file", "FTDIPeripheral").Str("s/n", serialNumber).Msg("FTDI sender temp created") - - // Write the embedded executable to the temp file - if _, err := tempFile.Write(dmxSender); err != nil { - return nil, err - } - log.Trace().Str("file", "FTDIPeripheral").Str("s/n", serialNumber).Msg("FTDI sender written") - - tempFile.Close() - return &FTDIPeripheral{ name: name, dmxSender: nil, - programName: tempFile.Name(), serialNumber: serialNumber, location: location, universesNumber: 1, diff --git a/hardware/MIDIDriver.go b/hardware/MIDIFinder.go similarity index 50% rename from hardware/MIDIDriver.go rename to hardware/MIDIFinder.go index 7137334..6116d12 100644 --- a/hardware/MIDIDriver.go +++ b/hardware/MIDIFinder.go @@ -6,44 +6,88 @@ import ( "regexp" "strconv" "strings" + "sync" + "time" "github.com/mattrtaylor/go-rtmidi" "github.com/rs/zerolog/log" ) -// MIDIDriver represents how the protocol is defined -type MIDIDriver struct { +// MIDIFinder represents how the protocol is defined +type MIDIFinder struct { + findTicker time.Ticker // Peripherals find ticker peripherals map[string]Peripheral // The list of peripherals + scanChannel chan struct{} // The channel to trigger a scan event + goWait sync.WaitGroup // Check goroutines execution } -// NewMIDIDriver creates a new DMXUSB protocol -func NewMIDIDriver() *MIDIDriver { - log.Trace().Str("file", "MIDIDriver").Msg("MIDI driver created") - return &MIDIDriver{ +// NewMIDIFinder creates a new DMXUSB protocol +func NewMIDIFinder(findPeriod time.Duration) *MIDIFinder { + log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder created") + return &MIDIFinder{ + findTicker: *time.NewTicker(findPeriod), peripherals: make(map[string]Peripheral), } } // Initialize initializes the MIDI driver -func (d *MIDIDriver) Initialize() error { - log.Trace().Str("file", "MIDIDriver").Msg("MIDI driver initialized") +func (f *MIDIFinder) Initialize() error { + log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder initialized") + return nil +} + +// Start starts the finder and search for peripherals +func (f *MIDIFinder) 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", "MIDIFinder").Msg("unable to scan MIDI peripherals") + } + case <-f.scanChannel: + // Scan the peripherals + err := f.scanPeripherals(ctx) + if err != nil { + log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals") + } + } + } + }() + return nil +} + +// Stop stops the finder +func (f *MIDIFinder) Stop() error { + log.Trace().Str("file", "MIDIFinder").Msg("stopping the MIDI finder...") + // Wait for goroutines to stop + f.goWait.Wait() + // Stop the ticker + f.findTicker.Stop() + log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder stopped") return nil } // GetName returns the name of the driver -func (d *MIDIDriver) GetName() string { +func (f *MIDIFinder) GetName() string { return "MIDI" } // GetPeripheral gets the peripheral that correspond to the specified ID -func (d *MIDIDriver) GetPeripheral(peripheralID string) (Peripheral, bool) { +func (f *MIDIFinder) GetPeripheral(peripheralID string) (Peripheral, bool) { // Return the specified peripheral - peripheral, found := d.peripherals[peripheralID] + peripheral, found := f.peripherals[peripheralID] if !found { - log.Error().Str("file", "MIDIDriver").Str("peripheralID", peripheralID).Msg("unable to get this peripheral in the MIDI driver") + log.Error().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral in the MIDI finder") return nil, false } - log.Trace().Str("file", "MIDIDriver").Str("peripheralID", peripheralID).Msg("MIDI peripheral found in the driver") + log.Trace().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("MIDI peripheral found in the driver") return peripheral, true } @@ -68,23 +112,26 @@ func splitStringAndNumber(input string) (string, int, error) { return "", 0, fmt.Errorf("no number found at the end of the string") } -// Scan scans the interfaces compatible with the MIDI protocol -func (d *MIDIDriver) Scan(ctx context.Context) error { +// ForceScan explicily asks for scanning peripherals +func (f *MIDIFinder) ForceScan() { + f.scanChannel <- struct{}{} +} + +// scanPeripherals scans the MIDI peripherals +func (f *MIDIFinder) scanPeripherals(ctx context.Context) error { midiPeripherals := make(map[string]Peripheral) - log.Trace().Str("file", "MIDIDriver").Msg("opening MIDI scanner port...") + log.Trace().Str("file", "MIDIFinder").Msg("opening MIDI scanner port...") midiScanner, err := rtmidi.NewMIDIInDefault() if err != nil { - log.Err(err).Str("file", "MIDIDriver").Msg("unable to open the MIDI scanner port...") + log.Err(err).Str("file", "MIDIFinder").Msg("unable to open the MIDI scanner port...") return fmt.Errorf("unable to open the MIDI scanner: %s", err) } defer midiScanner.Close() - midiScanner.SetCallback(func(m rtmidi.MIDIIn, b []byte, f float64) { - - }) - log.Trace().Str("file", "MIDIDriver").Msg("scanning MIDI peripherals...") + midiScanner.SetCallback(func(m rtmidi.MIDIIn, b []byte, f float64) {}) + log.Trace().Str("file", "MIDIFinder").Msg("scanning MIDI peripherals...") devicesCount, err := midiScanner.PortCount() if err != nil { - log.Err(err).Str("file", "MIDIDriver").Msg("unable to scan MIDI peripherals...") + log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals...") return fmt.Errorf("unable to scan MIDI peripherals: %s", err) } for i := 0; i < devicesCount; i++ { @@ -96,32 +143,32 @@ func (d *MIDIDriver) Scan(ctx context.Context) error { // Separate data name, location, err := splitStringAndNumber(portName) if err != nil { - log.Err(err).Str("file", "MIDIDriver").Str("description", portName).Msg("invalid peripheral description") + log.Err(err).Str("file", "MIDIFinder").Str("description", portName).Msg("invalid peripheral description") return fmt.Errorf("invalid pripheral description: %s", err) } - log.Info().Str("file", "MIDIDriver").Str("name", name).Int("location", location).Msg("MIDI peripheral found") + log.Info().Str("file", "MIDIFinder").Str("name", name).Int("location", location).Msg("MIDI peripheral found") // Add the peripheral to the temporary list sn := strings.ToLower(strings.Replace(name, " ", "_", -1)) midiPeripherals[sn] = NewMIDIPeripheral(name, location, sn) } // Compare with the current peripherals to detect arrivals/removals - removedList, addedList := comparePeripherals(d.peripherals, midiPeripherals) + removedList, addedList := comparePeripherals(f.peripherals, midiPeripherals) // Emit the events emitPeripheralsEvents(ctx, removedList, PeripheralRemoval) - log.Info().Str("file", "MIDIDriver").Msg("MIDI remove list emitted to the front") + log.Info().Str("file", "MIDIFinder").Msg("MIDI remove list emitted to the front") emitPeripheralsEvents(ctx, addedList, PeripheralArrival) - log.Info().Str("file", "MIDIDriver").Msg("MIDI add list emitted to the front") + log.Info().Str("file", "MIDIFinder").Msg("MIDI add list emitted to the front") // Store the new peripherals list - d.peripherals = midiPeripherals + f.peripherals = midiPeripherals return nil } // CreatePeripheral is not implemented here -func (d *MIDIDriver) CreatePeripheral(context.Context) (Peripheral, error) { +func (f *MIDIFinder) CreatePeripheral(context.Context) (Peripheral, error) { return nil, nil } -// RemovePeripheral is not implemented here -func (d *MIDIDriver) RemovePeripheral(serialNumber string) error { +// DeletePeripheral is not implemented here +func (f *MIDIFinder) DeletePeripheral(serialNumber string) error { return nil } diff --git a/hardware/hardware.go b/hardware/hardware.go index 6f343ff..3097490 100644 --- a/hardware/hardware.go +++ b/hardware/hardware.go @@ -3,9 +3,8 @@ package hardware import ( "context" "fmt" - "syscall" + "sync" "time" - "unsafe" "github.com/rs/zerolog/log" @@ -31,119 +30,63 @@ var ( // HardwareManager is the class who manages the hardware type HardwareManager struct { - drivers map[string]PeripheralDriver // The map of peripherals finders - peripherals []Peripheral // The current list of peripherals - deviceChangedEvent chan struct{} // The event when the devices list changed - ctx context.Context + drivers map[string]PeripheralFinder // The map of peripherals finders + peripherals []Peripheral // The current list of peripherals + peripheralsScanTrigger chan struct{} // Trigger the peripherals scans + goWait sync.WaitGroup // Wait for goroutines to terminate } // NewHardwareManager creates a new HardwareManager func NewHardwareManager() *HardwareManager { log.Trace().Str("package", "hardware").Msg("Hardware instance created") return &HardwareManager{ - drivers: make(map[string]PeripheralDriver), - peripherals: make([]Peripheral, 0), - deviceChangedEvent: make(chan struct{}), + drivers: make(map[string]PeripheralFinder), + peripherals: make([]Peripheral, 0), + peripheralsScanTrigger: make(chan struct{}), } } // Start starts to find new peripheral events func (h *HardwareManager) Start(ctx context.Context) error { - // Configure wndProc callback - cb := windows.NewCallback(h.wndProc) - inst := win.GetModuleHandle(nil) - - // Register window class - className, err := syscall.UTF16PtrFromString("DMXConnectPeripheralWatcher") - if err != nil { - return fmt.Errorf("failed to convert window class name to UTF16: %w", err) - } - wc := win.WNDCLASSEX{ - CbSize: uint32(unsafe.Sizeof(win.WNDCLASSEX{})), - HInstance: inst, - LpfnWndProc: cb, - LpszClassName: className, - } - if win.RegisterClassEx(&wc) == 0 { - return fmt.Errorf("failed to register window class: %w", syscall.GetLastError()) - } - - // Create hidden window - windowName, err := syscall.UTF16PtrFromString("usbevent.exe") - if err != nil { - return fmt.Errorf("failed to convert window name to UTF16: %w", err) - } - hwnd := win.CreateWindowEx( - 0, - wc.LpszClassName, - windowName, - win.WS_OVERLAPPEDWINDOW, - win.CW_USEDEFAULT, - win.CW_USEDEFAULT, - 100, - 100, - 0, - 0, - wc.HInstance, - nil, - ) - if hwnd == 0 { - return fmt.Errorf("failed to create window: %w", syscall.GetLastError()) - } - - // Hide and update window - win.ShowWindow(hwnd, win.SW_HIDE) - win.UpdateWindow(hwnd) - - // Start message loop in a goroutine - go messageLoop(ctx, hwnd) - - // To handle the peripheral changed - go func() { - defer log.Debug().Str("file", "hardware").Msg("peripheral getter goroutine exited") - for { - select { - case <-ctx.Done(): - return - case <-h.deviceChangedEvent: - log.Debug().Str("file", "hardware").Msg("peripheral change event, triggering scan...") - err := h.Scan(ctx) - if err != nil { - log.Err(err).Str("file", "hardware").Msg("unable to scan peripherals") - } - } + for finderName, finder := range h.drivers { + err := finder.Initialize() + if err != nil { + log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to initialize finder") + return err } - }() + err = finder.Start(ctx) + if err != nil { + log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to start finder") + return err + } + } + // n, err := detector.Register() + // if err != nil { + // log.Err(err).Str("file", "hardware").Msg("error registering the usb event") + // } + // h.detector = n + // // Run the detector + // n.Run(ctx) + // h.goWait.Add(1) + // go func() { + // defer h.goWait.Done() + // for { + // select { + // case <-ctx.Done(): + // return + // case <-h.detector.EventChannel: + // // Trigger hardware scans + // log.Info().Str("file", "hardware").Msg("peripheral change event") + // case <-h.peripheralsScanTrigger: + // log.Info().Str("file", "hardware").Msg("scan triggered") + // } + // } + // }() return nil } -func messageLoop(ctx context.Context, hwnd win.HWND) { - defer log.Debug().Str("file", "hardware").Msg("Peripheral watcher goroutine exited") - - for { - select { - case <-ctx.Done(): - win.PostQuitMessage(0) // Gracefully terminate message loop - return - default: - var msg win.MSG - result := win.GetMessage(&msg, hwnd, 0, 0) - if result > 0 { - win.TranslateMessage(&msg) - win.DispatchMessage(&msg) - } else if result == 0 { - log.Warn().Str("file", "hardware").Msg("WM_QUIT message received") - return - } else { - log.Error().Str("file", "hardware").Msg("GetMessage returned an error") - return - } - } - } -} - // GetDriver returns a register driver -func (h *HardwareManager) GetDriver(driverName string) (PeripheralDriver, error) { +func (h *HardwareManager) GetDriver(driverName string) (PeripheralFinder, error) { driver, exists := h.drivers[driverName] if !exists { log.Error().Str("file", "hardware").Str("driverName", driverName).Msg("unable to get the driver") @@ -154,7 +97,7 @@ func (h *HardwareManager) GetDriver(driverName string) (PeripheralDriver, error) } // RegisterDriver registers a new peripherals driver -func (h *HardwareManager) RegisterDriver(driver PeripheralDriver) { +func (h *HardwareManager) RegisterDriver(driver PeripheralFinder) { h.drivers[driver.GetName()] = driver log.Info().Str("file", "hardware").Str("driverName", driver.GetName()).Msg("driver registered") } @@ -174,41 +117,43 @@ func (h *HardwareManager) GetPeripheral(driverName string, peripheralID string) } // Scan scans all the peripherals for the registered finders -func (h *HardwareManager) Scan(ctx context.Context) error { - if len(h.drivers) == 0 { - log.Warn().Str("file", "hardware").Msg("no driver registered") - return fmt.Errorf("no driver registered") - } - for _, driver := range h.drivers { - driverCopy := driver - go func() { - err := driverCopy.Scan(ctx) - if err != nil { - log.Err(err).Str("file", "hardware").Str("driverName", driverCopy.GetName()).Msg("unable to scan peripheral") - return - } - }() - } +func (h *HardwareManager) Scan() error { + h.peripheralsScanTrigger <- struct{}{} return nil } func (h *HardwareManager) wndProc(hwnd windows.HWND, msg uint32, wParam, lParam uintptr) uintptr { - log.Trace().Str("file", "hardware").Msg("wndProc triggered") - switch msg { - case win.WM_DEVICECHANGE: + log.Trace().Str("file", "hardware").Uint32("msg", msg).Msg("wndProc triggered") + if msg == win.WM_DEVICECHANGE { // Trigger the devices scan when the last DEVICE_CHANGE event is received if debounceTimer != nil { debounceTimer.Stop() - log.Trace().Str("file", "hardware").Msg("scan debounce timer stopped") + log.Debug().Str("file", "hardware").Msg("scan debounce timer stopped") } debounceTimer = time.AfterFunc(debounceDuration, func() { log.Debug().Str("file", "hardware").Msg("peripheral changed") - h.deviceChangedEvent <- struct{}{} + h.peripheralsScanTrigger <- struct{}{} }) } return win.DefWindowProc(win.HWND(hwnd), msg, wParam, lParam) } +// Stop stops the hardware manager +func (h *HardwareManager) Stop() error { + log.Trace().Str("file", "hardware").Msg("closing the hardware manager") + // Stop each finder + for finderName, finder := range h.drivers { + err := finder.Stop() + if err != nil { + log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to stop the finder") + } + } + // Wait for goroutines to finish + h.goWait.Wait() + log.Info().Str("file", "hardware").Msg("hardware manager stopped") + return nil +} + // peripheralsList emits a peripheral event func emitPeripheralsEvents(ctx context.Context, peripheralsList map[string]Peripheral, peripheralEvent PeripheralEvent) { for _, peripheral := range peripheralsList { diff --git a/hardware/interfaces.go b/hardware/interfaces.go index aa1e86a..ff3cec1 100644 --- a/hardware/interfaces.go +++ b/hardware/interfaces.go @@ -21,12 +21,14 @@ type PeripheralInfo struct { Settings []interface{} `yaml:"settings"` // Number of DMX universes handled by the peripheral } -// PeripheralDriver represents how compatible peripheral drivers are implemented -type PeripheralDriver interface { +// PeripheralFinder represents how compatible peripheral drivers are implemented +type PeripheralFinder interface { Initialize() error // Initializes the protocol + Start(context.Context) error // Start the detection + Stop() error // Stop the detection + ForceScan() // Explicitly scans for peripherals + CreatePeripheral(ctx context.Context) (Peripheral, error) // Creates a new peripheral + DeletePeripheral(serialNumber string) error // Removes a peripheral GetName() string // Get the name of the finder GetPeripheral(string) (Peripheral, bool) // Get the peripheral - Scan(context.Context) error // Scan for peripherals - CreatePeripheral(ctx context.Context) (Peripheral, error) // Creates a new peripheral - RemovePeripheral(serialNumber string) error // Removes a peripheral } diff --git a/peripherals.go b/peripherals.go index e5578ff..3dd917e 100644 --- a/peripherals.go +++ b/peripherals.go @@ -1,7 +1,7 @@ package main import ( - "changeme/hardware" + "dmxconnect/hardware" "fmt" "github.com/rs/zerolog/log" diff --git a/project.go b/project.go index 7b51a73..e2afa78 100644 --- a/project.go +++ b/project.go @@ -1,7 +1,7 @@ package main import ( - "changeme/hardware" + "dmxconnect/hardware" "fmt" "github.com/rs/zerolog/log"