Make connections auto-retry on failure for connections to MQTT/MIDI devices. This should improve stability with physical devices when ran as a service.

This commit is contained in:
James Coleman 2024-09-08 16:10:19 -05:00
parent 24ecc9870e
commit 1eb7440496
3 changed files with 109 additions and 80 deletions

View File

@ -110,7 +110,7 @@ On MacOS, there is an IAC Driver that can be enabled in Audio MIDI Setup.
midi_routers: midi_routers:
- name: service_notifications - name: service_notifications
device: IAC Driver Bus 1 device: IAC Driver Bus 1
log_level: 1 log_level: 2
``` ```
### Example note trigger configuration ### Example note trigger configuration
@ -120,7 +120,7 @@ midi_routers:
midi_routers: midi_routers:
- name: service_notifications - name: service_notifications
device: IAC Driver Bus 1 device: IAC Driver Bus 1
log_level: 1 log_level: 2
note_triggers: note_triggers:
- channel: 0 - channel: 0
note: 0 note: 0
@ -136,7 +136,7 @@ midi_routers:
midi_routers: midi_routers:
- name: service_notifications - name: service_notifications
device: IAC Driver Bus 1 device: IAC Driver Bus 1
log_level: 1 log_level: 2
request_triggers: request_triggers:
- channel: 0 - channel: 0
note: 0 note: 0
@ -152,7 +152,7 @@ midi_routers:
midi_routers: midi_routers:
- name: service_notifications - name: service_notifications
device: IAC Driver Bus 1 device: IAC Driver Bus 1
log_level: 2 log_level: 3
note_triggers: note_triggers:
- channel: 0 - channel: 0
note: 0 note: 0
@ -190,7 +190,7 @@ midi_routers:
user: mqtt user: mqtt
password: password password: password
topic: midi/behringer_wing topic: midi/behringer_wing
log_level: 3 log_level: 4
note_triggers: note_triggers:
- channel: 0 - channel: 0
note: 1 note: 1

View File

