package hardware import ( "context" "encoding/json" "fmt" "net" "strings" "sync" "time" "github.com/rs/zerolog/log" "github.com/wailsapp/wails/v2/pkg/runtime" ) // 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 eventCallback func(any) // This callback is called for returning events } // NewOS2LPeripheral creates a new OS2L peripheral func NewOS2LPeripheral(peripheralData PeripheralInfo) (*OS2LPeripheral, error) { peripheral := &OS2LPeripheral{ info: peripheralData, listener: nil, eventCallback: 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 func (p *OS2LPeripheral) SetEventCallback(eventCallback func(any)) { p.eventCallback = eventCallback } // 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 } // 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) } 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 } // 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(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 } // 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 } 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 } // 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 } p.handleMessage(buffer[:n]) } } }(conn) } } }() runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.GetInfo(), PeripheralStatusActivated) log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral activated") return nil } // 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() } 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(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 } // SetDeviceProperty - not implemented for this kind of peripheral func (p *OS2LPeripheral) SetDeviceProperty(context.Context, uint32, byte) error { return nil } // GetSettings gets the peripheral settings func (p *OS2LPeripheral) GetSettings() map[string]any { return map[string]any{ "os2lIp": p.serverIP, "os2lPort": p.serverPort, } } // GetInfo gets the peripheral information func (p *OS2LPeripheral) GetInfo() PeripheralInfo { return p.info } // 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 }