First commit
This commit is contained in:
commit
40181226fb
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
config.yaml
|
||||
osc-mqtt-bridge
|
19
License.txt
Normal file
19
License.txt
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.
|
95
README.md
Normal file
95
README.md
Normal file
@ -0,0 +1,95 @@
|
||||
# osc-mqtt-bridge
|
||||
A bridge between [Open Sound Control](https://en.wikipedia.org/wiki/Open_Sound_Control) (OSC) and MQTT, allowind bidirectional communication. The main purpose of this tool is to provide a way to talk to devices that support OSC via MQTT messages for automation.
|
||||
|
||||
## Example configuration
|
||||
```yaml
|
||||
relays:
|
||||
- mqtt_host: 10.0.0.2
|
||||
mqtt_port: 1883
|
||||
mqtt_client_id: osc_mqtt_bridge
|
||||
mqtt_user: mqtt
|
||||
mqtt_password: PASSWORD
|
||||
mqtt_topic: osc/behringer_wing
|
||||
osc_host: 10.0.0.3
|
||||
osc_port: 2223
|
||||
osc_bind_addr: 10.0.0.4 # Change to this machine's IP address. Expected to be a static IP.
|
||||
log_level: 2
|
||||
```
|
||||
|
||||
## Configuration specification
|
||||
|
||||
### Relay
|
||||
- `mqtt_host`: Hostname of the MQTT broker.
|
||||
- `mqtt_port`: Port of the MQTT broker.
|
||||
- `mqtt_client_id`: MQTT client ID of this relay.
|
||||
- `mqtt_user`: User name used for MQTT authentication.
|
||||
- `mqtt_password`: Password used for MQTT authentication.
|
||||
- `mqtt_topic`: Topic where MQTT messages are pushed and received.
|
||||
|
||||
Set topic to `osc/example` and the following topics will be setup.
|
||||
- `osc/example/cmd/$OSC_CMD` - Any commands received on OSC will publish here.
|
||||
- `osc/example/send/$OSC_CMD` - Any commands pushed via MQTT will be forwarded to OSC.
|
||||
- `osc/example/bundle` - OSC Bundle messages.
|
||||
- `osc/example/bundle/send` - Send OSC Bundle messages.
|
||||
- `osc/example/status` - Configuration is published on startup.
|
||||
- `osc/example/status/check` - Request status.
|
||||
<br/><br/>
|
||||
|
||||
- `mqtt_disable_config_send`: Disables the config send.
|
||||
- `osc_host`: Hostname for OSC client connection.
|
||||
- `osc_port`: Port for OSC client connection.
|
||||
- `osc_bind_addr`: Bind address of the OSC server.
|
||||
|
||||
To have bidirectional mode, you must specify at least this, OscHost, and OscPort defined. You must specify the unicast IP address, cannot be `0.0.0.0`.
|
||||
|
||||
- `osc_bind_port`: Port of the OSC server. Defaults to OscPort if specified.
|
||||
- `osc_disallow_arbritary_command`: Disallows pushing to arbritary commands to the cmd topic.
|
||||
- `commands`: Pre-defined commands to relay.
|
||||
|
||||
This is an array with the following variables.
|
||||
|
||||
- `command`: The command path to send.
|
||||
- `mqtt_topic`: Absolute MQTT topic to subscribe.
|
||||
- `mqtt_sub_topic`: Sub topic off relay MQTT topic to subscribe.
|
||||
osc/example/$SUB_TOPIC
|
||||
- `disallow_payload`: Rather or not to disallow payload to be relayed.
|
||||
- `default_payload`: Payload to send if no payload is provided via MQTT or if DisallowPayload is true. This is an array of strings/integers/timestamps/bools.
|
||||
<br/><br/>
|
||||
|
||||
- `osc_subscriptions`: OSC Comamnds to send at regular intervals. Useful for OSC servers that offers data subscriptions.
|
||||
|
||||
This is an array with the following variables.
|
||||
|
||||
- `command`: The command to send every interval.
|
||||
- `payload`: Payload to send. This is an array of strings/integers/timestamps/bools.
|
||||
- `interval`: How often to call the command.
|
||||
<br/><br/>
|
||||
|
||||
- `log_level`: How much logging.
|
||||
|
||||
- 0 - Errors
|
||||
- 1 - MQTT and OSC receive logging.
|
||||
- 2 - MQTT and OSC send logging.
|
||||
- 3 - Debug
|
||||
|
||||
## MQTT Message Example
|
||||
|
||||
**Mute Behringer Wing channel 1**<br/>
|
||||
Topic: osc/behringer_wing/send/ch/1/mute<br/>
|
||||
Payload: `["1"]`
|
||||
|
||||
**Behringer Wing get info**<br/>
|
||||
Topic: osc/behringer_wing/send/?<br/>
|
||||
Payload:
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
go build
|
||||
```
|
||||
|
||||
[Golang](https://go.dev/) 1.19 and below are known to have issues, 1.20 works.
|
||||
|
||||
## Config file location
|
||||
|
||||
Same directory as the binary, in your home directory at `~/.config/mqtt-osc-bridge/config.yaml`, or under etc at `/etc/mqtt-osc-bridge/config.yaml`.
|
87
config.go
Normal file
87
config.go
Normal file
@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Configuration Structure
|
||||
type Config struct {
|
||||
// Relays: Different relays available.
|
||||
Relays []*Relay `yaml:"relays"`
|
||||
}
|
||||
|
||||
// ReadConfig Read the configuration file
|
||||
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/mqtt-osc-bridge/config.yaml"
|
||||
etcConfig := "/etc/mqtt-osc-bridge/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.")
|
||||
}
|
||||
|
||||
app.config = new(Config)
|
||||
|
||||
yamlFile, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading YAML file: %s\n", err)
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(yamlFile, &app.config)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing YAML file: %s\n", err)
|
||||
}
|
||||
|
||||
if len(app.config.Relays) == 0 {
|
||||
log.Fatal("No relays defined in the configuration file.")
|
||||
}
|
||||
|
||||
for _, relay := range app.config.Relays {
|
||||
if relay.OscBindAddr != "" && relay.OscBindPort == 0 {
|
||||
relay.OscBindPort = relay.OscPort
|
||||
}
|
||||
}
|
||||
|
||||
for i, relay := range app.config.Relays {
|
||||
if relay.MqttHost == "" || relay.MqttPort == 0 {
|
||||
log.Fatalf("Relay %d: MQTT host and port are required configurations.", i)
|
||||
}
|
||||
if relay.MqttTopic == "" {
|
||||
log.Fatalf("Relay %d: MQTT topic is a required configuration.", i)
|
||||
}
|
||||
if relay.OscBindAddr == "" && relay.OscHost == "" {
|
||||
log.Fatalf("Relay %d: You must define either a bind address or an OSC host in the configuration.", i)
|
||||
}
|
||||
for b, relay2 := range app.config.Relays {
|
||||
if b != i {
|
||||
if relay.MqttTopic == relay2.MqttTopic {
|
||||
log.Fatalf("Relay %d: MQTT topic cannot exist on 2 different relays.", i)
|
||||
}
|
||||
if relay.OscBindPort == relay2.OscBindPort {
|
||||
log.Fatalf("Relay %d: Cannot use the same OSC bind port on 2 different relays.", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
flags.go
Normal file
35
flags.go
Normal file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Flags Configuration options for cli execution
|
||||
type Flags struct {
|
||||
ConfigPath string
|
||||
}
|
||||
|
||||
// InitFlags Parses configuration options
|
||||
func (a *App) InitFlags() {
|
||||
app.flags = new(Flags)
|
||||
flag.Usage = func() {
|
||||
fmt.Printf(serviceName + ": " + serviceDescription + ".\n\nUsage:\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
var printVersion bool
|
||||
flag.BoolVar(&printVersion, "v", false, "Print version")
|
||||
|
||||
usage := "Load configuration from `FILE`"
|
||||
flag.StringVar(&app.flags.ConfigPath, "config", "", usage)
|
||||
flag.StringVar(&app.flags.ConfigPath, "c", "", usage+" (shorthand)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if printVersion {
|
||||
fmt.Println(serviceName + ": " + serviceVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
15
go.mod
Normal file
15
go.mod
Normal file
@ -0,0 +1,15 @@
|
||||
module github.com/GRMrGecko/osc-mqtt-bridge
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/eclipse/paho.mqtt.golang v1.4.2
|
||||
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
)
|
18
go.sum
Normal file
18
go.sum
Normal file
@ -0,0 +1,18 @@
|
||||
github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4=
|
||||
github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 h1:fqwINudmUrvGCuw+e3tedZ2UJ0hklSw6t8UPomctKyQ=
|
||||
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5/go.mod h1:lqMjoCs0y0GoRRujSPZRBaGb4c5ER6TfkFKSClxkMbY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
38
main.go
Normal file
38
main.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const serviceName = "osc-mqtt-bridge"
|
||||
const serviceDescription = "Bridges MQTT messages to OSC"
|
||||
const serviceVersion = "0.1"
|
||||
|
||||
// App is the global application structure for communicating between servers and storing information.
|
||||
type App struct {
|
||||
flags *Flags
|
||||
config *Config
|
||||
}
|
||||
|
||||
var app *App
|
||||
|
||||
func main() {
|
||||
thisPath, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
os.Chdir(filepath.Dir(thisPath))
|
||||
|
||||
app = new(App)
|
||||
app.InitFlags()
|
||||
app.ReadConfig()
|
||||
|
||||
for _, relay := range app.config.Relays {
|
||||
relay.Start()
|
||||
}
|
||||
|
||||
for {
|
||||
}
|
||||
}
|
472
relay.go
Normal file
472
relay.go
Normal file
@ -0,0 +1,472 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/hypebeast/go-osc/osc"
|
||||
)
|
||||
|
||||
// LogLevel Definition
|
||||
type LogLevel int
|
||||
|
||||
const (
|
||||
// ErrorLog Logs only errors.
|
||||
ErrorLog LogLevel = iota
|
||||
// ReceiveLog MQTT and OSC receive logging.
|
||||
ReceiveLog
|
||||
// SendLog MQTT and OSC send logging.
|
||||
SendLog
|
||||
// DebugLog Debug messages.
|
||||
DebugLog
|
||||
)
|
||||
|
||||
// String: Provides a string value for a log level.
|
||||
func (l LogLevel) String() string {
|
||||
return [...]string{"Error", "Receive", "Send", "Debug"}[l]
|
||||
}
|
||||
|
||||
// Relay command definition
|
||||
type RelayCommand struct {
|
||||
// Command: The command path to send.
|
||||
Command string `yaml:"command" json:"command"`
|
||||
// MqttTopic: Absolute MQTT topic to subscribe.
|
||||
MqttTopic string `yaml:"mqtt_topic" json:"mqtt_topic"`
|
||||
// MqttSubTopic: Sub topic off relay MQTT topic to subscribe.
|
||||
// osc/example/$SUB_TOPIC
|
||||
MqttSubTopic string `yaml:"mqtt_sub_topic" json:"mqtt_sub_topic"`
|
||||
// DisallowPayload: Rather or not to disallow payload to be relayed.
|
||||
DisallowPayload bool `yaml:"disallow_payload" json:"disallow_payload"`
|
||||
// DefaultPayload: Payload to send if no payload is provided via MQTT or if DisallowPayload is true.
|
||||
DefaultPayload []interface{} `yaml:"default_payload" json:"default_payload"`
|
||||
}
|
||||
|
||||
// Relay OSC subscription
|
||||
type RelayOscSubscription struct {
|
||||
// Command: The command to send every interval.
|
||||
Command string `yaml:"command" json:"command"`
|
||||
// Payload: Payload to send.
|
||||
Payload []interface{} `yaml:"payload" json:"payload"`
|
||||
// Interval: How often to call the command.
|
||||
Interval time.Duration `yaml:"interval" json:"interval"`
|
||||
}
|
||||
|
||||
// Relay configurations
|
||||
type Relay struct {
|
||||
// MqttHost: Hostname of the MQTT broker.
|
||||
MqttHost string `yaml:"mqtt_host" json:"mqtt_host"`
|
||||
// MqttPort: Port of the MQTT broker.
|
||||
MqttPort int `yaml:"mqtt_port" json:"mqtt_port"`
|
||||
// MqttClientId: MQTT client ID of this relay.
|
||||
MqttClientId string `yaml:"mqtt_client_id" json:"mqtt_client_id"`
|
||||
// MqttUser: User name used for MQTT authentication.
|
||||
MqttUser string `yaml:"mqtt_user" json:"mqtt_user"`
|
||||
// MqttPassword: Password used for MQTT authentication.
|
||||
MqttPassword string `yaml:"mqtt_password" json:"mqtt_password"`
|
||||
// MqttTopic: Topic where MQTT messages are pushed and received.
|
||||
// Set topic to `osc/example` and the following topics will be setup.
|
||||
// osc/example/cmd/$OSC_CMD - Any commands received on OSC will publish here.
|
||||
// osc/example/send/$OSC_CMD - Any commands pushed via MQTT will be forwarded to OSC.
|
||||
// osc/example/bundle - OSC Bundle messages.
|
||||
// osc/example/bundle/send - Send OSC Bundle messages.
|
||||
// osc/example/status - Configuration is published on startup.
|
||||
// osc/example/status/check - Request status.
|
||||
MqttTopic string `yaml:"mqtt_topic" json:"mqtt_topic"`
|
||||
// MqttDisableConfigSend: Disables the config send.
|
||||
MqttDisableConfigSend bool `yaml:"mqtt_disable_config_send" json:"mqtt_disable_config_send"`
|
||||
|
||||
// OscHost: Hostname for OSC client connection.
|
||||
OscHost string `yaml:"osc_host" json:"osc_host"`
|
||||
// OscPort: Port for OSC client connection.
|
||||
OscPort int `yaml:"osc_port" json:"osc_port"`
|
||||
// OscBindAddr: Bind address of the OSC server.
|
||||
// To have bidirectional mode, you must specify at least this, OscHost, and OscPort defined.
|
||||
// You must specify the unicast IP address, cannot be 0.0.0.0.
|
||||
OscBindAddr string `yaml:"osc_bind_addr" json:"osc_bind_addr"`
|
||||
// OscBindPort: Port of the OSC server. Defaults to OscPort if specified.
|
||||
OscBindPort int `yaml:"osc_bind_port" json:"osc_bind_port"`
|
||||
// OscDisallowArbritaryCommand: Disallows pushing to arbritary commands to the cmd topic.
|
||||
OscDisallowArbritaryCommand bool `yaml:"osc_disallow_arbritary_command" json:"osc_disallow_arbritary_command"`
|
||||
|
||||
// RelayCommands: Pre-defined commands to relay.
|
||||
Commands []RelayCommand `yaml:"relay_commands" json:"commands"`
|
||||
// RelayOscSubscriptions: OSC Comamnds to send at regular intervals. Useful for OSC servers that offers data subscriptions.
|
||||
OscSubscriptions []RelayOscSubscription `yaml:"osc_subscriptions" json:"osc_subscriptions"`
|
||||
|
||||
// LogLevel: How much logging.
|
||||
// 0 - Errors
|
||||
// 1 - MQTT and OSC receive logging.
|
||||
// 2 - MQTT and OSC send logging.
|
||||
// 3 - Debug
|
||||
LogLevel LogLevel `yaml:"log_level" json:"log_level"`
|
||||
|
||||
// MqttClient: The client connection to MQTT.
|
||||
MqttClient mqtt.Client `yaml:"-" json:"-"`
|
||||
// OscClient: The client connection to OSC.
|
||||
OscClient *osc.Client `yaml:"-" json:"-"`
|
||||
// OscServer: OSC Server.
|
||||
OscServer *osc.Server `yaml:"-" json:"-"`
|
||||
// OscServerConn: Server connection.
|
||||
// The OSC software is limited in bidirectional support, so I do my own connection here.
|
||||
OscServerConn net.PacketConn `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
// OscMessage: Used for json encode/decode to/from MQTT for bundles.
|
||||
type OscMessage struct {
|
||||
Address string `json:"address"`
|
||||
Arguments []interface{} `json:"arguments"`
|
||||
}
|
||||
|
||||
// OscBundle: Used for json encode/decode to/from MQTT.
|
||||
type OscBundle struct {
|
||||
Timetag time.Time `json:"timetag"`
|
||||
Messages []*OscMessage `json:"messages"`
|
||||
Bundles []*OscBundle `json:"bundles"`
|
||||
}
|
||||
|
||||
// OscDispatcher: Handles OSC messages.
|
||||
type OscDispatcher struct {
|
||||
r *Relay
|
||||
}
|
||||
|
||||
// makeBundle: Makes an OscBundle from an osc.Bundle.
|
||||
func (d OscDispatcher) makeBundle(bundle *osc.Bundle) *OscBundle {
|
||||
b := new(OscBundle)
|
||||
b.Timetag = bundle.Timetag.Time()
|
||||
for _, message := range bundle.Messages {
|
||||
m := new(OscMessage)
|
||||
m.Address = message.Address
|
||||
m.Arguments = message.Arguments
|
||||
b.Messages = append(b.Messages, m)
|
||||
}
|
||||
|
||||
for _, sbundle := range bundle.Bundles {
|
||||
subBundle := d.makeBundle(sbundle)
|
||||
b.Bundles = append(b.Bundles, subBundle)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Dispatch: Handle OSC packet.
|
||||
func (d OscDispatcher) Dispatch(packet osc.Packet) {
|
||||
// Determine packet type and process.
|
||||
if packet != nil {
|
||||
switch packet.(type) {
|
||||
default:
|
||||
d.r.Log(ErrorLog, "Unknown OSC packet received.")
|
||||
|
||||
// Message packets can just go to /cmd/$OSC_CMD and arguments encoded to JSON.
|
||||
case *osc.Message:
|
||||
message := packet.(*osc.Message)
|
||||
d.r.Log(ReceiveLog, "<- [OSC] %s: %s", message.Address, message.Arguments)
|
||||
topic := d.r.MqttTopic + "/cmd" + message.Address
|
||||
data, err := json.Marshal(message.Arguments)
|
||||
if err != nil {
|
||||
d.r.Log(ErrorLog, "Json Encode: %s", err)
|
||||
return
|
||||
}
|
||||
d.r.MqttClient.Publish(topic, 0, true, data)
|
||||
d.r.Log(SendLog, "-> [MQTT] %s: %s", topic, data)
|
||||
|
||||
// Bundle packets are capable of having multiple messages and bundles embeded in it,
|
||||
// so I translate to my own bundle structure that is JSON aware.
|
||||
case *osc.Bundle:
|
||||
b := d.makeBundle(packet.(*osc.Bundle))
|
||||
d.r.Log(ReceiveLog, "<- [OSC] Bundle %s", b.Timetag)
|
||||
topic := d.r.MqttTopic + "/bundle"
|
||||
data, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
d.r.Log(ErrorLog, "Json Encode: %s", err)
|
||||
return
|
||||
}
|
||||
d.r.MqttClient.Publish(topic, 0, true, data)
|
||||
d.r.Log(SendLog, "-> [MQTT] %s: %s", topic, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OscSend: Sends an OSC packet. I use my own function to allow bidirectional communication.
|
||||
func (r *Relay) OscSend(packet osc.Packet) error {
|
||||
// Do not send nil packets.
|
||||
if packet == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log send request.
|
||||
switch packet.(type) {
|
||||
default:
|
||||
case *osc.Message:
|
||||
message := packet.(*osc.Message)
|
||||
r.Log(SendLog, "-> [OSC] %s: %s", message.Address, message.Arguments)
|
||||
case *osc.Bundle:
|
||||
bundle := packet.(*osc.Bundle)
|
||||
r.Log(SendLog, "-> [OSC] Bundle %s", bundle.Timetag.Time())
|
||||
}
|
||||
|
||||
// Hosts can be DNS names, or IP addresses, so we need to resolve.
|
||||
var err error
|
||||
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", r.OscHost, r.OscPort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert packet to OSC bytes.
|
||||
data, err := packet.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.LogLevel >= DebugLog {
|
||||
r.Log(DebugLog, "-> [OSC] Binary %s", bytes.ReplaceAll(data, []byte{byte(0)}, []byte("~")))
|
||||
}
|
||||
|
||||
// If we have an OSC Server defined, we use its connection to write the data for bidirectional support.
|
||||
if r.OscServer != nil {
|
||||
_, err = r.OscServerConn.WriteTo(data, addr)
|
||||
} else {
|
||||
// Otherwise, we dial the address with a unused source port.
|
||||
// Specifying a manual source port could end up with conflicts.
|
||||
conn, err := net.DialUDP("udp", nil, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write(data)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// SendStatus: Send config to MQTT status.
|
||||
func (r *Relay) SendStatus() {
|
||||
// If disabled, ignore.
|
||||
if r.MqttDisableConfigSend {
|
||||
return
|
||||
}
|
||||
|
||||
// Make JSON dump.
|
||||
config, err := json.Marshal(&r)
|
||||
if err != nil {
|
||||
r.Log(ErrorLog, "Json Error: %s", err)
|
||||
}
|
||||
|
||||
// Send config.
|
||||
r.MqttClient.Publish(r.MqttTopic+"/status", 0, true, config)
|
||||
}
|
||||
|
||||
// MakeOSCBundle: Makes an osc.Bundle. from an OscBundle.
|
||||
func (r *Relay) MakeOSCBundle(bundle *OscBundle) *osc.Bundle {
|
||||
b := osc.NewBundle(bundle.Timetag)
|
||||
|
||||
// Add attached messages.
|
||||
for _, message := range bundle.Messages {
|
||||
m := osc.NewMessage(message.Address)
|
||||
m.Arguments = message.Arguments
|
||||
b.Append(m)
|
||||
}
|
||||
|
||||
// Add sub bundles.
|
||||
for _, sbundle := range bundle.Bundles {
|
||||
subBundle := r.MakeOSCBundle(sbundle)
|
||||
b.Append(subBundle)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// MqttOnEvent: Handle MQTT events.
|
||||
func (r *Relay) MqttOnEvent(client mqtt.Client, message mqtt.Message) {
|
||||
r.Log(ReceiveLog, "<- [MQTT] %s: %s\n", message.Topic(), message.Payload())
|
||||
|
||||
// Check commands to see if one matches this topic.
|
||||
for _, cmd := range r.Commands {
|
||||
if message.Topic() == cmd.MqttTopic ||
|
||||
(cmd.MqttSubTopic != "" && message.Topic() == r.MqttTopic+"/"+cmd.MqttSubTopic) {
|
||||
// Configure OSC message.
|
||||
oscMessage := osc.NewMessage(cmd.Command)
|
||||
|
||||
// If arguments allowed and provided, parse, otherwise use default payload.
|
||||
var arguments []interface{}
|
||||
if !cmd.DisallowPayload && len(message.Payload()) != 0 {
|
||||
err := json.Unmarshal(message.Payload(), &arguments)
|
||||
if err != nil {
|
||||
r.Log(ErrorLog, "Json Error: %s", err)
|
||||
return
|
||||
}
|
||||
} else if len(cmd.DefaultPayload) != 0 {
|
||||
arguments = cmd.DefaultPayload
|
||||
}
|
||||
oscMessage.Arguments = arguments
|
||||
|
||||
// Send OSC message.
|
||||
err := r.OscSend(oscMessage)
|
||||
if err != nil {
|
||||
r.Log(ErrorLog, "Send Error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If standard send topic.
|
||||
if strings.HasPrefix(message.Topic(), r.MqttTopic+"/send") {
|
||||
// Verify arbritary commands can be sent.
|
||||
if r.OscDisallowArbritaryCommand {
|
||||
r.Log(ErrorLog, "Arbritary commands are disabled on this relay.")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the command from topic.
|
||||
cmd := strings.Replace(message.Topic(), r.MqttTopic+"/send", "", 1)
|
||||
if cmd == "" {
|
||||
cmd = "/"
|
||||
}
|
||||
|
||||
// Parse the arguments.
|
||||
var arguments []interface{}
|
||||
if len(message.Payload()) != 0 {
|
||||
err := json.Unmarshal(message.Payload(), &arguments)
|
||||
if err != nil {
|
||||
r.Log(ErrorLog, "Json Error: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create OSC message.
|
||||
oscMessage := osc.NewMessage(cmd)
|
||||
oscMessage.Arguments = arguments
|
||||
|
||||
// Send OSC message.
|
||||
err := r.OscSend(oscMessage)
|
||||
if err != nil {
|
||||
r.Log(ErrorLog, "Send Error: %s", err)
|
||||
}
|
||||
} else if message.Topic() == r.MqttTopic+"/bundle/send" {
|
||||
// Verify arbritary commands can be sent.
|
||||
if r.OscDisallowArbritaryCommand {
|
||||
r.Log(ErrorLog, "Arbritary commands are disabled on this relay.")
|
||||
return
|
||||
}
|
||||
|
||||
// Create bundle.
|
||||
bundle := new(OscBundle)
|
||||
err := json.Unmarshal(message.Payload(), bundle)
|
||||
if err != nil {
|
||||
r.Log(ErrorLog, "Json Error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Make the OSC bundle based on received bundle.
|
||||
b := r.MakeOSCBundle(bundle)
|
||||
|
||||
// Send OSC bundle.
|
||||
err = r.OscSend(b)
|
||||
if err != nil {
|
||||
r.Log(ErrorLog, "Send Error: %s", err)
|
||||
}
|
||||
} else if message.Topic() == r.MqttTopic+"/status/check" {
|
||||
r.SendStatus()
|
||||
}
|
||||
}
|
||||
|
||||
// MqttSubscribe: Subscribe to MQTT Topic.
|
||||
func (r *Relay) MqttSubscribe(topic string) {
|
||||
r.Log(DebugLog, "Subscribing MQTT: %s", topic)
|
||||
if t := r.MqttClient.Subscribe(topic, 0, r.MqttOnEvent); t.Wait() && t.Error() != nil {
|
||||
r.Log(ErrorLog, "MQTT Subscribe Error: %s", t.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Log: Logging function to allow log levels.
|
||||
func (r *Relay) Log(level LogLevel, format string, args ...interface{}) {
|
||||
if level <= r.LogLevel {
|
||||
log.Println(fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
|
||||
// Start: Start the relay.
|
||||
func (r *Relay) Start() {
|
||||
// Connect to MQTT.
|
||||
mqtt_opts := mqtt.NewClientOptions()
|
||||
mqtt_opts.AddBroker(fmt.Sprintf("tcp://%s:%d", r.MqttHost, r.MqttPort))
|
||||
mqtt_opts.SetClientID(r.MqttClientId)
|
||||
mqtt_opts.SetUsername(r.MqttUser)
|
||||
mqtt_opts.SetPassword(r.MqttPassword)
|
||||
r.MqttClient = mqtt.NewClient(mqtt_opts)
|
||||
|
||||
// Connect and failures are fatal exiting service.
|
||||
r.Log(DebugLog, "Connecting to MQTT")
|
||||
if t := r.MqttClient.Connect(); t.Wait() && t.Error() != nil {
|
||||
log.Fatalf("MQTT error: %s", t.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Subscribe to MQTT topics.
|
||||
r.MqttSubscribe(r.MqttTopic + "/send/#")
|
||||
r.MqttSubscribe(r.MqttTopic + "/bundle/send")
|
||||
r.MqttSubscribe(r.MqttTopic + "/status/check")
|
||||
// Subscribe to command topics configured.
|
||||
for _, cmd := range r.Commands {
|
||||
if cmd.MqttTopic != "" {
|
||||
r.MqttSubscribe(cmd.MqttTopic)
|
||||
}
|
||||
if cmd.MqttSubTopic != "" {
|
||||
r.MqttSubscribe(r.MqttTopic + "/" + cmd.MqttSubTopic)
|
||||
}
|
||||
}
|
||||
|
||||
// If an OSC client configuration is provided, setup client.
|
||||
if r.OscHost != "" && r.OscPort != 0 {
|
||||
r.OscClient = osc.NewClient(r.OscHost, r.OscPort)
|
||||
}
|
||||
|
||||
// If OSC server configured, setup server.
|
||||
if r.OscBindAddr != "" && r.OscBindPort != 0 {
|
||||
r.OscServer = &osc.Server{Addr: fmt.Sprintf("%s:%d", r.OscBindAddr, r.OscBindPort), Dispatcher: OscDispatcher{r: r}}
|
||||
|
||||
// Run server in thread.
|
||||
go func() {
|
||||
r.Log(DebugLog, "Starting OSC Server")
|
||||
var err error
|
||||
// I setup our own UDP connection to overcome a limit in go-osc
|
||||
// where bidirectional isn't built in.
|
||||
r.OscServerConn, err = net.ListenPacket("udp", r.OscServer.Addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Close connection when function ends.
|
||||
defer r.OscServerConn.Close()
|
||||
|
||||
// Have Go-OSC handle OSC traffic on this connection.
|
||||
if err = r.OscServer.Serve(r.OscServerConn); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Setup subscriptions.
|
||||
for _, subcription := range r.OscSubscriptions {
|
||||
// Each subscription runs in its own thread.
|
||||
go func(subcription RelayOscSubscription) {
|
||||
r.Log(DebugLog, "Started subscription: %s", subcription.Command)
|
||||
ticker := time.NewTicker(subcription.Interval)
|
||||
for range ticker.C {
|
||||
// Send OSC message as configured.
|
||||
r.Log(DebugLog, "Running subscription: %s", subcription.Command)
|
||||
oscMessage := osc.NewMessage(subcription.Command)
|
||||
oscMessage.Arguments = subcription.Payload
|
||||
err := r.OscSend(oscMessage)
|
||||
if err != nil {
|
||||
r.Log(ErrorLog, "Send Error: %s", err)
|
||||
}
|
||||
}
|
||||
}(subcription)
|
||||
}
|
||||
|
||||
// Send current config to MQTT.
|
||||
r.SendStatus()
|
||||
}
|
Loading…
Reference in New Issue
Block a user