From d730cf4f1c1bbcde7202705705461a31bf7999c8 Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Thu, 13 Nov 2025 11:39:27 +0100 Subject: [PATCH] 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 }