From 1b636980597133097e30a368b6f3a9eec04270a2 Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Wed, 12 Nov 2025 13:42:38 +0100 Subject: [PATCH 1/8] 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 -- 2.49.1 From d730cf4f1c1bbcde7202705705461a31bf7999c8 Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Thu, 13 Nov 2025 11:39:27 +0100 Subject: [PATCH 2/8] implement OS2L protocol --- hardware/OS2LPeripheral.go | 123 +++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 5 deletions(-) diff --git a/hardware/OS2LPeripheral.go b/hardware/OS2LPeripheral.go index 76f472c..2ab48cb 100644 --- a/hardware/OS2LPeripheral.go +++ b/hardware/OS2LPeripheral.go @@ -2,17 +2,34 @@ package hardware import ( "context" + "encoding/json" "fmt" + "net" + "sync" "time" "github.com/rs/zerolog/log" ) +// OS2LMessage represents an OS2L message +type OS2LMessage struct { + Event string `json:"evt"` + Name string `json:"name"` + State string `json:"state"` + ID int64 `json:"id"` + Param float64 `json:"param"` +} + // OS2LPeripheral contains the data of an OS2L peripheral type OS2LPeripheral struct { + wg sync.WaitGroup + info PeripheralInfo // The basic info for this peripheral serverIP string // OS2L server IP serverPort int // OS2L server port + + listener net.Listener // Net listener (TCP) + listenerCancel context.CancelFunc // Call this function to cancel the peripheral activation } // NewOS2LPeripheral creates a new OS2L peripheral @@ -21,33 +38,126 @@ func NewOS2LPeripheral(peripheralData PeripheralInfo) (*OS2LPeripheral, error) { return &OS2LPeripheral{ info: peripheralData, serverIP: "127.0.0.1", - serverPort: 9005, + serverPort: 9995, + listener: nil, }, nil } // Connect connects the MIDI peripheral func (p *OS2LPeripheral) Connect(ctx context.Context) error { + var err error + addr := net.TCPAddr{Port: p.serverPort, IP: net.ParseIP(p.serverIP)} + p.listener, err = net.ListenTCP("tcp", &addr) + if err != nil { + return fmt.Errorf("unable to set the OS2L TCP listener") + } + log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral connected") + return nil +} - time.Sleep(5 * time.Second) - +func handleMessage(raw []byte) error { + message := OS2LMessage{} + err := json.Unmarshal(raw, &message) + if err != nil { + return fmt.Errorf("Unable to parse the OS2L message: %w", err) + } + // Display the message + fmt.Printf("Event: %s, Name: %s, State: %s, ID: %d, Param: %f\n", message.Event, message.Name, message.State, message.ID, message.Param) return nil } // Disconnect disconnects the MIDI peripheral func (p *OS2LPeripheral) Disconnect() error { + + // Close the TCP listener if not null + if p.listener != nil { + p.listener.Close() + } + log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral disconnected") return nil } -// Activate activates the MIDI peripheral +// Activate activates the OS2L peripheral func (p *OS2LPeripheral) Activate(ctx context.Context) error { + // Create a derived context to handle deactivation + var listenerCtx context.Context + listenerCtx, p.listenerCancel = context.WithCancel(ctx) + + if p.listener == nil { + return fmt.Errorf("the listener isn't defined") + } + + p.wg.Add(1) + go func() { + defer p.wg.Done() + + for { + select { + case <-listenerCtx.Done(): + return + default: + p.listener.(*net.TCPListener).SetDeadline(time.Now().Add(1 * time.Second)) + conn, err := p.listener.Accept() + if err != nil { + if ne, ok := err.(net.Error); ok && ne.Timeout() { + continue + } + log.Err(err).Str("file", "OS2LPeripheral").Msg("unable to accept the connection") + continue + } + + // Every client is handled in a dedicated goroutine + p.wg.Add(1) + go func(c net.Conn) { + defer p.wg.Done() + defer c.Close() + + buffer := make([]byte, 1024) + for { + select { + case <-listenerCtx.Done(): + return + default: + c.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) + n, err := c.Read(buffer) + if err != nil { + if ne, ok := err.(net.Error); ok && ne.Timeout() { + // Lecture a expiré → vérifier si le contexte est annulé + select { + case <-listenerCtx.Done(): + return + default: + continue // pas annulé → relancer Read + } + } + return // autre erreur ou EOF + } + + handleMessage(buffer[:n]) + } + } + }(conn) + } + } + }() log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral activated") + return nil } -// Deactivate deactivates the MIDI peripheral +// Deactivate deactivates the OS2L peripheral func (p *OS2LPeripheral) Deactivate(ctx context.Context) error { + if p.listener == nil { + return fmt.Errorf("the listener isn't defined") + } + + // Cancel listener by context + if p.listenerCancel != nil { + p.listenerCancel() + } + log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral deactivated") return nil } @@ -102,5 +212,8 @@ func (p *OS2LPeripheral) GetInfo() PeripheralInfo { // WaitStop stops the peripheral func (p *OS2LPeripheral) WaitStop() error { + log.Info().Str("file", "OS2LPeripheral").Str("s/n", p.info.SerialNumber).Msg("waiting for OS2L peripheral to close...") + p.wg.Wait() + log.Info().Str("file", "OS2LPeripheral").Str("s/n", p.info.SerialNumber).Msg("OS2L peripheral closed!") return nil } -- 2.49.1 From 18a3a716ef37f62791d43570d106f5860f1fe240 Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Thu, 13 Nov 2025 13:01:06 +0100 Subject: [PATCH 3/8] show event signal on the interface --- .../src/components/Settings/DeviceCard.svelte | 2 +- .../Settings/InputsOutputsContent.svelte | 2 +- frontend/src/runtime-events.js | 32 +++++++++++++++++ frontend/src/stores.js | 3 +- hardware/OS2LFinder.go | 6 ++++ hardware/OS2LPeripheral.go | 35 ++++++++++++------- hardware/hardware.go | 4 ++- hardware/interfaces.go | 1 + 8 files changed, 69 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/Settings/DeviceCard.svelte b/frontend/src/components/Settings/DeviceCard.svelte index 8c72cb1..a9620ee 100644 --- a/frontend/src/components/Settings/DeviceCard.svelte +++ b/frontend/src/components/Settings/DeviceCard.svelte @@ -53,7 +53,7 @@
- +
diff --git a/frontend/src/components/Settings/InputsOutputsContent.svelte b/frontend/src/components/Settings/InputsOutputsContent.svelte index 403ac56..2da0fe1 100644 --- a/frontend/src/components/Settings/InputsOutputsContent.svelte +++ b/frontend/src/components/Settings/InputsOutputsContent.svelte @@ -104,7 +104,7 @@ {#each Object.entries($peripherals) as [serialNumber, peripheral]} {#if peripheral.isSaved} removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)} on:click={() => selectPeripheral(peripheral)} - title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} selected={serialNumber == selectedPeripheralSN} removable signalizable/> + title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} selected={serialNumber == selectedPeripheralSN} removable signalizable signalized={peripheral.eventEmitted}/> {/if} {/each} {:else} diff --git a/frontend/src/runtime-events.js b/frontend/src/runtime-events.js index 59a290b..d509a83 100644 --- a/frontend/src/runtime-events.js +++ b/frontend/src/runtime-events.js @@ -120,6 +120,32 @@ function unloadPeripheral (peripheralInfo) { needProjectSave.set(true) } +// A peripheral event has been emitted +function onPeripheralEvent(sn, event) { + // If not exists, add it to the map + // eventEmitted key to true for 0.2 sec + + peripherals.update((storedPeripherals) => { + return { + ...storedPeripherals, + [sn]: { + ...storedPeripherals[sn], + eventEmitted: true + }, + }}) + + setTimeout(() => { + peripherals.update((storedPeripherals) => { + return { + ...storedPeripherals, + [sn]: { + ...storedPeripherals[sn], + eventEmitted: false + }, + }}) + }, 200); +} + let initialized = false export function initRuntimeEvents(){ @@ -143,6 +169,9 @@ export function initRuntimeEvents(){ // Handle a peripheral unloaded from the project EventsOn('UNLOAD_PERIPHERAL', unloadPeripheral) + + // Handle a peripheral event + EventsOn('PERIPHERAL_EVENT_EMITTED', onPeripheralEvent) } export function destroyRuntimeEvents(){ @@ -166,4 +195,7 @@ export function destroyRuntimeEvents(){ // Handle a peripheral unloaded from the project EventsOff('UNLOAD_PERIPHERAL') + + // Handle a peripheral event + EventsOff('PERIPHERAL_EVENT_EMITTED') } \ No newline at end of file diff --git a/frontend/src/stores.js b/frontend/src/stores.js index 02aeb53..984a346 100644 --- a/frontend/src/stores.js +++ b/frontend/src/stores.js @@ -45,4 +45,5 @@ export let peripherals = writable({}) // isSaved // if the peripheral is saved in the project // isDetected // if the peripheral is detected by the system -// status // the status of connection \ No newline at end of file +// status // the status of connection +// eventEmitted // if an event has been emitted for this peripheral (disappear after a delay) \ No newline at end of file diff --git a/hardware/OS2LFinder.go b/hardware/OS2LFinder.go index 8974ac3..16b65c5 100644 --- a/hardware/OS2LFinder.go +++ b/hardware/OS2LFinder.go @@ -57,10 +57,16 @@ func (f *OS2LFinder) RegisterPeripheral(ctx context.Context, peripheralData Peri peripheralData.SerialNumber = strings.ToUpper(fmt.Sprintf("%08x", rand.Intn(1<<32))) } + // Create a new OS2L peripheral peripheral, err := NewOS2LPeripheral(peripheralData) if err != nil { return "", fmt.Errorf("unable to create the OS2L peripheral: %v", err) } + // Set the event callback + peripheral.SetEventCallback(func(event any) { + runtime.EventsEmit(ctx, string(PeripheralEventEmitted), peripheralData.SerialNumber, event) + }) + f.saved[peripheralData.SerialNumber] = peripheral log.Trace().Str("file", "OS2LFinder").Str("serialNumber", peripheralData.SerialNumber).Msg("OS2L peripheral created") diff --git a/hardware/OS2LPeripheral.go b/hardware/OS2LPeripheral.go index 2ab48cb..c67f971 100644 --- a/hardware/OS2LPeripheral.go +++ b/hardware/OS2LPeripheral.go @@ -24,25 +24,32 @@ type OS2LMessage struct { type OS2LPeripheral struct { wg sync.WaitGroup - info PeripheralInfo // The basic info for this peripheral - serverIP string // OS2L server IP - serverPort int // OS2L server port - + info PeripheralInfo // The basic info for this peripheral + serverIP string // OS2L server IP + serverPort int // OS2L server port listener net.Listener // Net listener (TCP) listenerCancel context.CancelFunc // Call this function to cancel the peripheral activation + + eventCallback func(any) // This callback is called for returning events } // NewOS2LPeripheral creates a new OS2L peripheral func NewOS2LPeripheral(peripheralData PeripheralInfo) (*OS2LPeripheral, error) { log.Trace().Str("file", "OS2LPeripheral").Str("name", peripheralData.Name).Str("s/n", peripheralData.SerialNumber).Msg("OS2L peripheral created") return &OS2LPeripheral{ - info: peripheralData, - serverIP: "127.0.0.1", - serverPort: 9995, - listener: nil, + info: peripheralData, + serverIP: "127.0.0.1", + serverPort: 9995, + listener: nil, + eventCallback: nil, }, nil } +// SetEventCallback sets the callback for returning events +func (p *OS2LPeripheral) SetEventCallback(eventCallback func(any)) { + p.eventCallback = eventCallback +} + // Connect connects the MIDI peripheral func (p *OS2LPeripheral) Connect(ctx context.Context) error { var err error @@ -56,14 +63,18 @@ func (p *OS2LPeripheral) Connect(ctx context.Context) error { return nil } -func handleMessage(raw []byte) error { +// handleMessage handles an OS2L message +func (p *OS2LPeripheral) handleMessage(raw []byte) error { message := OS2LMessage{} err := json.Unmarshal(raw, &message) if err != nil { return fmt.Errorf("Unable to parse the OS2L message: %w", err) } - // Display the message - fmt.Printf("Event: %s, Name: %s, State: %s, ID: %d, Param: %f\n", message.Event, message.Name, message.State, message.ID, message.Param) + log.Debug().Str("event", message.Event).Str("name", message.Name).Str("state", message.State).Int("ID", int(message.ID)).Float64("param", message.Param).Msg("OS2L event received") + // Return the event to the finder + if p.eventCallback != nil { + go p.eventCallback(message) + } return nil } @@ -135,7 +146,7 @@ func (p *OS2LPeripheral) Activate(ctx context.Context) error { return // autre erreur ou EOF } - handleMessage(buffer[:n]) + p.handleMessage(buffer[:n]) } } }(conn) diff --git a/hardware/hardware.go b/hardware/hardware.go index 466f4b4..81ba457 100644 --- a/hardware/hardware.go +++ b/hardware/hardware.go @@ -21,8 +21,10 @@ const ( PeripheralArrival PeripheralEvent = "PERIPHERAL_ARRIVAL" // PeripheralRemoval is triggered when a peripheral has been disconnected from the system PeripheralRemoval PeripheralEvent = "PERIPHERAL_REMOVAL" - // PeripheralStatusUpdated is triggered when a peripheral status has been updated (disconnected - connecting - connected) + // PeripheralStatusUpdated is triggered when a peripheral status has been updated (disconnected - connecting - deactivated - activated) PeripheralStatusUpdated PeripheralEvent = "PERIPHERAL_STATUS" + // PeripheralEventEmitted is triggered when a peripheral event is emitted + PeripheralEventEmitted PeripheralEvent = "PERIPHERAL_EVENT_EMITTED" // PeripheralStatusDisconnected : peripheral is now disconnected PeripheralStatusDisconnected PeripheralStatus = "PERIPHERAL_DISCONNECTED" // PeripheralStatusConnecting : peripheral is now connecting diff --git a/hardware/interfaces.go b/hardware/interfaces.go index e27d216..0b31397 100644 --- a/hardware/interfaces.go +++ b/hardware/interfaces.go @@ -5,6 +5,7 @@ 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 + SetEventCallback(func(any)) // Callback is called when an event is emitted from the peripheral Disconnect() error // Disconnect the peripheral Activate(context.Context) error // Activate the peripheral Deactivate(context.Context) error // Deactivate the peripheral -- 2.49.1 From c7d9f35080a5083be562465c61095d38676dd8f9 Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Thu, 13 Nov 2025 16:00:50 +0100 Subject: [PATCH 4/8] apply settings for OS2L --- hardware/FTDIFinder.go | 2 +- hardware/OS2LFinder.go | 19 +++--- hardware/OS2LPeripheral.go | 124 +++++++++++++++++++++++++++---------- hardware/interfaces.go | 38 ++++++------ peripherals.go | 11 ++-- project.go | 2 +- 6 files changed, 124 insertions(+), 72 deletions(-) diff --git a/hardware/FTDIFinder.go b/hardware/FTDIFinder.go index c82951c..7e3e0cb 100644 --- a/hardware/FTDIFinder.go +++ b/hardware/FTDIFinder.go @@ -180,7 +180,7 @@ func (f *FTDIFinder) GetPeripheralSettings(peripheralID string) (map[string]inte } // SetPeripheralSettings sets the peripheral settings -func (f *FTDIFinder) SetPeripheralSettings(peripheralID string, settings map[string]interface{}) error { +func (f *FTDIFinder) SetPeripheralSettings(ctx context.Context, peripheralID string, settings map[string]any) error { // Return the specified peripheral // peripheral, found := f.registeredPeripherals[peripheralID] // if !found { diff --git a/hardware/OS2LFinder.go b/hardware/OS2LFinder.go index 16b65c5..26d534d 100644 --- a/hardware/OS2LFinder.go +++ b/hardware/OS2LFinder.go @@ -60,7 +60,7 @@ func (f *OS2LFinder) RegisterPeripheral(ctx context.Context, peripheralData Peri // Create a new OS2L peripheral peripheral, err := NewOS2LPeripheral(peripheralData) if err != nil { - return "", fmt.Errorf("unable to create the OS2L peripheral: %v", err) + return "", fmt.Errorf("unable to create the OS2L peripheral: %w", err) } // Set the event callback peripheral.SetEventCallback(func(event any) { @@ -79,21 +79,17 @@ func (f *OS2LFinder) RegisterPeripheral(ctx context.Context, peripheralData Peri 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 @@ -114,14 +110,12 @@ func (f *OS2LFinder) UnregisterPeripheral(ctx context.Context, peripheralData Pe 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() + err = peripheral.Disconnect(ctx) 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) } // The OS2L peripheral has gone @@ -138,7 +132,7 @@ func (f *OS2LFinder) GetName() string { } // GetPeripheralSettings gets the peripheral settings -func (f *OS2LFinder) GetPeripheralSettings(peripheralID string) (map[string]interface{}, error) { +func (f *OS2LFinder) GetPeripheralSettings(peripheralID string) (map[string]any, error) { // Return the specified peripheral peripheral, found := f.saved[peripheralID] if !found { @@ -150,15 +144,16 @@ 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 { +func (f *OS2LFinder) SetPeripheralSettings(ctx context.Context, peripheralID string, settings map[string]any) error { // Return the specified peripheral 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") } - log.Debug().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") - return peripheral.SetSettings(settings) + + // Set the peripheral settings + return peripheral.SetSettings(ctx, settings) } // Start starts the finder diff --git a/hardware/OS2LPeripheral.go b/hardware/OS2LPeripheral.go index c67f971..ee17343 100644 --- a/hardware/OS2LPeripheral.go +++ b/hardware/OS2LPeripheral.go @@ -5,10 +5,12 @@ import ( "encoding/json" "fmt" "net" + "strings" "sync" "time" "github.com/rs/zerolog/log" + "github.com/wailsapp/wails/v2/pkg/runtime" ) // OS2LMessage represents an OS2L message @@ -35,14 +37,13 @@ type OS2LPeripheral struct { // NewOS2LPeripheral creates a new OS2L peripheral func NewOS2LPeripheral(peripheralData PeripheralInfo) (*OS2LPeripheral, error) { - log.Trace().Str("file", "OS2LPeripheral").Str("name", peripheralData.Name).Str("s/n", peripheralData.SerialNumber).Msg("OS2L peripheral created") - return &OS2LPeripheral{ + peripheral := &OS2LPeripheral{ info: peripheralData, - serverIP: "127.0.0.1", - serverPort: 9995, listener: nil, eventCallback: nil, - }, nil + } + log.Trace().Str("file", "OS2LPeripheral").Str("name", peripheralData.Name).Str("s/n", peripheralData.SerialNumber).Msg("OS2L peripheral created") + return peripheral, peripheral.loadSettings(peripheralData.Settings) } // SetEventCallback sets the callback for returning events @@ -50,15 +51,21 @@ func (p *OS2LPeripheral) SetEventCallback(eventCallback func(any)) { p.eventCallback = eventCallback } -// Connect connects the MIDI peripheral +// Connect connects the OS2L peripheral func (p *OS2LPeripheral) Connect(ctx context.Context) error { + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusConnecting) + var err error addr := net.TCPAddr{Port: p.serverPort, IP: net.ParseIP(p.serverIP)} + + log.Debug().Any("addr", addr).Msg("parametres de connexion à la connexion") p.listener, err = net.ListenTCP("tcp", &addr) if err != nil { + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusDisconnected) return fmt.Errorf("unable to set the OS2L TCP listener") } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusDeactivated) log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral connected") return nil } @@ -78,14 +85,49 @@ func (p *OS2LPeripheral) handleMessage(raw []byte) error { return nil } +// loadSettings check and load the settings in the peripheral +func (p *OS2LPeripheral) loadSettings(settings map[string]any) error { + // Check if the IP exists + serverIP, found := settings["os2lIp"] + if !found { + // Set default IP address + serverIP = "127.0.0.1" + } + // Check if it is a string + ipSetting, ok := serverIP.(string) + if ok { + p.serverIP = ipSetting + } else { + return fmt.Errorf("The specified IP is not a string") + } + // Check if the port exists + serverPort, found := settings["os2lPort"] + if !found { + // Set default port + serverPort = 9995 + } + switch v := serverPort.(type) { + case int: + p.serverPort = v + case float64: + p.serverPort = int(v) // JSON numbers are float64 + default: + return fmt.Errorf("The specified port is not a number, got %T", serverPort) + } + + return nil +} + // Disconnect disconnects the MIDI peripheral -func (p *OS2LPeripheral) Disconnect() error { +func (p *OS2LPeripheral) Disconnect(ctx context.Context) error { // Close the TCP listener if not null if p.listener != nil { p.listener.Close() } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusDisconnected) + log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral disconnected") return nil } @@ -115,6 +157,9 @@ func (p *OS2LPeripheral) Activate(ctx context.Context) error { if ne, ok := err.(net.Error); ok && ne.Timeout() { continue } + if strings.Contains(err.Error(), "use of closed network connection") || strings.Contains(err.Error(), "invalid argument") { + return + } log.Err(err).Str("file", "OS2LPeripheral").Msg("unable to accept the connection") continue } @@ -153,6 +198,8 @@ func (p *OS2LPeripheral) Activate(ctx context.Context) error { } } }() + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusActivated) + log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral activated") return nil @@ -169,37 +216,46 @@ func (p *OS2LPeripheral) Deactivate(ctx context.Context) error { p.listenerCancel() } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusDeactivated) + log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral deactivated") return nil } // SetSettings sets a specific setting for this peripheral -func (p *OS2LPeripheral) SetSettings(settings map[string]interface{}) error { - // Check if the IP exists - serverIP, found := settings["os2lIp"] - if !found { - return fmt.Errorf("Unable to find the OS2L server IP") - } - // Check if it is a string - ipSetting, ok := serverIP.(string) - if ok { - p.serverIP = ipSetting - - } else { - return fmt.Errorf("The specified IP is not a string") - } - // Check if the port exists - serverPort, found := settings["os2lPort"] - if !found { - return fmt.Errorf("Unable to find the OS2L server port") - } - // Check if it is a float and convert to int - portFloat, ok := serverPort.(float64) - if ok { - p.serverPort = int(portFloat) - } else { - return fmt.Errorf("The specified port is not an int") +func (p *OS2LPeripheral) SetSettings(ctx context.Context, settings map[string]any) error { + err := p.loadSettings(settings) + if err != nil { + return fmt.Errorf("unable to load settings: %w", err) } + // Reconnect the peripheral + p.wg.Add(1) + go func() { + defer p.wg.Done() + err := p.Deactivate(ctx) + if err != nil { + log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to deactivate") + return + } + err = p.Disconnect(ctx) + if err != nil { + log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to disconnect") + return + } + // Add a sleep to view changes + time.Sleep(500 * time.Millisecond) + err = p.Connect(ctx) + if err != nil { + log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to connect") + return + } + err = p.Activate(ctx) + if err != nil { + log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to activate") + return + } + }() + log.Info().Str("sn", p.GetInfo().SerialNumber).Msg("peripheral settings set") return nil } @@ -209,8 +265,8 @@ func (p *OS2LPeripheral) SetDeviceProperty(context.Context, uint32, uint32, byte } // GetSettings gets the peripheral settings -func (p *OS2LPeripheral) GetSettings() map[string]interface{} { - return map[string]interface{}{ +func (p *OS2LPeripheral) GetSettings() map[string]any { + return map[string]any{ "os2lIp": p.serverIP, "os2lPort": p.serverPort, } diff --git a/hardware/interfaces.go b/hardware/interfaces.go index 0b31397..6c9ea82 100644 --- a/hardware/interfaces.go +++ b/hardware/interfaces.go @@ -6,36 +6,36 @@ import "context" type Peripheral interface { Connect(context.Context) error // Connect the peripheral SetEventCallback(func(any)) // Callback is called when an event is emitted from the peripheral - Disconnect() error // Disconnect the peripheral + Disconnect(context.Context) 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 + SetSettings(context.Context, map[string]any) 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 + GetInfo() PeripheralInfo // Get the peripheral information + GetSettings() map[string]any // Get the peripheral settings } // 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 - 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]any `yaml:"settings"` // Peripheral settings } // PeripheralFinder represents how compatible peripheral drivers are implemented type PeripheralFinder interface { - Initialize() error // Initializes the protocol - OnArrival(cb func(p PeripheralInfo)) // Callback function when a peripheral arrives - OnRemoval(cb func(p PeripheralInfo)) // Callback function when a peripheral goes away - Start(context.Context) error // Start the detection - WaitStop() error // Waiting for finder to close - ForceScan() // Explicitly scans for peripherals - RegisterPeripheral(context.Context, PeripheralInfo) (string, error) // Registers a new peripheral data - UnregisterPeripheral(context.Context, PeripheralInfo) error // Unregisters an existing peripheral - GetPeripheralSettings(string) (map[string]interface{}, error) // Gets the peripheral settings - SetPeripheralSettings(string, map[string]interface{}) error // Sets the peripheral settings - GetName() string // Get the name of the finder + Initialize() error // Initializes the protocol + OnArrival(cb func(p PeripheralInfo)) // Callback function when a peripheral arrives + OnRemoval(cb func(p PeripheralInfo)) // Callback function when a peripheral goes away + Start(context.Context) error // Start the detection + WaitStop() error // Waiting for finder to close + ForceScan() // Explicitly scans for peripherals + RegisterPeripheral(context.Context, PeripheralInfo) (string, error) // Registers a new peripheral data + UnregisterPeripheral(context.Context, PeripheralInfo) error // Unregisters an existing peripheral + GetPeripheralSettings(string) (map[string]any, error) // Gets the peripheral settings + SetPeripheralSettings(context.Context, string, map[string]any) error // Sets the peripheral settings + GetName() string // Get the name of the finder } diff --git a/peripherals.go b/peripherals.go index 91ae570..f9fb409 100644 --- a/peripherals.go +++ b/peripherals.go @@ -18,9 +18,10 @@ func (a *App) AddPeripheral(peripheralData hardware.PeripheralInfo) (string, err // Register this new peripheral serialNumber, err := f.RegisterPeripheral(a.ctx, peripheralData) if err != nil { - log.Trace().Str("file", "peripheral").Str("protocolName", peripheralData.ProtocolName).Str("periphID", serialNumber).Msg("device registered to the finder") - return "", fmt.Errorf("unable to register the peripheral '%s'", serialNumber) + return "", fmt.Errorf("unable to register the peripheral '%s': %w", serialNumber, err) } + log.Trace().Str("file", "peripheral").Str("protocolName", peripheralData.ProtocolName).Str("periphID", serialNumber).Msg("device registered to the finder") + // Rewrite the serialnumber for virtual devices peripheralData.SerialNumber = serialNumber @@ -35,7 +36,7 @@ func (a *App) AddPeripheral(peripheralData hardware.PeripheralInfo) (string, err } // GetPeripheralSettings gets the peripheral settings -func (a *App) GetPeripheralSettings(protocolName, peripheralID string) (map[string]interface{}, error) { +func (a *App) GetPeripheralSettings(protocolName, peripheralID string) (map[string]any, error) { // Get the peripheral from its finder f, err := a.hardwareManager.GetFinder(protocolName) if err != nil { @@ -46,7 +47,7 @@ func (a *App) GetPeripheralSettings(protocolName, peripheralID string) (map[stri } // UpdatePeripheralSettings updates a specific setting of a peripheral -func (a *App) UpdatePeripheralSettings(protocolName, peripheralID string, settings map[string]interface{}) error { +func (a *App) UpdatePeripheralSettings(protocolName, peripheralID string, settings map[string]any) error { // Sets the settings with the finder f, err := a.hardwareManager.GetFinder(protocolName) if err != nil { @@ -61,7 +62,7 @@ func (a *App) UpdatePeripheralSettings(protocolName, peripheralID string, settin pInfo.Settings = settings a.projectInfo.PeripheralsInfo[peripheralID] = pInfo // Apply changes in the peripheral - return f.SetPeripheralSettings(peripheralID, pInfo.Settings) + return f.SetPeripheralSettings(a.ctx, peripheralID, pInfo.Settings) } // RemovePeripheral removes a peripheral from the project diff --git a/project.go b/project.go index af7a157..9b9954d 100644 --- a/project.go +++ b/project.go @@ -136,7 +136,7 @@ func (a *App) OpenProject(projectInfo ProjectInfo) error { } _, err = hostFinder.RegisterPeripheral(a.ctx, value) if err != nil { - return fmt.Errorf("unable to register the peripheral S/N '%s'", key) + return fmt.Errorf("unable to register the peripheral S/N '%s': %w", key, err) } } return nil -- 2.49.1 From 4600ac66457974102af30236992084028b214e11 Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Thu, 13 Nov 2025 16:11:50 +0100 Subject: [PATCH 5/8] ftdi: responsability changes --- hardware/FTDIFinder.go | 50 +++++++++++++------------------------- hardware/FTDIPeripheral.go | 14 +++++++++-- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/hardware/FTDIFinder.go b/hardware/FTDIFinder.go index 7e3e0cb..bad2ef9 100644 --- a/hardware/FTDIFinder.go +++ b/hardware/FTDIFinder.go @@ -60,27 +60,24 @@ func (f *FTDIFinder) RegisterPeripheral(ctx context.Context, peripheralData Peri defer f.mu.Unlock() f.saved[peripheralData.SerialNumber] = peripheralData + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) // If already detected, connect it if peripheral, ok := f.detected[peripheralData.SerialNumber]; ok { 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) }() } @@ -102,14 +99,12 @@ func (f *FTDIFinder) UnregisterPeripheral(ctx context.Context, peripheralData Pe 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() + err = peripheral.Disconnect(ctx) 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.saved, peripheralData.SerialNumber) runtime.EventsEmit(ctx, "UNLOAD_PERIPHERAL", peripheralData) @@ -169,27 +164,25 @@ func (f *FTDIFinder) GetName() string { // GetPeripheralSettings gets the peripheral settings func (f *FTDIFinder) GetPeripheralSettings(peripheralID string) (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 nil, fmt.Errorf("unable to found the peripheral") - // } - // log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") - // return peripheral.GetSettings(), nil - return make(map[string]interface{}), nil + peripheral, found := f.detected[peripheralID] + if !found { + log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") + return nil, fmt.Errorf("unable to found the peripheral") + } + log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") + return peripheral.GetSettings(), nil } // SetPeripheralSettings sets the peripheral settings func (f *FTDIFinder) SetPeripheralSettings(ctx context.Context, peripheralID string, settings map[string]any) 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) - return nil + peripheral, found := f.detected[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) } // scanPeripherals scans the FTDI peripherals @@ -242,28 +235,21 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { } log.Info().Str("sn", sn).Str("name", peripheralData.Name).Msg("[FTDI] New peripheral detected") - // Disconnected by default - runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) - // If the peripheral is saved in the project => connect if _, saved := f.saved[sn]; saved { 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) } } @@ -278,12 +264,10 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { if err != nil { 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() + err = oldPeripheral.Disconnect(ctx) 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) // Delete it from the detected list delete(f.detected, sn) diff --git a/hardware/FTDIPeripheral.go b/hardware/FTDIPeripheral.go index 8debace..ac9a088 100644 --- a/hardware/FTDIPeripheral.go +++ b/hardware/FTDIPeripheral.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/wailsapp/wails/v2/pkg/runtime" ) /* @@ -41,6 +42,8 @@ func (p *FTDIPeripheral) Connect(ctx context.Context) error { return errors.Errorf("the DMX device has already been created!") } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusConnecting) + // Create the DMX sender p.dmxSender = C.dmx_create() @@ -48,6 +51,7 @@ func (p *FTDIPeripheral) Connect(ctx context.Context) error { serialNumber := C.CString(p.info.SerialNumber) defer C.free(unsafe.Pointer(serialNumber)) if C.dmx_connect(p.dmxSender, serialNumber) != C.DMX_OK { + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusDisconnected) log.Error().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("unable to connect the DMX device") return errors.Errorf("unable to connect '%s'", p.info.SerialNumber) } @@ -56,15 +60,16 @@ func (p *FTDIPeripheral) Connect(ctx context.Context) error { go func() { defer p.wg.Done() <-ctx.Done() - _ = p.Disconnect() + _ = p.Disconnect(ctx) }() + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusDeactivated) log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device connected successfully") return nil } // Disconnect disconnects the FTDI peripheral -func (p *FTDIPeripheral) Disconnect() error { +func (p *FTDIPeripheral) Disconnect(ctx context.Context) error { // Check if the device has already been created if p.dmxSender == nil { return errors.Errorf("the DMX device has not been connected!") @@ -75,6 +80,8 @@ func (p *FTDIPeripheral) Disconnect() error { // Reset the pointer to the peripheral p.dmxSender = nil + + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusDisconnected) return nil } @@ -96,6 +103,8 @@ func (p *FTDIPeripheral) Activate(ctx context.Context) error { C.dmx_setValue(p.dmxSender, C.uint16_t(1), C.uint8_t(255)) C.dmx_setValue(p.dmxSender, C.uint16_t(5), C.uint8_t(255)) + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusActivated) + log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device activated successfully") return nil } @@ -114,6 +123,7 @@ func (p *FTDIPeripheral) Deactivate(ctx context.Context) error { return errors.Errorf("unable to deactivate the DMX sender!") } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusDeactivated) log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device deactivated successfully") return nil } -- 2.49.1 From b579bb743ab2a8e4117958e3a0f2594eb10fea3c Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Fri, 14 Nov 2025 11:23:19 +0100 Subject: [PATCH 6/8] fixed scroll feature in the devices library --- frontend/src/components/General/Tab.svelte | 9 +-- .../src/components/Settings/DeviceCard.svelte | 45 +++++++------ .../Settings/InputsOutputsContent.svelte | 63 ++++++++++--------- .../src/components/Settings/Settings.svelte | 2 +- frontend/src/lang/en.json | 9 +-- 5 files changed, 62 insertions(+), 66 deletions(-) diff --git a/frontend/src/components/General/Tab.svelte b/frontend/src/components/General/Tab.svelte index 7304396..26ffc44 100644 --- a/frontend/src/components/General/Tab.svelte +++ b/frontend/src/components/General/Tab.svelte @@ -15,15 +15,14 @@
-
+
{#each tabs as tab, index} - setActiveTab(index)}/> + setActiveTab(index)}/> {/each}
+ style='background-color: {$colors.first}; max-width: {maxWidth};'> {#if tabs[activeTab]} {/if} @@ -41,13 +40,11 @@ } .headerContainer{ padding: 0.1em; - margin-bottom: 1em; border-radius: 0.5em; } .bodyContainer{ padding: 0.5em; background-color: red; border-radius: 0.5em; - overflow:auto; } \ No newline at end of file diff --git a/frontend/src/components/Settings/DeviceCard.svelte b/frontend/src/components/Settings/DeviceCard.svelte index a9620ee..47c89e4 100644 --- a/frontend/src/components/Settings/DeviceCard.svelte +++ b/frontend/src/components/Settings/DeviceCard.svelte @@ -38,7 +38,7 @@
-
+

{#if status == "PERIPHERAL_DISCONNECTED" } {/if}{title}

{type} {location != '' ? "- " : ""}{location}
@@ -52,7 +52,7 @@
- +
@@ -63,19 +63,33 @@ background: linear-gradient(to bottom right, var(--second-color), var(--third-color)); } .card{ - position: relative; + position: relative; } .selected { - background-color: var(--first-color); + background-color: var(--third-color); + position: relative; margin: 0.2em; - padding-left: 0.3em; - padding-bottom: 0.3em; - border-radius: 0.2em; + padding: 0.2em 0.3em 0.5em 0.5em; + border-radius: 0.5em; display: flex; justify-content: space-between; text-align: left; cursor: pointer; + overflow: hidden; } + .unselected{ + background-color: var(--second-color); + position: relative; + margin: 0.2em; + padding: 0.2em 0.3em 0.5em 0.5em; + border-radius: 0.5em; + display: flex; + justify-content: space-between; + text-align: left; + cursor: pointer; + overflow: hidden; + } + .subtitle{ margin-bottom: 0.5em; } @@ -94,7 +108,7 @@ .waiting::before{ content: ''; position: absolute; - background: linear-gradient(var(--second-color), var(--first-color)); + background: linear-gradient(var(--first-color), var(--second-color)); width: 100%; height: 60%; animation: rotate 3s linear infinite; @@ -108,21 +122,6 @@ border-radius: 0.2em; } - .unselected{ - background-color: var(--third-color); - background: fixed; - position: relative; - margin: 0.2em; - padding-left: 0.3em; - padding-bottom: 0.3em; - border-radius: 0.2em; - display: flex; - justify-content: space-between; - text-align: left; - cursor: pointer; - overflow: hidden; - } - /* Définition de l'animation */ @keyframes rotate { from { diff --git a/frontend/src/components/Settings/InputsOutputsContent.svelte b/frontend/src/components/Settings/InputsOutputsContent.svelte index 2da0fe1..f67b102 100644 --- a/frontend/src/components/Settings/InputsOutputsContent.svelte +++ b/frontend/src/components/Settings/InputsOutputsContent.svelte @@ -1,9 +1,8 @@
-
-

{$_("projectHardwareAvailableLabel")}

+
-

{$_("projectHardwareDetectedLabel")}

- {#each Object.entries($peripherals) as [serialNumber, peripheral]} - {#if peripheral.isDetected} - addPeripheral(peripheral)} on:dblclick={() => { - if(!peripheral.isSaved) - addPeripheral(peripheral) - }} - status="PERIPHERAL_CONNECTED" title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={"S/N: " + peripheral.SerialNumber} addable={!peripheral.isSaved}/> - {/if} - {/each} -

{$_("projectHardwareOthersLabel")}

- addPeripheral({Name: "OS2L connection", ProtocolName: "OS2L", SerialNumber: ""})} text="Add an OS2L peripheral" icon="bx-plus-circle" tooltip="Configure an OS2L connection"/> +

{$_("projectHardwareDetectedLabel")}

+ {#each Object.entries($peripherals) as [serialNumber, peripheral]} + {#if peripheral.isDetected} + addPeripheral(peripheral)} on:dblclick={() => { + if(!peripheral.isSaved) + addPeripheral(peripheral) + }} + status="PERIPHERAL_CONNECTED" title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={"S/N: " + peripheral.SerialNumber} addable={!peripheral.isSaved}/> + {/if} + {/each} +
+

{$_("projectHardwareOthersLabel")}

+
+ addPeripheral({Name: "OS2L virtual device", ProtocolName: "OS2L", SerialNumber: ""})} + on:dblclick={() => addPeripheral({Name: "OS2L virtual device", ProtocolName: "OS2L", SerialNumber: ""})} + status="PERIPHERAL_CONNECTED" title={"OS2L virtual device"} type={"OS2L"} location={""} addable={true}/>
@@ -111,7 +113,6 @@ {$_("projectHardwareEmptyLabel")} {/if}
-

{$_("projectHardwareSettingsLabel")} ({selectedPeripheralSN == null ? $_("projectHardwareNoSelection") : selectedPeripheralSN})

{#if Object.keys(selectedPeripheralSettings).length > 0} {#each Object.entries(selectedPeripheralSettings) as [settingName, settingValue]} @@ -119,8 +120,6 @@ validate(settingName, event.detail.target.value)} label={$t(settingName)} type="{typeof(settingValue)}" width='100%' value="{settingValue}"/>
{/each} - {:else} - {$_("projectHardwareNoSettingLabel")} {/if}
@@ -149,19 +148,23 @@ border-radius: 0.5em; padding: 0.2em; max-height: calc(100vh - 300px); - overflow-y: auto; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE and Edge */ - /* overflow: visible; */ + margin-top: 0.5em; + margin-bottom: 0.5em; } - .availableHardware::-webkit-scrollbar { - display: none; - } - .configuredHardware { - background-color: var(--second-color); - border-radius: 0.5em; + + .libraryPanel { padding: 0.5em; - padding: 0.2em; + /* width: 13em; */ + height: calc(100vh - 2*8px - 2*4px - 40px - 1em - 2*0.1em - 2*0.1em - 2*0.4em - 21.6px - 2*0.5em); + overflow: auto; + scrollbar-width: none; + -ms-overflow-style: none; + } + .libraryPanel::-webkit-scrollbar { + display: none; /* Chrome, Safari, Edge */ + } + + .configuredHardware { display: flex; flex-wrap: wrap; } diff --git a/frontend/src/components/Settings/Settings.svelte b/frontend/src/components/Settings/Settings.svelte index 9625ecb..a1fb30d 100644 --- a/frontend/src/components/Settings/Settings.svelte +++ b/frontend/src/components/Settings/Settings.svelte @@ -12,7 +12,7 @@ - + \ No newline at end of file diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index f185eb7..cebc1b4 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -34,16 +34,13 @@ "projectHardwareShowLabel" : "My Show", "projectHardwareInputsLabel": "INPUTS", "projectHardwareOutputsLabel": "OUTPUTS", - "projectHardwareDeleteTooltip": "Delete this peripheral", - "projectHardwareAddTooltip": "Add this peripheral to project", + "projectHardwareDeleteTooltip": "Delete", + "projectHardwareAddTooltip": "Add to project", "projectHardwareNoSelection": "Empty", - "projectHardwareAvailableLabel": "Available peripherals", - "projectHardwareSavedLabel": "Project peripherals", + "projectHardwareSavedLabel": "Saved in project", "projectHardwareDetectedLabel": "Detected", "projectHardwareOthersLabel": "Others", "projectHardwareEmptyLabel": "No hardware saved for this project", - "projectHardwareSettingsLabel": "Peripheral settings", - "projectHardwareNoSettingLabel": "No setting can be displayed", "peripheralArrivalToast": "Peripheral inserted:", "peripheralRemovalToast": "Peripheral removed:", -- 2.49.1 From a688420e431733e11535257d536ef22de1561b9c Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Fri, 14 Nov 2025 11:33:41 +0100 Subject: [PATCH 7/8] fixed: no project situation in dropdown --- frontend/src/App.svelte | 1 - .../src/components/General/RoundDropdownList.svelte | 10 +++++++--- frontend/src/lang/en.json | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index c17324c..e294f62 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -7,7 +7,6 @@ import Settings from './components/Settings/Settings.svelte'; import Devices from './components/Devices/Devices.svelte'; import Show from './components/Show/Show.svelte'; - import DropdownList from "./components/General/DropdownList.svelte"; import RoundDropdownList from "./components/General/RoundDropdownList.svelte"; import GeneralConsole from './components/Console/GeneralConsole.svelte'; import RoundIconButton from './components/General/RoundIconButton.svelte'; diff --git a/frontend/src/components/General/RoundDropdownList.svelte b/frontend/src/components/General/RoundDropdownList.svelte index 80f618f..e0d8d00 100644 --- a/frontend/src/components/General/RoundDropdownList.svelte +++ b/frontend/src/components/General/RoundDropdownList.svelte @@ -101,9 +101,13 @@
- {#each Array.from(choices) as [key, value]} -
handleclick({key})}>{value}
- {/each} + {#if choices.size != 0} + {#each Array.from(choices) as [key, value]} +
handleclick({key})}>{value}
+ {/each} + {:else} +
{$_("openProjectEmpty")}
+ {/if}
diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index cebc1b4..f153022 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -13,6 +13,7 @@ "newProjectTooltip": "Create a new project", "openProjectString": "Open", "openProjectTooltip": "Open an existing project", + "openProjectEmpty": "No project found", "unsavedProjectFlag": "unsaved", "projectPropertiesTab": "Project properties", "projectPropertiesTooltip": "The project properties", -- 2.49.1 From 5b1372826f051209fd4ddbb7356d03065d9072ec Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Fri, 14 Nov 2025 11:44:54 +0100 Subject: [PATCH 8/8] fixed: FTDI get settings when disconnected --- frontend/src/App.svelte | 1 - hardware/FTDIFinder.go | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index e294f62..31d9e32 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -133,7 +133,6 @@