First commit

This commit is contained in:
James Coleman 2023-04-20 22:28:28 -05:00
commit 40181226fb
9 changed files with 781 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
config.yaml
osc-mqtt-bridge

19
License.txt Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}