First commit
This commit is contained in:
commit
dbfc41c659
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
config.yaml
|
||||
midi-request-trigger
|
19
LICENSE
Normal file
19
LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2023 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
178
README.md
Normal file
178
README.md
Normal file
@ -0,0 +1,178 @@
|
||||
# midi-request-trigger
|
||||
|
||||
A service that triggers HTTP requests when MIDI messages are recieved and triggers MIDI messages when HTTP requests are received.
|
||||
|
||||
## Install
|
||||
|
||||
You can install either by downloading the latest binary release or by building.
|
||||
|
||||
### Building
|
||||
|
||||
Building should be as simple as running:
|
||||
|
||||
```bash
|
||||
go build
|
||||
```
|
||||
|
||||
### Running as a service
|
||||
|
||||
You are likely going to want to run the tool as a service to ensure it runs at boot and restarts in case of failures. Below is an example service config file you can place in `/etc/systemd/system/midi-request-trigger.service` on a linux system to run as a service if you install the binary in `/usr/local/bin/`.
|
||||
|
||||
```systemd
|
||||
[Unit]
|
||||
Description=MIDI Request Trigger
|
||||
After=network.target
|
||||
StartLimitIntervalSec=500
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/midi-request-trigger
|
||||
ExecReload=/bin/kill -s HUP $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Once the service file is installed, you can run the following to start it:
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl start midi-request-trigger.service
|
||||
```
|
||||
|
||||
On MacOS, you can setup a Launch Agent in `~/Library/LaunchAgents/com.mrgeckosmedia.midi-request-trigger.plist` as follows:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.mrgeckosmedia.midi-request-trigger</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/path/to/bin/midi-request-trigger</string>
|
||||
<string>-c</string>
|
||||
<string>/path/to/config.yaml</string>
|
||||
</array>
|
||||
<key>KeepAlive</key>
|
||||
<dict>
|
||||
<key>Crashed</key>
|
||||
<true/>
|
||||
<key>SuccessfulExit</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>OnDemand</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
```
|
||||
|
||||
Start with:
|
||||
```bash
|
||||
launchctl load ~/Library/LaunchAgents/com.mrgeckosmedia.midi-request-trigger.plist
|
||||
```
|
||||
|
||||
Check status with:
|
||||
```bash
|
||||
launchctl list com.mrgeckosmedia.midi-request-trigger
|
||||
```
|
||||
|
||||
Stop with:
|
||||
```bash
|
||||
launchctl unload ~/Library/LaunchAgents/com.mrgeckosmedia.midi-request-trigger.plist
|
||||
```
|
||||
|
||||
|
||||
## Config
|
||||
|
||||
The default configuration paths are:
|
||||
|
||||
- `./config.yaml` - A file in the current working directory.
|
||||
- `~/.config/midi-request-trigger/config.yaml` - A file in your home directory's config path.
|
||||
- `/etc/midi-request-trigger/config.yaml` - A file in the IPA config folder.
|
||||
|
||||
### To verify listener works
|
||||
|
||||
You can find the device name by running the following:
|
||||
```bash
|
||||
midi-request-trigger -l
|
||||
```
|
||||
|
||||
On MacOS, there is an IAC Driver that can be enabled in Audio MIDI Setup.
|
||||
```yaml
|
||||
---
|
||||
midi_routers:
|
||||
- name: service_notifications
|
||||
device: IAC Driver Bus 1
|
||||
debug_listener: true
|
||||
```
|
||||
|
||||
### Example note trigger configuration
|
||||
|
||||
```yaml
|
||||
---
|
||||
midi_routers:
|
||||
- name: service_notifications
|
||||
device: IAC Driver Bus 1
|
||||
debug_listener: true
|
||||
note_triggers:
|
||||
- channel: 0
|
||||
note: 0
|
||||
match_all_velocities: true
|
||||
url: http://example.com
|
||||
midi_info_in_request: true
|
||||
```
|
||||
|
||||
### Example request trigger configuration
|
||||
|
||||
```yaml
|
||||
---
|
||||
midi_routers:
|
||||
- name: service_notifications
|
||||
device: IAC Driver Bus 1
|
||||
debug_listener: true
|
||||
request_triggers:
|
||||
- channel: 0
|
||||
note: 0
|
||||
velocity: 1
|
||||
midi_info_in_request: true
|
||||
uri: /send_note
|
||||
```
|
||||
|
||||
### Example multi part request
|
||||
|
||||
```yaml
|
||||
---
|
||||
midi_routers:
|
||||
- name: service_notifications
|
||||
device: IAC Driver Bus 1
|
||||
debug_listener: true
|
||||
note_triggers:
|
||||
- channel: 0
|
||||
note: 0
|
||||
match_all_velocities: true
|
||||
url: http://example.com
|
||||
method: POST
|
||||
body: |
|
||||
-----------------------------888832887744
|
||||
Content-Disposition: form-data; name="message"
|
||||
|
||||
example variable
|
||||
-----------------------------888832887744
|
||||
Content-Disposition: form-data; name="file"; filename="example.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
Content of file.
|
||||
|
||||
-----------------------------888832887744--
|
||||
headers:
|
||||
Content-Type:
|
||||
- multipart/form-data; boundary=---------------------------888832887744
|
||||
debug_request: true
|
||||
```
|
85
config.go
Normal file
85
config.go
Normal file
@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kkyr/fig"
|
||||
)
|
||||
|
||||
// Configurations relating to HTTP server.
|
||||
type HTTPConfig struct {
|
||||
BindAddr string `fig:"bind_addr"`
|
||||
Port uint `fig:"port"`
|
||||
Debug bool `fig:"debug"`
|
||||
APIKey string `fig:"api_key"`
|
||||
Enabled bool `fig:"enabled"`
|
||||
}
|
||||
|
||||
// Configuration Structure.
|
||||
type Config struct {
|
||||
HTTP HTTPConfig `fig:"http"`
|
||||
MidiRouters []*MidiRouter `fig:"midi_routers"`
|
||||
}
|
||||
|
||||
// Load the configuration.
|
||||
func (a *App) ReadConfig() {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Configuration paths.
|
||||
localConfig, _ := filepath.Abs("./config.yaml")
|
||||
homeDirConfig := usr.HomeDir + "/.config/midi-request-trigger/config.yaml"
|
||||
etcConfig := "/etc/midi-request-trigger/config.yaml"
|
||||
|
||||
// Determine which configuration to use.
|
||||
var configFile string
|
||||
if _, err := os.Stat(app.flags.ConfigPath); err == nil && app.flags.ConfigPath != "" {
|
||||
configFile = app.flags.ConfigPath
|
||||
} else if _, err := os.Stat(localConfig); err == nil {
|
||||
configFile = localConfig
|
||||
} else if _, err := os.Stat(homeDirConfig); err == nil {
|
||||
configFile = homeDirConfig
|
||||
} else if _, err := os.Stat(etcConfig); err == nil {
|
||||
configFile = etcConfig
|
||||
} else {
|
||||
log.Fatal("Unable to find a configuration file.")
|
||||
}
|
||||
|
||||
// Load the configuration file.
|
||||
config := &Config{
|
||||
HTTP: HTTPConfig{
|
||||
BindAddr: "",
|
||||
Port: 34936,
|
||||
Debug: true,
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Load configuration.
|
||||
filePath, fileName := path.Split(configFile)
|
||||
err = fig.Load(config,
|
||||
fig.File(fileName),
|
||||
fig.Dirs(filePath),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error parsing configuration: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Flag Overrides.
|
||||
if app.flags.HTTPBind != "" {
|
||||
config.HTTP.BindAddr = app.flags.HTTPBind
|
||||
}
|
||||
if app.flags.HTTPPort != 0 {
|
||||
config.HTTP.Port = app.flags.HTTPPort
|
||||
}
|
||||
|
||||
// Set global config structure.
|
||||
app.config = config
|
||||
}
|
51
flags.go
Normal file
51
flags.go
Normal file
@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Flags supplied to cli.
|
||||
type Flags struct {
|
||||
ConfigPath string
|
||||
HTTPBind string
|
||||
HTTPPort uint
|
||||
ListMidiDevices bool
|
||||
}
|
||||
|
||||
// Parse the supplied flags.
|
||||
func (a *App) ParseFlags() {
|
||||
app.flags = new(Flags)
|
||||
flag.Usage = func() {
|
||||
fmt.Printf(serviceName + ": " + serviceDescription + ".\n\nUsage:\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
// If version is requested.
|
||||
var printVersion bool
|
||||
flag.BoolVar(&printVersion, "v", false, "Print version")
|
||||
|
||||
// Override configuration path.
|
||||
usage := "Load configuration from `FILE`"
|
||||
flag.StringVar(&app.flags.ConfigPath, "config", "", usage)
|
||||
flag.StringVar(&app.flags.ConfigPath, "c", "", usage+" (shorthand)")
|
||||
|
||||
// Config overrides for http configurations.
|
||||
flag.StringVar(&app.flags.HTTPBind, "http-bind", "", "Bind address for http server")
|
||||
flag.UintVar(&app.flags.HTTPPort, "http-port", 0, "Bind port for http server")
|
||||
|
||||
// Lists available devices.
|
||||
usage = "List available midi devices for use in configurations"
|
||||
flag.BoolVar(&app.flags.ListMidiDevices, "list", false, usage)
|
||||
flag.BoolVar(&app.flags.ListMidiDevices, "l", false, usage+" (shorthand)")
|
||||
|
||||
// Parse the flags.
|
||||
flag.Parse()
|
||||
|
||||
// Print version and exit if requested.
|
||||
if printVersion {
|
||||
fmt.Println(serviceName + ": " + serviceVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
17
go.mod
Normal file
17
go.mod
Normal file
@ -0,0 +1,17 @@
|
||||
module github.com/GRMrGecko/midi-request-trigger
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/kkyr/fig v0.3.2
|
||||
gitlab.com/gomidi/midi/v2 v2.0.30
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.3 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
18
go.sum
Normal file
18
go.sum
Normal file
@ -0,0 +1,18 @@
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
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/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
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/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
github.com/mitchellh/mapstructure v1.4.1/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/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
gitlab.com/gomidi/midi/v2 v2.0.30 h1:RgRYbQeQSab5ZaP1lqRcCTnTSBQroE3CE6V9HgMmOAc=
|
||||
gitlab.com/gomidi/midi/v2 v2.0.30/go.mod h1:Y6IFFyABN415AYsFMPJb0/43TRIuVYDpGKp2gDYLTLI=
|
||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
82
http.go
Normal file
82
http.go
Normal file
@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Basic HTTP server structure.
|
||||
type HTTPServer struct {
|
||||
server *http.Server
|
||||
mux *mux.Router
|
||||
config *HTTPConfig
|
||||
}
|
||||
|
||||
// This functions starts the HTTP server.
|
||||
func NewHTTPServer() *HTTPServer {
|
||||
s := new(HTTPServer)
|
||||
// Update config reference.
|
||||
s.config = &app.config.HTTP
|
||||
s.server = &http.Server{}
|
||||
s.server.Addr = fmt.Sprintf("%s:%d", s.config.BindAddr, s.config.Port)
|
||||
|
||||
// Setup router.
|
||||
r := mux.NewRouter()
|
||||
s.mux = r
|
||||
// Default to notice of service being online.
|
||||
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "MIDI Request Trigger is available\n")
|
||||
})
|
||||
|
||||
s.server.Handler = r
|
||||
// If the debug log is enabled, we'll add a middleware handler to log then pass the request to mux router.
|
||||
if app.config.HTTP.Debug {
|
||||
s.server.Handler = handlers.CombinedLoggingHandler(os.Stdout, r)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start the HTTP server.
|
||||
func (s *HTTPServer) Start(ctx context.Context) {
|
||||
isListening := make(chan bool)
|
||||
// Start server.
|
||||
go s.StartWithIsListening(ctx, isListening)
|
||||
// Allow the http server to initialize.
|
||||
<-isListening
|
||||
}
|
||||
|
||||
// Starts the HTTP server with a listening channel.
|
||||
func (s *HTTPServer) StartWithIsListening(ctx context.Context, isListening chan bool) {
|
||||
// Watch the background context for when we need to shutdown.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
err := s.server.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
// Error from closing listeners, or context timeout:
|
||||
log.Println("Error shutting down http server:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the server.
|
||||
log.Println("Starting http server:", s.server.Addr)
|
||||
l, err := net.Listen("tcp", s.server.Addr)
|
||||
if err != nil {
|
||||
log.Fatal("Listen: ", err)
|
||||
}
|
||||
// Now notify we are listening.
|
||||
isListening <- true
|
||||
// Serve http server on the listening port.
|
||||
err = s.server.Serve(l)
|
||||
if err != nil {
|
||||
log.Println("HTTP server failure:", err)
|
||||
}
|
||||
}
|
79
main.go
Normal file
79
main.go
Normal file
@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"gitlab.com/gomidi/midi/v2"
|
||||
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceName = "midi-request-trigger"
|
||||
serviceDescription = "Takes trigger MIDI messages by HTTP requests and trigger HTTP requests by MIDI messages"
|
||||
serviceVersion = "0.1"
|
||||
)
|
||||
|
||||
// App is the global application structure for communicating between servers and storing information.
|
||||
type App struct {
|
||||
flags *Flags
|
||||
config *Config
|
||||
http *HTTPServer
|
||||
}
|
||||
|
||||
var app *App
|
||||
|
||||
func main() {
|
||||
app = new(App)
|
||||
app.ParseFlags()
|
||||
app.ReadConfig()
|
||||
app.http = NewHTTPServer()
|
||||
|
||||
// Make sure midi drivers are closed when the app closes.
|
||||
defer midi.CloseDriver()
|
||||
|
||||
// If no routers defined, or request to list devices.
|
||||
if app.flags.ListMidiDevices || len(app.config.MidiRouters) == 0 {
|
||||
// If no routers are defined, print notice about configuring one.
|
||||
if len(app.config.MidiRouters) == 0 {
|
||||
log.Println("No routers configured, please configure one.")
|
||||
}
|
||||
// Print available devices.
|
||||
fmt.Printf("MIDI in ports\n")
|
||||
fmt.Println(midi.GetInPorts())
|
||||
fmt.Printf("\n\nMIDI out ports\n")
|
||||
fmt.Println(midi.GetOutPorts())
|
||||
fmt.Printf("\n\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Connect to each router and and setup HTTP handlers.
|
||||
for _, router := range app.config.MidiRouters {
|
||||
router.Connect()
|
||||
for _, trig := range router.RequestTriggers {
|
||||
app.http.mux.HandleFunc(trig.URI, router.Handler)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup context with cancellation function to allow background services to gracefully stop.
|
||||
ctx, ctxCancel := context.WithCancel(context.Background())
|
||||
// Start listening on HTTP server.
|
||||
app.http.Start(ctx)
|
||||
|
||||
// Monitor common signals.
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
// Wait for a signal.
|
||||
<-c
|
||||
// Stop HTTP server.
|
||||
ctxCancel()
|
||||
|
||||
// Disconnect all MIDI listeners.
|
||||
for _, router := range app.config.MidiRouters {
|
||||
router.Disconnect()
|
||||
}
|
||||
}
|
271
midiRouter.go
Normal file
271
midiRouter.go
Normal file
@ -0,0 +1,271 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/gomidi/midi/v2"
|
||||
"gitlab.com/gomidi/midi/v2/drivers"
|
||||
)
|
||||
|
||||
// Triggers that occur from MIDI messages received.
|
||||
type NoteTrigger struct {
|
||||
MatchAllNotes bool `fig:"match_all_notes"`
|
||||
Channel uint8 `fig:"channel"`
|
||||
Note uint8 `fig:"note"`
|
||||
Velocity uint8 `fig:"velocity"`
|
||||
MatchAllVelocities bool `fig:"match_all_velocities"`
|
||||
MidiInfoInRequest bool `fig:"midi_info_in_request"`
|
||||
InsecureSkipVerify bool `fig:"insecure_skip_verify"`
|
||||
URL string `fig:"url"`
|
||||
Method string `fig:"method"`
|
||||
Body string `fig:"body"`
|
||||
Headers http.Header `fig:"headers"`
|
||||
DebugRequest bool `fig:"debug_request"`
|
||||
}
|
||||
|
||||
// Triggers that occur from HTTP messsages received.
|
||||
type RequestTrigger struct {
|
||||
Channel uint8 `fig:"channel"`
|
||||
Note uint8 `fig:"note"`
|
||||
Velocity uint8 `fig:"velocity"`
|
||||
MidiInfoInRequest bool `fig:"midi_info_in_request"`
|
||||
URI string `fig:"uri"`
|
||||
}
|
||||
|
||||
// A common router for both receiving and sending MIDI messages.
|
||||
type MidiRouter struct {
|
||||
Name string `fig:"name"`
|
||||
Device string `fig:"device"`
|
||||
DebugListener bool `fig:"debug_listener"`
|
||||
DisableListener bool `fig:"disable_listener"`
|
||||
NoteTriggers []NoteTrigger `fig:"note_triggers"`
|
||||
RequestTriggers []RequestTrigger `fig:"request_triggers"`
|
||||
|
||||
MidiOut drivers.Out `fig:"-"`
|
||||
ListenerStop func() `fig:"-"`
|
||||
}
|
||||
|
||||
// When a MIDI message occurs, send the HTTP request.
|
||||
func (r *MidiRouter) sendRequest(channel, note, velocity uint8) {
|
||||
// Check each trigger to find requests that match this message.
|
||||
for _, trig := range r.NoteTriggers {
|
||||
// If match all notes, process this request.
|
||||
// If not, check if channel, note, and velocity matches.
|
||||
// The velocity may be defined to accept all.
|
||||
if trig.MatchAllNotes || (trig.Channel == channel && trig.Note == note && (trig.Velocity == velocity || trig.MatchAllVelocities)) {
|
||||
// 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)
|
||||
|
||||
// Default method to GET if nothing is defined.
|
||||
if trig.Method == "" {
|
||||
trig.Method = "GET"
|
||||
}
|
||||
|
||||
// Parse the URL to make sure its valid.
|
||||
url, err := url.Parse(trig.URL)
|
||||
// If not valid, we need to stop processing this request.
|
||||
if err != nil {
|
||||
log.Printf("Trigger failed to parse url: %s\n %s\n", err, logInfo)
|
||||
continue
|
||||
}
|
||||
|
||||
// If MIDI info needs to be added to the request, add it.
|
||||
if trig.MidiInfoInRequest {
|
||||
query := url.Query()
|
||||
query.Add("channel", strconv.Itoa(int(channel)))
|
||||
query.Add("note", strconv.Itoa(int(note)))
|
||||
query.Add("velocity", strconv.Itoa(int(velocity)))
|
||||
url.RawQuery = query.Encode()
|
||||
}
|
||||
|
||||
// If body provided, setup a reader for it.
|
||||
var body io.Reader
|
||||
if trig.Body != "" {
|
||||
body = strings.NewReader(trig.Body)
|
||||
}
|
||||
|
||||
// If debugging, log that we're starting a request.
|
||||
if trig.DebugRequest {
|
||||
log.Printf("Starting request for trigger: %s %s\n%s\n", trig.Method, url.String(), logInfo)
|
||||
}
|
||||
|
||||
// Make the request.
|
||||
req, err := http.NewRequest(trig.Method, url.String(), body)
|
||||
if err != nil {
|
||||
log.Printf("Trigger failed to parse url: %s\n %s\n", err, logInfo)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add headers to the request.
|
||||
req.Header = trig.Headers
|
||||
|
||||
// Configure transport with trigger config.
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: trig.InsecureSkipVerify},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
// Perform the request.
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Trigger failed to request: %s\n %s\n", err, logInfo)
|
||||
continue
|
||||
}
|
||||
|
||||
// Close the body at end of request.
|
||||
defer res.Body.Close()
|
||||
|
||||
// If debug enabled, read the body and log it.
|
||||
if trig.DebugRequest {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
log.Printf("Trigger failed to read body: %s\n %s\n", err, logInfo)
|
||||
continue
|
||||
}
|
||||
log.Printf("Trigger response: %s\n%s\n", logInfo, string(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for HTTP requests.
|
||||
func (m *MidiRouter) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
// Check each request trigger for ones that match the request URI.
|
||||
for _, t := range m.RequestTriggers {
|
||||
// If matches request, process MIDI message.
|
||||
if t.URI == r.URL.RawPath {
|
||||
// Set default values to those from this trigger.
|
||||
channel, note, velocity := t.Channel, t.Note, t.Velocity
|
||||
// If MIDI info is in the request query, update to request.
|
||||
if t.MidiInfoInRequest {
|
||||
query := r.URL.Query()
|
||||
// Regex to ensure only numbers are processed.
|
||||
numRx := regexp.MustCompile(`^[0-9]+$`)
|
||||
|
||||
// Check for channel, and only configure if request has a valid value.
|
||||
ch := query.Get("channel")
|
||||
if numRx.MatchString(ch) {
|
||||
i, err := strconv.Atoi(ch)
|
||||
if err != nil && i <= 255 && i >= 0 {
|
||||
channel = uint8(i)
|
||||
}
|
||||
}
|
||||
// Check for note, and only configure if request has a valid value.
|
||||
key := query.Get("note")
|
||||
if numRx.MatchString(key) {
|
||||
i, err := strconv.Atoi(key)
|
||||
if err != nil && i < 255 && i >= 0 {
|
||||
note = uint8(i)
|
||||
}
|
||||
}
|
||||
// Check for velocity, and only configure if request has a valid value.
|
||||
vel := query.Get("velocity")
|
||||
if numRx.MatchString(vel) {
|
||||
i, err := strconv.Atoi(vel)
|
||||
if err != nil && i < 128 && i >= 0 {
|
||||
velocity = uint8(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get send function for output.
|
||||
send, err := midi.SendTo(m.MidiOut)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get midi sender for request: %s\n%s\n", t.URI, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Make the MIDI message based on information.
|
||||
msg := midi.NoteOn(channel, note, velocity)
|
||||
if velocity == 0 {
|
||||
msg = midi.NoteOff(channel, note)
|
||||
}
|
||||
|
||||
// Send MIDI message.
|
||||
err = send(msg)
|
||||
if err != nil {
|
||||
log.Printf("Failed to send midi message: %s\n%s\n", t.URI, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Update HTTP status to no content as an success message.
|
||||
http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to MIDI devices and start listening.
|
||||
func (r *MidiRouter) Connect() {
|
||||
// If request triggers defined, find the out port.
|
||||
if len(r.RequestTriggers) != 0 {
|
||||
out, err := midi.FindOutPort(r.Device)
|
||||
if err != nil {
|
||||
log.Println("Can't find output device:", r.Device)
|
||||
} else {
|
||||
r.MidiOut = out
|
||||
}
|
||||
}
|
||||
// If listener is disabled, stop here.
|
||||
if r.DisableListener {
|
||||
return
|
||||
}
|
||||
|
||||
// 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, ¬e, &velocity):
|
||||
// If debug, log.
|
||||
if r.DebugListener {
|
||||
log.Printf("starting note %s(%d) on channel %v with velocity %v\n", 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, ¬e):
|
||||
// If debug, log.
|
||||
if r.DebugListener {
|
||||
log.Printf("ending note %s(%d) on channel %v\n", midi.Note(note), note, channel)
|
||||
}
|
||||
// Process request.
|
||||
r.sendRequest(channel, note, 0)
|
||||
default:
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Error listening to device: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update stop function for disconnects.
|
||||
r.ListenerStop = stop
|
||||
}
|
||||
|
||||
// On disconnect, stop and remove output device.
|
||||
func (r *MidiRouter) Disconnect() {
|
||||
r.MidiOut = nil
|
||||
if r.ListenerStop != nil {
|
||||
r.ListenerStop()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user