@ -15,7 +15,7 @@ import (
const ( const (
serviceName = "midi-request-trigger" serviceName = "midi-request-trigger"
serviceDescription = "Takes trigger MIDI messages by HTTP or MQTT requests and trigger HTTP or MQTT requests by MIDI messages" serviceDescription = "Takes trigger MIDI messages by HTTP or MQTT requests and trigger HTTP or MQTT requests by MIDI messages"
serviceVersion = "0.2.1" serviceVersion = "0.2.2"
) )
// App is the global application structure for communicating between servers and storing information. // App is the global application structure for communicating between servers and storing information.

View File

@ -11,6 +11,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
@ -21,8 +22,10 @@ import (
type LogLevel int type LogLevel int
const ( const (
// Logs only errors. // Logs info messages.
ErrorLog LogLevel = iota InfoLog LogLevel = iota
// Log only errors.
ErrorLog
// MQTT, HTTP, and MIDI receive logging. // MQTT, HTTP, and MIDI receive logging.
ReceiveLog ReceiveLog
// MQTT, HTTP, and MIDI send logging. // MQTT, HTTP, and MIDI send logging.
@ -33,7 +36,7 @@ const (
// Provides a string value for a log level. // Provides a string value for a log level.
func (l LogLevel) String() string { func (l LogLevel) String() string {
return [...]string{"Error", "Receive", "Send", "Debug"}[l] return [...]string{"Info", "Error", "Receive", "Send", "Debug"}[l]
} }
// Configurations relating to MQTT connection. // Configurations relating to MQTT connection.
@ -134,10 +137,11 @@ type MidiRouter struct {
RequestTriggers []RequestTrigger `fig:"request_triggers"` RequestTriggers []RequestTrigger `fig:"request_triggers"`
// How much logging. // How much logging.
// 0 - Errors // 0 - Info
// 1 - MQTT, HTTP, and MIDI receive logging. // 1 - Errors
// 2 - MQTT, HTTP, and MIDI send logging. // 2 - MQTT, HTTP, and MIDI receive logging.
// 3 - Debug // 3 - MQTT, HTTP, and MIDI send logging.
// 4 - Debug
LogLevel LogLevel `fig:"log_level"` LogLevel LogLevel `fig:"log_level"`
// Connection to MIDI device. // Connection to MIDI device.
@ -462,81 +466,105 @@ func (r *MidiRouter) MqttSubscribe(topic string) {
func (r *MidiRouter) Connect() { func (r *MidiRouter) Connect() {
// If request triggers defined, find the out port. // If request triggers defined, find the out port.
if len(r.RequestTriggers) != 0 { if len(r.RequestTriggers) != 0 {
out, err := midi.FindOutPort(r.Device) go func() {
if err != nil { for {
log.Println("Can't find output device:", r.Device) out, err := midi.FindOutPort(r.Device)
} else { if err != nil {
r.MidiOut = out r.Log(ErrorLog, "Can't find output device: %s", r.Device)
} } else {
r.MidiOut = out
break
}
r.Log(ErrorLog, "Retrying in 1 minute.")
time.Sleep(time.Minute)
}
}()
} }
// If listener is disabled, stop here. // If listener is disabled, stop here.
if r.DisableListener { if !r.DisableListener {
return go func() {
for {
// Try finding input port.
r.Log(InfoLog, "Connecting to input device: %s", r.Device)
in, err := midi.FindInPort(r.Device)
if err != nil {
r.Log(ErrorLog, "Can't find input device: %s", r.Device)
r.Log(ErrorLog, "Retrying in 1 minute.")
time.Sleep(time.Minute)
continue
}
// Start listening to MIDI messages.
stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) {
var channel, note, velocity uint8
switch {
// Get notes with an velocity set.
case msg.GetNoteStart(&channel, &note, &velocity):
r.Log(ReceiveLog, "starting note %s(%d) on channel %v with velocity %v", midi.Note(note), note, channel, velocity)
// Process request.
r.sendRequest(channel, note, velocity)
// If no velocity is set, an note end message is received.
case msg.GetNoteEnd(&channel, &note):
r.Log(ReceiveLog, "ending note %s(%d) on channel %v", midi.Note(note), note, channel)
// Process request.
r.sendRequest(channel, note, 0)
default:
// ignore
}
})
if err != nil {
r.Log(ErrorLog, "Error listening to device: %s", err)
r.Log(ErrorLog, "Retrying in 1 minute.")
time.Sleep(time.Minute)
continue
}
r.Log(InfoLog, "Connected to input device: %s", r.Device)
// Update stop function for disconnects.
r.ListenerStop = stop
break
}
}()
} }
// Try finding input port.
log.Println("Connecting to device:", r.Device)
in, err := midi.FindInPort(r.Device)
if err != nil {
log.Println("Can't find device:", r.Device)
return
}
// Start listening to MIDI messages.
stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) {
var channel, note, velocity uint8
switch {
// Get notes with an velocity set.
case msg.GetNoteStart(&channel, &note, &velocity):
r.Log(ReceiveLog, "starting note %s(%d) on channel %v with velocity %v", midi.Note(note), note, channel, velocity)
// Process request.
r.sendRequest(channel, note, velocity)
// If no velocity is set, an note end message is received.
case msg.GetNoteEnd(&channel, &note):
r.Log(ReceiveLog, "ending note %s(%d) on channel %v", midi.Note(note), note, channel)
// Process request.
r.sendRequest(channel, note, 0)
default:
// ignore
}
})
if err != nil {
r.Log(ErrorLog, "Error listening to device: %s", err)
return
}
// Update stop function for disconnects.
r.ListenerStop = stop
if r.MQTT.Host != "" && r.MQTT.Port != 0 { if r.MQTT.Host != "" && r.MQTT.Port != 0 {
// Connect to MQTT. go func() {
mqtt_opts := mqtt.NewClientOptions() for {
mqtt_opts.AddBroker(fmt.Sprintf("tcp://%s:%d", r.MQTT.Host, r.MQTT.Port)) // Connect to MQTT.
mqtt_opts.SetClientID(r.MQTT.ClientId) mqtt_opts := mqtt.NewClientOptions()
mqtt_opts.SetUsername(r.MQTT.User) mqtt_opts.AddBroker(fmt.Sprintf("tcp://%s:%d", r.MQTT.Host, r.MQTT.Port))
mqtt_opts.SetPassword(r.MQTT.Password) mqtt_opts.SetClientID(r.MQTT.ClientId)
r.MqttClient = mqtt.NewClient(mqtt_opts) mqtt_opts.SetUsername(r.MQTT.User)
mqtt_opts.SetPassword(r.MQTT.Password)
r.MqttClient = mqtt.NewClient(mqtt_opts)
// Connect and failures are fatal exiting service. // Connect and failures are fatal exiting service.
r.Log(DebugLog, "Connecting to MQTT") r.Log(DebugLog, "Connecting to MQTT")
if t := r.MqttClient.Connect(); t.Wait() && t.Error() != nil { if t := r.MqttClient.Connect(); t.Wait() && t.Error() != nil {
log.Fatalf("MQTT error: %s", t.Error()) log.Fatalf("MQTT error: %s", t.Error())
return r.Log(ErrorLog, "Retrying in 1 minute.")
} time.Sleep(time.Minute)
continue
}
// Subscribe to MQTT topics. // Subscribe to MQTT topics.
r.MqttSubscribe(r.MQTT.Topic + "/send") r.MqttSubscribe(r.MQTT.Topic + "/send")
r.MqttSubscribe(r.MQTT.Topic + "/status/check") r.MqttSubscribe(r.MQTT.Topic + "/status/check")
// Subscribe to command topics configured. // Subscribe to command topics configured.
for _, trig := range r.RequestTriggers { for _, trig := range r.RequestTriggers {
if trig.MqttTopic != "" { if trig.MqttTopic != "" {
r.MqttSubscribe(trig.MqttTopic) r.MqttSubscribe(trig.MqttTopic)
}
if trig.MqttSubTopic != "" {
r.MqttSubscribe(r.MQTT.Topic + "/" + trig.MqttSubTopic)
}
}
break
} }
if trig.MqttSubTopic != "" { }()
r.MqttSubscribe(r.MQTT.Topic + "/" + trig.MqttSubTopic)
}
}
} }
} }
@ -546,4 +574,5 @@ func (r *MidiRouter) Disconnect() {
if r.ListenerStop != nil { if r.ListenerStop != nil {
r.ListenerStop() r.ListenerStop()
} }
r.MqttClient.Disconnect(0)
} }