Compare commits

...

9 Commits
v0.2 ... main

10 changed files with 458 additions and 109 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
config.yaml config.yaml
midi-request-trigger midi-request-trigger
midi-request-trigger.log

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM golang:1.20
# Build app
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
RUN go build -o /midi-request-trigger
WORKDIR /app
RUN rm -Rf /app; mkdir /etc/midi-request-trigger
# Configuration volume
VOLUME ["/etc/midi-request-trigger"]
# Command
CMD ["/midi-request-trigger"]

View File

@ -70,7 +70,11 @@ On MacOS, you can setup a Launch Agent in `~/Library/LaunchAgents/com.mrgeckosme
<false/> <false/>
</dict> </dict>
</plist> </plist>
```
For local network connection, you need to code sign your build.
```bash
codesign -s - --force --deep /path/to/bin/midi-request-trigger
``` ```
Start with: Start with:
@ -110,7 +114,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
debug_listener: true log_level: 2
``` ```
### Example note trigger configuration ### Example note trigger configuration
@ -120,7 +124,7 @@ midi_routers:
midi_routers: midi_routers:
- name: service_notifications - name: service_notifications
device: IAC Driver Bus 1 device: IAC Driver Bus 1
debug_listener: true log_level: 2
note_triggers: note_triggers:
- channel: 0 - channel: 0
note: 0 note: 0
@ -136,7 +140,7 @@ midi_routers:
midi_routers: midi_routers:
- name: service_notifications - name: service_notifications
device: IAC Driver Bus 1 device: IAC Driver Bus 1
debug_listener: true log_level: 2
request_triggers: request_triggers:
- channel: 0 - channel: 0
note: 0 note: 0
@ -152,7 +156,7 @@ midi_routers:
midi_routers: midi_routers:
- name: service_notifications - name: service_notifications
device: IAC Driver Bus 1 device: IAC Driver Bus 1
debug_listener: true log_level: 3
note_triggers: note_triggers:
- channel: 0 - channel: 0
note: 0 note: 0
@ -174,5 +178,85 @@ midi_routers:
headers: headers:
Content-Type: Content-Type:
- multipart/form-data; boundary=---------------------------888832887744 - multipart/form-data; boundary=---------------------------888832887744
debug_request: true ```
### Example mqtt config
```yaml
---
midi_routers:
- name: Wing Midi Signals
device: WING Port 4
mqtt:
host: 10.0.0.2
port: 1883
client_id: midi_mqtt_bridge
user: mqtt
password: password
topic: midi/behringer_wing
log_level: 4
note_triggers:
- channel: 0
note: 1
match_all_velocities: true
mqtt_topic: osc/behringer_wing/send/$ctl/user/2/2/enc/val
mqtt_payload:
- "1"
- channel: 0
note: 2
match_all_velocities: true
mqtt_topic: osc/behringer_wing/send/$ctl/user/2/2/enc/val
mqtt_payload:
- "2"
- channel: 0
note: 3
match_all_velocities: true
mqtt_topic: osc/behringer_wing/send/$ctl/user/2/2/enc/val
mqtt_payload:
- "3"
- channel: 0
note: 4
match_all_velocities: true
mqtt_topic: osc/behringer_wing/send/$ctl/user/2/2/enc/val
mqtt_payload:
- "4"
- channel: 0
note: 5
match_all_velocities: true
mqtt_topic: osc/behringer_wing/send/$ctl/user/2/2/enc/val
mqtt_payload:
- "5"
- channel: 0
note: 6
match_all_velocities: true
mqtt_topic: osc/behringer_wing/send/$ctl/user/2/2/enc/val
mqtt_payload:
- "6"
- channel: 0
note: 7
match_all_velocities: true
mqtt_topic: osc/behringer_wing/send/$ctl/user/2/2/enc/val
mqtt_payload:
- "7"
- channel: 0
note: 8
match_all_velocities: true
mqtt_topic: osc/behringer_wing/send/$ctl/user/2/2/enc/val
mqtt_payload:
- "8"
- channel: 0
match_all_notes: true
match_all_velocities: true
delay_before: 200ms
mqtt_topic: osc/behringer_wing/send/$ctl/user/2/2/bu/val
mqtt_payload:
- "1"
- channel: 0
match_all_notes: true
match_all_velocities: true
delay_before: 200ms
mqtt_topic: osc/behringer_wing/send/$ctl/user/2/2/bu/val
mqtt_payload:
- "0"
delay_after: 200ms
``` ```

131
config.go
View File

@ -1,13 +1,17 @@
package main package main
import ( import (
"log" "fmt"
"io"
"os" "os"
"os/user" "os/user"
"path" "path"
"path/filepath" "path/filepath"
"runtime"
"github.com/kkyr/fig" "github.com/kkyr/fig"
log "github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
) )
// Configurations relating to HTTP server. // Configurations relating to HTTP server.
@ -19,9 +23,127 @@ type HTTPConfig struct {
Enabled bool `fig:"enabled"` Enabled bool `fig:"enabled"`
} }
// Configuration for logging.
type LogConfig struct {
// Limit the log output by the log level.
Level string `fig:"level" yaml:"level" enum:"debug,info,warn,error" default:"info"`
// How should the log output be formatted.
Type string `fig:"type" yaml:"type" enum:"json,console" default:"console"`
// The outputs that the log should go to. Output of `console` will
// go to the stderr. An file path, will log to the file. Using `default-file`
// it'll either save to `/var/log/name.log`, or to the same directory as the
// executable if the path is not writable, or on Windows.
Outputs []string `fig:"outputs" yaml:"outputs" default:"console,default-file"`
// Maximum size of the log file in megabytes before it gets rotated.
MaxSize int `fig:"max_size" yaml:"max_size" default:"1"`
// Maximum number of backups to save.
MaxBackups int `fig:"max_backups" yaml:"max_backups" default:"3"`
// Maximum number of days to retain old log files.
MaxAge int `fig:"max_age" yaml:"max_age" default:"0"`
// Use the logal system time instead of UTC for file names of rotated backups.
LocalTime *bool `fig:"local_time" yaml:"local_time" default:"true"`
// Should the rotated logs be compressed.
Compress *bool `fig:"compress" yaml:"compress" default:"true"`
}
// Apply log config.
func (l *LogConfig) Apply() {
// Apply level.
switch l.Level {
case "debug":
log.SetLevel(log.DebugLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "warn":
log.SetLevel(log.WarnLevel)
default:
log.SetLevel(log.ErrorLevel)
}
// Apply type.
switch l.Type {
case "json":
log.SetFormatter(&log.JSONFormatter{})
default:
log.SetFormatter(&log.TextFormatter{})
}
// Change the outputs.
var outputs []io.Writer
for _, output := range l.Outputs {
// If output is console, add stderr and continue.
if output == "console" {
outputs = append(outputs, os.Stderr)
continue
}
// If default-file defined, find the default file.
if output == "default-file" {
var f *os.File
var err error
var logDir, logPath string
logName := fmt.Sprintf("%s.log", serviceName)
// On *nix, `/var/log/` should be default if writable.
if runtime.GOOS != "windows" {
logDir = "/var/log"
logPath = filepath.Join(logDir, logName)
// Verify we can write to log file.
f, err = os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
}
// If we could not open the file, then we should try the executable path.
if err != nil || f == nil {
exe, err := os.Executable()
if err != nil {
log.Println("Unable to find an writable log path to save log to.")
continue
} else {
logDir = filepath.Dir(exe)
logPath = filepath.Join(logDir, logName)
// Verify we can write to log file.
f, err = os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Println("Unable to find an writable log path to save log to.")
continue
} else {
f.Close()
}
}
} else {
// Close file.
f.Close()
}
// Update the config log path.
output = logPath
}
// Setup lumberjack log rotate for the output, and add to the list.
logFile := &lumberjack.Logger{
Filename: output,
MaxSize: l.MaxSize,
MaxBackups: l.MaxBackups,
MaxAge: l.MaxAge,
LocalTime: *l.LocalTime,
Compress: *l.Compress,
}
outputs = append(outputs, logFile)
}
// If there are outputs, set the outputs.
if len(outputs) != 0 {
mw := io.MultiWriter(outputs...)
log.SetOutput(mw)
}
}
// Configuration Structure. // Configuration Structure.
type Config struct { type Config struct {
HTTP HTTPConfig `fig:"http"` HTTP HTTPConfig `fig:"http"`
Log *LogConfig `fig:"log" yaml:"log"`
MidiRouters []*MidiRouter `fig:"midi_routers"` MidiRouters []*MidiRouter `fig:"midi_routers"`
} }
@ -48,7 +170,7 @@ func (a *App) ReadConfig() {
} else if _, err := os.Stat(etcConfig); err == nil { } else if _, err := os.Stat(etcConfig); err == nil {
configFile = etcConfig configFile = etcConfig
} else { } else {
log.Fatal("Unable to find a configuration file.") log.Println("Unable to find a configuration file.")
} }
// Load the configuration file. // Load the configuration file.
@ -59,6 +181,7 @@ func (a *App) ReadConfig() {
Debug: true, Debug: true,
Enabled: false, Enabled: false,
}, },
Log: &LogConfig{},
} }
// Load configuration. // Load configuration.
@ -68,6 +191,7 @@ func (a *App) ReadConfig() {
fig.Dirs(filePath), fig.Dirs(filePath),
) )
if err != nil { if err != nil {
app.config = config
log.Printf("Error parsing configuration: %s\n", err) log.Printf("Error parsing configuration: %s\n", err)
return return
} }
@ -80,6 +204,9 @@ func (a *App) ReadConfig() {
config.HTTP.Port = app.flags.HTTPPort config.HTTP.Port = app.flags.HTTPPort
} }
// Apply log configs.
config.Log.Apply()
// Set global config structure. // Set global config structure.
app.config = config app.config = config
} }

26
go.mod
View File

@ -1,21 +1,27 @@
module github.com/GRMrGecko/midi-request-trigger module github.com/GRMrGecko/midi-request-trigger
go 1.20 go 1.24.2
toolchain go1.24.4
require ( require (
github.com/eclipse/paho.mqtt.golang v1.5.0 github.com/eclipse/paho.mqtt.golang v1.5.0
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.1
github.com/kkyr/fig v0.3.2 github.com/kkyr/fig v0.5.0
gitlab.com/gomidi/midi/v2 v2.0.30 github.com/sirupsen/logrus v1.9.3
gitlab.com/gomidi/midi/v2 v2.3.14
gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
require ( require (
github.com/felixge/httpsnoop v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
golang.org/x/net v0.27.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

38
go.sum
View File

@ -1,26 +1,64 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kkyr/fig v0.3.2 h1:+vMj52FL6RJUxeKOBB6JXIMyyi1/2j1ERDrZXjoBjzM= github.com/kkyr/fig v0.3.2 h1:+vMj52FL6RJUxeKOBB6JXIMyyi1/2j1ERDrZXjoBjzM=
github.com/kkyr/fig v0.3.2/go.mod h1:ItUILF8IIzgZOMhx5xpJ1W/bviQsWRKOwKXfE/tqUoA= github.com/kkyr/fig v0.3.2/go.mod h1:ItUILF8IIzgZOMhx5xpJ1W/bviQsWRKOwKXfE/tqUoA=
github.com/kkyr/fig v0.5.0 h1:D4ym5MYYScOSgqyx1HYQaqFn9dXKzIuSz8N6SZ4rzqM=
github.com/kkyr/fig v0.5.0/go.mod h1:U4Rq/5eUNJ8o5UvOEc9DiXtNf41srOLn2r/BfCyuc58=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gitlab.com/gomidi/midi/v2 v2.0.30 h1:RgRYbQeQSab5ZaP1lqRcCTnTSBQroE3CE6V9HgMmOAc= gitlab.com/gomidi/midi/v2 v2.0.30 h1:RgRYbQeQSab5ZaP1lqRcCTnTSBQroE3CE6V9HgMmOAc=
gitlab.com/gomidi/midi/v2 v2.0.30/go.mod h1:Y6IFFyABN415AYsFMPJb0/43TRIuVYDpGKp2gDYLTLI= gitlab.com/gomidi/midi/v2 v2.0.30/go.mod h1:Y6IFFyABN415AYsFMPJb0/43TRIuVYDpGKp2gDYLTLI=
gitlab.com/gomidi/midi/v2 v2.3.14 h1:BbTDExFlg0zm90AtyGDdO87jdKjn+eYqeSlSGGpFPzQ=
gitlab.com/gomidi/midi/v2 v2.3.14/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -4,13 +4,13 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log"
"net" "net"
"net/http" "net/http"
"os" "os"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
) )
// Basic HTTP server structure. // Basic HTTP server structure.

View File

@ -3,11 +3,11 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
log "github.com/sirupsen/logrus"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
) )
@ -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" serviceVersion = "0.4.1"
) )
// 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

@ -5,14 +5,15 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
log "github.com/sirupsen/logrus"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
"gitlab.com/gomidi/midi/v2/drivers" "gitlab.com/gomidi/midi/v2/drivers"
) )
@ -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.
@ -70,20 +73,25 @@ type MQTTPayload struct {
// Triggers that occur from MIDI messages received. // Triggers that occur from MIDI messages received.
type NoteTrigger struct { type NoteTrigger struct {
// If set, every note played will be matched.
MatchAllNotes bool `fig:"match_all_notes"`
// Channel to match. // Channel to match.
Channel uint8 `fig:"channel"` Channel uint8 `fig:"channel"`
// If we should match all channel values.
MatchAllChannels bool `fig:"match_all_channels"`
// Note to match. // Note to match.
Note uint8 `fig:"note"` Note uint8 `fig:"note"`
// If we should match all note values.
MatchAllNotes bool `fig:"match_all_notes"`
// Velocity to match. // Velocity to match.
Velocity uint8 `fig:"velocity"` Velocity uint8 `fig:"velocity"`
// If we should match all velocity values. // If we should match all velocity values.
MatchAllVelocities bool `fig:"match_all_velocities"` MatchAllVelocities bool `fig:"match_all_velocities"`
// Allow delaying the request.
DelayBefore time.Duration `fig:"delay_before"`
DelayAfter time.Duration `fig:"deplay_after"`
// Custom MQTT message. Do not set to ignore MQTT. // Custom MQTT message. Do not set to ignore MQTT.
MqttTopic string `fig:"mqtt_topic"` MqttTopic string `fig:"mqtt_topic"`
// Nil payload will generate a payload with midi info. // Nil payload will generate a payload with midi info.
MqttPayload []interface{} `fig:"mqtt_payload"` MqttPayload interface{} `fig:"mqtt_payload"`
// If the HTTP request should includ midi info. // If the HTTP request should includ midi info.
MidiInfoInRequest bool `fig:"midi_info_in_request"` MidiInfoInRequest bool `fig:"midi_info_in_request"`
// Should SSL requests require a valid certificate. // Should SSL requests require a valid certificate.
@ -120,7 +128,7 @@ type RequestTrigger struct {
type MidiRouter struct { type MidiRouter struct {
// Used for human readable config. // Used for human readable config.
Name string `fig:"name"` Name string `fig:"name"`
// Midi device to connect. // Midi device to connect, accepts regular expression.
Device string `fig:"device"` Device string `fig:"device"`
// MQTT Connection if you are to integrate with MQTT. // MQTT Connection if you are to integrate with MQTT.
MQTT MQTTConfig `fig:"mqtt"` MQTT MQTTConfig `fig:"mqtt"`
@ -132,18 +140,19 @@ 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 and OSC receive logging. // 1 - Errors
// 2 - MQTT and OSC send logging. // 2 - MQTT, HTTP, and MIDI receive logging.
// 3 - Debug // 3 - MQTT, HTTP, and MIDI send logging.
LogLevel LogLevel `yaml:"log_level" json:"log_level"` // 4 - Debug
LogLevel LogLevel `fig:"log_level"`
// Connection to MIDI device. // Connection to MIDI device.
MidiOut drivers.Out `fig:"-"` MidiOut drivers.Out `fig:"-"`
// Function to stop listening to MIDI device. // Function to stop listening to MIDI device.
ListenerStop func() `fig:"-"` ListenerStop func() `fig:"-"`
// The client connection to MQTT. // The client connection to MQTT.
MqttClient mqtt.Client `yaml:"-" json:"-"` MqttClient mqtt.Client `fig:"-"`
} }
// Logging function to allow log levels. // Logging function to allow log levels.
@ -156,7 +165,7 @@ func (r *MidiRouter) Log(level LogLevel, format string, args ...interface{}) {
// When a MIDI message occurs, send the HTTP request. // When a MIDI message occurs, send the HTTP request.
func (r *MidiRouter) sendRequest(channel, note, velocity uint8) { func (r *MidiRouter) sendRequest(channel, note, velocity uint8) {
// If MQTT firehose not disabled, send to general cmd topic. // If MQTT firehose not disabled, send to general cmd topic.
if r.MQTT.Host != "" && r.MQTT.Port != 0 && !r.MQTT.DisableMidiFirehose { if r.MqttClient != nil && !r.MQTT.DisableMidiFirehose {
payload := MQTTPayload{ payload := MQTTPayload{
Channel: channel, Channel: channel,
Note: note, Note: note,
@ -168,7 +177,7 @@ func (r *MidiRouter) sendRequest(channel, note, velocity uint8) {
} else { } else {
topic := r.MQTT.Topic + "/cmd" topic := r.MQTT.Topic + "/cmd"
r.MqttClient.Publish(topic, 0, true, data) r.MqttClient.Publish(topic, 0, true, data)
r.Log(SendLog, "-> [MQTT] %s", topic) r.Log(SendLog, "-> [MQTT] %s: %s", topic, string(data))
} }
} }
@ -177,15 +186,26 @@ func (r *MidiRouter) sendRequest(channel, note, velocity uint8) {
// If match all notes, process this request. // If match all notes, process this request.
// If not, check if channel, note, and velocity matches. // If not, check if channel, note, and velocity matches.
// The velocity may be defined to accept all. // The velocity may be defined to accept all.
if trig.MatchAllNotes || (trig.Channel == channel && trig.Note == note && (trig.Velocity == velocity || trig.MatchAllVelocities)) { if (trig.Channel == channel || trig.MatchAllChannels) && (trig.Note == note || trig.MatchAllNotes) && (trig.Velocity == velocity || trig.MatchAllVelocities) {
// For all logging, we want to print the message so setup a common string to print. // For all logging, we want to print the message so setup a common string to print.
logInfo := fmt.Sprintf("note %s(%d) on channel %v with velocity %v", midi.Note(note), note, channel, velocity) logInfo := fmt.Sprintf("note %s(%d) on channel %v with velocity %v", midi.Note(note), note, channel, velocity)
if trig.MqttTopic != "" { // Delay before.
time.Sleep(trig.DelayBefore)
// If MQTT trigger, send the MQTT request.
if trig.MqttTopic != "" && r.MqttClient != nil {
// If payload provided, send the defined payload.
if trig.MqttPayload != nil { if trig.MqttPayload != nil {
r.MqttClient.Publish(trig.MqttTopic, 0, true, trig.MqttPayload) data, err := json.Marshal(trig.MqttPayload)
r.Log(SendLog, "-> [MQTT] %s", trig.MqttTopic) if err != nil {
r.Log(ErrorLog, "Json Encode: %s", err)
} else {
r.MqttClient.Publish(trig.MqttTopic, 0, true, data)
r.Log(SendLog, "-> [MQTT] %s: %s", trig.MqttTopic, string(data))
}
} else { } else {
// If no payload provided, send the note information as JSON.
payload := MQTTPayload{ payload := MQTTPayload{
Channel: channel, Channel: channel,
Note: note, Note: note,
@ -196,11 +216,12 @@ func (r *MidiRouter) sendRequest(channel, note, velocity uint8) {
r.Log(ErrorLog, "Json Encode: %s", err) r.Log(ErrorLog, "Json Encode: %s", err)
} else { } else {
r.MqttClient.Publish(trig.MqttTopic, 0, true, data) r.MqttClient.Publish(trig.MqttTopic, 0, true, data)
r.Log(SendLog, "-> [MQTT] %s", trig.MqttTopic) r.Log(SendLog, "-> [MQTT] %s: %s", trig.MqttTopic, string(data))
} }
} }
} }
// If URL trigger defined, perform a HTTP request.
if trig.URL != "" { if trig.URL != "" {
// Default method to GET if nothing is defined. // Default method to GET if nothing is defined.
if trig.Method == "" { if trig.Method == "" {
@ -269,6 +290,9 @@ func (r *MidiRouter) sendRequest(channel, note, velocity uint8) {
r.Log(DebugLog, "Trigger response: %s\n%s", logInfo, string(body)) r.Log(DebugLog, "Trigger response: %s\n%s", logInfo, string(body))
} }
} }
// Delay after.
time.Sleep(trig.DelayAfter)
} }
} }
} }
@ -455,81 +479,131 @@ 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 { deviceRx, err := regexp.Compile(r.Device)
log.Println("Can't find output device:", r.Device) if err != nil {
} else { log.Printf("Failed to compile regexp of '%s': %v", r.Device, err)
r.MidiOut = out }
} for {
var out drivers.Out
for _, device := range midi.GetOutPorts() {
if deviceRx.MatchString(device.String()) {
err = device.Open()
out = device
}
}
if out == nil {
err = fmt.Errorf("unable to find matching device")
}
if err != nil {
r.Log(ErrorLog, "Failed to find output device '%s': %v", r.Device, err)
} 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() {
deviceRx, err := regexp.Compile(r.Device)
if err != nil {
log.Printf("Failed to compile regexp of '%s': %v", r.Device, err)
}
for {
// Try finding input port.
r.Log(InfoLog, "Connecting to input device: %s", r.Device)
var in drivers.In
for _, device := range midi.GetInPorts() {
if deviceRx.MatchString(device.String()) {
err = device.Open()
in = device
}
}
if in == nil {
err = fmt.Errorf("unable to find matching device")
}
if err != nil {
r.Log(ErrorLog, "Can't find input device '%s': %v", r.Device, err)
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)
}
}
} }
} }
@ -539,4 +613,7 @@ func (r *MidiRouter) Disconnect() {
if r.ListenerStop != nil { if r.ListenerStop != nil {
r.ListenerStop() r.ListenerStop()
} }
if r.MqttClient != nil {
r.MqttClient.Disconnect(0)
}
} }