From 1b636980597133097e30a368b6f3a9eec04270a2 Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Wed, 12 Nov 2025 13:42:38 +0100 Subject: [PATCH] implements the OS2L finder and improve behaviors of finders --- app.go | 2 +- .../Settings/InputsOutputsContent.svelte | 2 +- hardware/FTDIFinder.go | 102 ++++++++------ hardware/OS2LFinder.go | 132 +++++++++++++++--- hardware/OS2LPeripheral.go | 14 +- hardware/interfaces.go | 13 +- 6 files changed, 183 insertions(+), 82 deletions(-) diff --git a/app.go b/app.go index 67b9289..3b5b12f 100644 --- a/app.go +++ b/app.go @@ -32,7 +32,7 @@ func NewApp() *App { hardwareManager := hardware.NewHardwareManager() // hardwareManager.RegisterFinder(hardware.NewMIDIFinder(5 * time.Second)) hardwareManager.RegisterFinder(hardware.NewFTDIFinder(5 * time.Second)) - // hardwareManager.RegisterFinder(hardware.NewOS2LFinder()) + hardwareManager.RegisterFinder(hardware.NewOS2LFinder()) return &App{ hardwareManager: hardwareManager, projectSave: "", diff --git a/frontend/src/components/Settings/InputsOutputsContent.svelte b/frontend/src/components/Settings/InputsOutputsContent.svelte index a8373a4..403ac56 100644 --- a/frontend/src/components/Settings/InputsOutputsContent.svelte +++ b/frontend/src/components/Settings/InputsOutputsContent.svelte @@ -93,7 +93,7 @@ {/if} {/each}

{$_("projectHardwareOthersLabel")}

- addPeripheral({Name: "OS2L connection", ProtocolName: "OS2L"})} text="Add an OS2L peripheral" icon="bx-plus-circle" tooltip="Configure an OS2L connection"/> + addPeripheral({Name: "OS2L connection", ProtocolName: "OS2L", SerialNumber: ""})} text="Add an OS2L peripheral" icon="bx-plus-circle" tooltip="Configure an OS2L connection"/> diff --git a/hardware/FTDIFinder.go b/hardware/FTDIFinder.go index 2fc2e5b..c82951c 100644 --- a/hardware/FTDIFinder.go +++ b/hardware/FTDIFinder.go @@ -2,6 +2,7 @@ package hardware import ( "context" + "errors" "fmt" goRuntime "runtime" "sync" @@ -62,21 +63,25 @@ func (f *FTDIFinder) RegisterPeripheral(ctx context.Context, peripheralData Peri // If already detected, connect it if peripheral, ok := f.detected[peripheralData.SerialNumber]; ok { - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusConnecting) - err := peripheral.Connect(ctx) - if err != nil { - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) - log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to connect the peripheral") - return peripheralData.SerialNumber, nil - } - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDeactivated) - // Peripheral connected, activate it - err = peripheral.Activate(ctx) - if err != nil { - log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to activate the FTDI peripheral") - return peripheralData.SerialNumber, nil - } - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusActivated) + f.wg.Add(1) + go func() { + defer f.wg.Done() + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusConnecting) + err := peripheral.Connect(ctx) + if err != nil { + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) + log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to connect the peripheral") + return + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDeactivated) + // Peripheral connected, activate it + err = peripheral.Activate(ctx) + if err != nil { + log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to activate the FTDI peripheral") + return + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusActivated) + }() } // Emits the event in the hardware @@ -233,7 +238,7 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { f.detected[sn] = peripheral if f.onArrival != nil { - go f.onArrival(peripheralData) + f.onArrival(peripheralData) } log.Info().Str("sn", sn).Str("name", peripheralData.Name).Msg("[FTDI] New peripheral detected") @@ -242,20 +247,24 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { // If the peripheral is saved in the project => connect if _, saved := f.saved[sn]; saved { - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusConnecting) - err := peripheral.Connect(ctx) - if err != nil { - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) - log.Err(err).Str("sn", sn).Msg("unable to connect the FTDI peripheral") - return nil - } - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDeactivated) - err = peripheral.Activate(ctx) - if err != nil { - log.Err(err).Str("sn", sn).Msg("unable to activate the FTDI peripheral") - return nil - } - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusActivated) + f.wg.Add(1) + go func(p PeripheralInfo) { + defer f.wg.Done() + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p, PeripheralStatusConnecting) + err := peripheral.Connect(ctx) + if err != nil { + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p, PeripheralStatusDisconnected) + log.Err(err).Str("sn", p.SerialNumber).Msg("unable to connect the FTDI peripheral") + return + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p, PeripheralStatusDeactivated) + err = peripheral.Activate(ctx) + if err != nil { + log.Err(err).Str("sn", p.SerialNumber).Msg("unable to activate the FTDI peripheral") + return + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p, PeripheralStatusActivated) + }(peripheralData) } } } @@ -265,9 +274,14 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { if _, still := currentMap[sn]; !still { // Properly clean the DMX device - err := oldPeripheral.Disconnect() + err := oldPeripheral.Deactivate(ctx) if err != nil { - log.Err(err).Str("sn", sn).Msg("unable to clean the FTDI peripheral after disconnection") + log.Err(err).Str("sn", sn).Msg("unable to deactivate the FTDI peripheral after disconnection") + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), oldPeripheral.GetInfo(), PeripheralStatusDeactivated) + err = oldPeripheral.Disconnect() + if err != nil { + log.Err(err).Str("sn", sn).Msg("unable to disconnect the FTDI peripheral after disconnection") } runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), oldPeripheral.GetInfo(), PeripheralStatusDisconnected) @@ -277,7 +291,7 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { // Execute the removal callback if f.onRemoval != nil { - go f.onRemoval(oldPeripheral.GetInfo()) + f.onRemoval(oldPeripheral.GetInfo()) } } } @@ -292,22 +306,22 @@ func (f *FTDIFinder) WaitStop() error { // close(f.scanChannel) // Wait for all the peripherals to close - // log.Trace().Str("file", "FTDIFinder").Msg("closing all FTDI peripherals") - // var errs []error - // for registeredPeripheralSN, registeredPeripheral := range f.registeredPeripherals { - // err := registeredPeripheral.WaitStop() - // if err != nil { - // errs = append(errs, fmt.Errorf("%s: %w", registeredPeripheralSN, err)) - // } - // } + log.Trace().Str("file", "FTDIFinder").Msg("closing all FTDI peripherals") + var errs []error + for registeredPeripheralSN, registeredPeripheral := range f.detected { + err := registeredPeripheral.WaitStop() + if err != nil { + errs = append(errs, fmt.Errorf("%s: %w", registeredPeripheralSN, err)) + } + } // Wait for goroutines to stop f.wg.Wait() // Returning errors - // if len(errs) > 0 { - // return errors.Join(errs...) - // } + if len(errs) > 0 { + return errors.Join(errs...) + } log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder stopped") return nil } diff --git a/hardware/OS2LFinder.go b/hardware/OS2LFinder.go index 24a8d42..8974ac3 100644 --- a/hardware/OS2LFinder.go +++ b/hardware/OS2LFinder.go @@ -2,23 +2,32 @@ package hardware import ( "context" + "errors" "fmt" "math/rand" "strings" + "sync" "github.com/rs/zerolog/log" + "github.com/wailsapp/wails/v2/pkg/runtime" ) // OS2LFinder represents how the protocol is defined type OS2LFinder struct { - registeredPeripherals map[string]OS2LPeripheral // The list of found peripherals + wg sync.WaitGroup + mu sync.Mutex + + saved map[string]*OS2LPeripheral // The list of saved peripherals + + onArrival func(p PeripheralInfo) // When a peripheral arrives + onRemoval func(p PeripheralInfo) // When a peripheral goes away } // NewOS2LFinder creates a new OS2L finder func NewOS2LFinder() *OS2LFinder { log.Trace().Str("file", "OS2LFinder").Msg("OS2L finder created") return &OS2LFinder{ - registeredPeripherals: make(map[string]OS2LPeripheral), + saved: make(map[string]*OS2LPeripheral), } } @@ -28,37 +37,92 @@ func (f *OS2LFinder) Initialize() error { return nil } +// OnArrival is the callback function when a new peripheral arrives +func (f *OS2LFinder) OnArrival(cb func(p PeripheralInfo)) { + f.onArrival = cb +} + +// OnRemoval i the callback when a peripheral goes away +func (f *OS2LFinder) OnRemoval(cb func(p PeripheralInfo)) { + f.onRemoval = cb +} + // RegisterPeripheral registers a new peripheral func (f *OS2LFinder) RegisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) (string, error) { - // Create a random serial number for this peripheral - peripheralData.SerialNumber = strings.ToUpper(fmt.Sprintf("%08x", rand.Intn(1<<32))) - log.Trace().Str("file", "OS2LFinder").Str("serialNumber", peripheralData.SerialNumber).Msg("OS2L peripheral created") + f.mu.Lock() + defer f.mu.Unlock() - os2lPeripheral, err := NewOS2LPeripheral(peripheralData) + // If the SerialNumber is empty, generate another one + if peripheralData.SerialNumber == "" { + peripheralData.SerialNumber = strings.ToUpper(fmt.Sprintf("%08x", rand.Intn(1<<32))) + } + + peripheral, err := NewOS2LPeripheral(peripheralData) if err != nil { return "", fmt.Errorf("unable to create the OS2L peripheral: %v", err) } - // Connect this peripheral - err = os2lPeripheral.Connect(ctx) - if err != nil { - return "", err + f.saved[peripheralData.SerialNumber] = peripheral + log.Trace().Str("file", "OS2LFinder").Str("serialNumber", peripheralData.SerialNumber).Msg("OS2L peripheral created") + + // New OS2L peripheral has arrived + if f.onArrival != nil { + f.onArrival(peripheral.GetInfo()) } - f.registeredPeripherals[peripheralData.SerialNumber] = *os2lPeripheral - log.Trace().Any("periph", &os2lPeripheral).Str("file", "OS2LFinder").Str("peripheralName", peripheralData.Name).Msg("OS2L peripheral has been created") + f.wg.Add(1) + go func() { + defer f.wg.Done() + // Connect the OS2L peripheral + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusConnecting) + err = peripheral.Connect(ctx) + if err != nil { + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) + log.Err(err).Str("file", "OS2LFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to connect the peripheral") + return + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDeactivated) + // Peripheral connected, activate it + err = peripheral.Activate(ctx) + if err != nil { + log.Err(err).Str("file", "OS2LFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to activate the OS2L peripheral") + return + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusActivated) + }() + + // Emits the event in the hardware + runtime.EventsEmit(ctx, "LOAD_PERIPHERAL", peripheralData) + return peripheralData.SerialNumber, nil } // UnregisterPeripheral unregisters an existing peripheral -func (f *OS2LFinder) UnregisterPeripheral(peripheralID string) error { - peripheral, registered := f.registeredPeripherals[peripheralID] - if registered { - err := peripheral.Disconnect() +func (f *OS2LFinder) UnregisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) error { + f.mu.Lock() + defer f.mu.Unlock() + + if peripheral, detected := f.saved[peripheralData.SerialNumber]; detected { + // Deactivating peripheral + err := peripheral.Deactivate(ctx) if err != nil { - return err + log.Err(err).Str("sn", peripheralData.SerialNumber).Msg("unable to deactivate the peripheral") + return nil } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDeactivated) + // Disconnecting peripheral + err = peripheral.Disconnect() + if err != nil { + log.Err(err).Str("sn", peripheralData.SerialNumber).Msg("unable to disconnect the peripheral") + return nil + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) } - delete(f.registeredPeripherals, peripheralID) + + // The OS2L peripheral has gone + f.onRemoval(peripheralData) + + delete(f.saved, peripheralData.SerialNumber) + runtime.EventsEmit(ctx, "UNLOAD_PERIPHERAL", peripheralData) return nil } @@ -70,7 +134,7 @@ func (f *OS2LFinder) GetName() string { // GetPeripheralSettings gets the peripheral settings func (f *OS2LFinder) GetPeripheralSettings(peripheralID string) (map[string]interface{}, error) { // Return the specified peripheral - peripheral, found := f.registeredPeripherals[peripheralID] + peripheral, found := f.saved[peripheralID] if !found { log.Error().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the OS2L finder") return nil, fmt.Errorf("unable to found the peripheral") @@ -82,7 +146,7 @@ func (f *OS2LFinder) GetPeripheralSettings(peripheralID string) (map[string]inte // SetPeripheralSettings sets the peripheral settings func (f *OS2LFinder) SetPeripheralSettings(peripheralID string, settings map[string]interface{}) error { // Return the specified peripheral - peripheral, found := f.registeredPeripherals[peripheralID] + peripheral, found := f.saved[peripheralID] if !found { log.Error().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") return fmt.Errorf("unable to found the peripheral") @@ -93,11 +157,35 @@ func (f *OS2LFinder) SetPeripheralSettings(peripheralID string, settings map[str // Start starts the finder func (f *OS2LFinder) Start(ctx context.Context) error { + // No peripherals to scan here return nil } -// Stop stops this finder -func (f *OS2LFinder) Stop() error { +// WaitStop stops the finder +func (f *OS2LFinder) WaitStop() error { + log.Trace().Str("file", "OS2LFinder").Msg("stopping the OS2L finder...") + + // Close the channel + // close(f.scanChannel) + + // Wait for all the peripherals to close + log.Trace().Str("file", "OS2LFinder").Msg("closing all OS2L peripherals") + var errs []error + for registeredPeripheralSN, registeredPeripheral := range f.saved { + err := registeredPeripheral.WaitStop() + if err != nil { + errs = append(errs, fmt.Errorf("%s: %w", registeredPeripheralSN, err)) + } + } + + // Waiting internal tasks + f.wg.Wait() + + // Returning errors + if len(errs) > 0 { + return errors.Join(errs...) + } + log.Trace().Str("file", "OS2LFinder").Msg("OS2L finder stopped") return nil } diff --git a/hardware/OS2LPeripheral.go b/hardware/OS2LPeripheral.go index 4bc9703..76f472c 100644 --- a/hardware/OS2LPeripheral.go +++ b/hardware/OS2LPeripheral.go @@ -6,7 +6,6 @@ import ( "time" "github.com/rs/zerolog/log" - "github.com/wailsapp/wails/v2/pkg/runtime" ) // OS2LPeripheral contains the data of an OS2L peripheral @@ -29,11 +28,9 @@ func NewOS2LPeripheral(peripheralData PeripheralInfo) (*OS2LPeripheral, error) { // Connect connects the MIDI peripheral func (p *OS2LPeripheral) Connect(ctx context.Context) error { log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral connected") - go func() { - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.info, "connecting") - time.Sleep(5 * time.Second) - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.info, "disconnected") - }() + + time.Sleep(5 * time.Second) + return nil } @@ -102,3 +99,8 @@ func (p *OS2LPeripheral) GetSettings() map[string]interface{} { func (p *OS2LPeripheral) GetInfo() PeripheralInfo { return p.info } + +// WaitStop stops the peripheral +func (p *OS2LPeripheral) WaitStop() error { + return nil +} diff --git a/hardware/interfaces.go b/hardware/interfaces.go index d48b3fe..e27d216 100644 --- a/hardware/interfaces.go +++ b/hardware/interfaces.go @@ -5,12 +5,12 @@ import "context" // Peripheral represents the methods used to manage a peripheral (input or output hardware) type Peripheral interface { Connect(context.Context) error // Connect the peripheral - IsConnected() bool // Return if the peripheral is connected or not Disconnect() error // Disconnect the peripheral Activate(context.Context) error // Activate the peripheral Deactivate(context.Context) error // Deactivate the peripheral SetSettings(map[string]interface{}) error // Set a peripheral setting SetDeviceProperty(context.Context, uint32, byte) error // Update a device property + WaitStop() error // Properly close the peripheral GetInfo() PeripheralInfo // Get the peripheral information GetSettings() map[string]interface{} // Get the peripheral settings @@ -18,13 +18,10 @@ type Peripheral interface { // PeripheralInfo represents a peripheral information type PeripheralInfo struct { - Name string `yaml:"name"` // Name of the peripheral - SerialNumber string `yaml:"sn"` // S/N of the peripheral - ProtocolName string `yaml:"protocol"` // Protocol name of the peripheral - // IsConnected bool // If the peripheral is connected to the system - // IsActivated bool // If the peripheral is activated in the project - // IsDetected bool // If the peripheral is detected by the system - Settings map[string]interface{} `yaml:"settings"` // Peripheral settings + Name string `yaml:"name"` // Name of the peripheral + SerialNumber string `yaml:"sn"` // S/N of the peripheral + ProtocolName string `yaml:"protocol"` // Protocol name of the peripheral + Settings map[string]interface{} `yaml:"settings"` // Peripheral settings } // PeripheralFinder represents how compatible peripheral drivers are implemented