package hardware import ( "context" "fmt" "regexp" "strconv" "strings" "sync" "time" "github.com/mattrtaylor/go-rtmidi" "github.com/rs/zerolog/log" ) // MIDIFinder represents how the protocol is defined type MIDIFinder struct { findTicker time.Ticker // Peripherals find ticker peripherals map[string]Peripheral // The list of peripherals scanChannel chan struct{} // The channel to trigger a scan event goWait sync.WaitGroup // Check goroutines execution } // NewMIDIFinder creates a new DMXUSB protocol func NewMIDIFinder(findPeriod time.Duration) *MIDIFinder { log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder created") return &MIDIFinder{ findTicker: *time.NewTicker(findPeriod), peripherals: make(map[string]Peripheral), } } // Initialize initializes the MIDI driver func (f *MIDIFinder) Initialize() error { log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder initialized") return nil } // Start starts the finder and search for peripherals func (f *MIDIFinder) Start(ctx context.Context) error { f.goWait.Add(1) go func() { defer f.goWait.Done() for { select { case <-ctx.Done(): return case <-f.findTicker.C: // Scan the peripherals err := f.scanPeripherals(ctx) if err != nil { log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals") } case <-f.scanChannel: // Scan the peripherals err := f.scanPeripherals(ctx) if err != nil { log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals") } } } }() return nil } // Stop stops the finder func (f *MIDIFinder) Stop() error { log.Trace().Str("file", "MIDIFinder").Msg("stopping the MIDI finder...") // Wait for goroutines to stop f.goWait.Wait() // Stop the ticker f.findTicker.Stop() log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder stopped") return nil } // GetName returns the name of the driver func (f *MIDIFinder) GetName() string { return "MIDI" } // GetPeripheral gets the peripheral that correspond to the specified ID func (f *MIDIFinder) GetPeripheral(peripheralID string) (Peripheral, bool) { // Return the specified peripheral peripheral, found := f.peripherals[peripheralID] if !found { log.Error().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral in the MIDI finder") return nil, false } log.Trace().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("MIDI peripheral found in the driver") return peripheral, true } func splitStringAndNumber(input string) (string, int, error) { // Regular expression to match the text part and the number at the end re := regexp.MustCompile(`^(.*?)(\d+)$`) matches := re.FindStringSubmatch(input) // Check if the regex found both a text part and a number if len(matches) == 3 { // matches[1]: text part (might contain trailing spaces) // matches[2]: numeric part as a string textPart := strings.TrimSpace(matches[1]) // Remove any trailing spaces from the text numberPart, err := strconv.Atoi(matches[2]) if err != nil { return "", 0, err // Return error if the number conversion fails } return textPart, numberPart, nil } // Return an error if no trailing number is found return "", 0, fmt.Errorf("no number found at the end of the string") } // ForceScan explicily asks for scanning peripherals func (f *MIDIFinder) ForceScan() { f.scanChannel <- struct{}{} } // scanPeripherals scans the MIDI peripherals func (f *MIDIFinder) scanPeripherals(ctx context.Context) error { midiPeripherals := make(map[string]Peripheral) log.Trace().Str("file", "MIDIFinder").Msg("opening MIDI scanner port...") midiScanner, err := rtmidi.NewMIDIInDefault() if err != nil { log.Err(err).Str("file", "MIDIFinder").Msg("unable to open the MIDI scanner port...") return fmt.Errorf("unable to open the MIDI scanner: %s", err) } defer midiScanner.Close() midiScanner.SetCallback(func(m rtmidi.MIDIIn, b []byte, f float64) {}) log.Trace().Str("file", "MIDIFinder").Msg("scanning MIDI peripherals...") devicesCount, err := midiScanner.PortCount() if err != nil { log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals...") return fmt.Errorf("unable to scan MIDI peripherals: %s", err) } for i := 0; i < devicesCount; i++ { portName, err := midiScanner.PortName(i) if err != nil { log.Warn().Str("file", "MIDIPeripheral").Msg("found peripheral without a correct name, set it to unknown") portName = "Unknown device 0" } // Separate data name, location, err := splitStringAndNumber(portName) if err != nil { log.Err(err).Str("file", "MIDIFinder").Str("description", portName).Msg("invalid peripheral description") return fmt.Errorf("invalid pripheral description: %s", err) } log.Info().Str("file", "MIDIFinder").Str("name", name).Int("location", location).Msg("MIDI peripheral found") // Add the peripheral to the temporary list sn := strings.ToLower(strings.Replace(name, " ", "_", -1)) midiPeripherals[sn] = NewMIDIPeripheral(name, location, sn) } // Compare with the current peripherals to detect arrivals/removals removedList, addedList := comparePeripherals(f.peripherals, midiPeripherals) // Emit the events emitPeripheralsEvents(ctx, removedList, PeripheralRemoval) log.Info().Str("file", "MIDIFinder").Msg("MIDI remove list emitted to the front") emitPeripheralsEvents(ctx, addedList, PeripheralArrival) log.Info().Str("file", "MIDIFinder").Msg("MIDI add list emitted to the front") // Store the new peripherals list f.peripherals = midiPeripherals return nil } // CreatePeripheral is not implemented here func (f *MIDIFinder) CreatePeripheral(context.Context) (Peripheral, error) { return nil, nil } // DeletePeripheral is not implemented here func (f *MIDIFinder) DeletePeripheral(serialNumber string) error { return nil }