peripheral optimizations

This commit is contained in:
2025-01-04 00:36:29 +01:00
parent 556f24991e
commit e4392c8902
10 changed files with 408 additions and 342 deletions

33
app.go
View File

@@ -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
}

2
go.mod
View File

@@ -1,4 +1,4 @@
module changeme
module dmxconnect
go 1.21

View File

@@ -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
}

234
hardware/FTDIFinder.go Normal file
View File

@@ -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
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -1,7 +1,7 @@
package main
import (
"changeme/hardware"
"dmxconnect/hardware"
"fmt"
"github.com/rs/zerolog/log"

View File

@@ -1,7 +1,7 @@
package main
import (
"changeme/hardware"
"dmxconnect/hardware"
"fmt"
"github.com/rs/zerolog/log"