mirror of
https://github.com/jwetzell/showbridge-go.git
synced 2026-04-26 21:05:30 +00:00
move route and module into internal
This commit is contained in:
73
internal/module/http-client.go
Normal file
73
internal/module/http-client.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type HTTPClient struct {
|
||||
config config.ModuleConfig
|
||||
ctx context.Context
|
||||
client *http.Client
|
||||
router route.RouteIO
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "net.http.client",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
|
||||
return &HTTPClient{config: config, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (hc *HTTPClient) Id() string {
|
||||
return hc.config.Id
|
||||
}
|
||||
|
||||
func (hc *HTTPClient) Type() string {
|
||||
return hc.config.Type
|
||||
}
|
||||
|
||||
func (hc *HTTPClient) Run() error {
|
||||
|
||||
hc.client = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
<-hc.ctx.Done()
|
||||
slog.Debug("router context done in module", "id", hc.config.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hc *HTTPClient) Output(payload any) error {
|
||||
|
||||
payloadRequest, ok := payload.(*http.Request)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("net.http.client is only able to output an http.Request")
|
||||
}
|
||||
|
||||
if hc.client == nil {
|
||||
return fmt.Errorf("net.http.client client is nil")
|
||||
}
|
||||
|
||||
response, err := hc.client.Do(payloadRequest)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hc.router != nil {
|
||||
hc.router.HandleInput(hc.config.Id, response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
109
internal/module/http-server.go
Normal file
109
internal/module/http-server.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
config config.ModuleConfig
|
||||
Port uint16
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
}
|
||||
|
||||
type ResponseData struct {
|
||||
Message string `json:"message"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "net.http.server",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
port, ok := params["port"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.http.server requires a port parameter")
|
||||
}
|
||||
|
||||
portNum, ok := port.(float64)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.http.server port must be uint16")
|
||||
}
|
||||
|
||||
return &HTTPServer{Port: uint16(portNum), config: config, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Id() string {
|
||||
return hs.config.Id
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Type() string {
|
||||
return hs.config.Type
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) HandleDefault(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
response := ResponseData{
|
||||
Message: "routing successful",
|
||||
Status: "ok",
|
||||
}
|
||||
|
||||
if hs.router != nil {
|
||||
routingErrors := hs.router.HandleInput(hs.config.Id, r)
|
||||
if routingErrors != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
response.Status = "error"
|
||||
response.Message = "routing failed"
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
response.Message = "routing successful"
|
||||
}
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
response.Message = "no router registered"
|
||||
response.Status = "error"
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Run() error {
|
||||
http.HandleFunc("/", hs.HandleDefault)
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", hs.Port),
|
||||
Handler: http.DefaultServeMux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-hs.ctx.Done()
|
||||
slog.Debug("router context done in module", "id", hs.config.Id)
|
||||
httpServer.Close()
|
||||
}()
|
||||
|
||||
err := httpServer.ListenAndServe()
|
||||
slog.Debug("net.http.server closed", "id", hs.config.Id)
|
||||
// TODO(jwetzell): handle server closed error differently
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-hs.ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Output(payload any) error {
|
||||
return fmt.Errorf("net.http.server output is not implemented")
|
||||
}
|
||||
73
internal/module/interval.go
Normal file
73
internal/module/interval.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type Interval struct {
|
||||
config config.ModuleConfig
|
||||
Duration uint32
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
ticker *time.Ticker
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "gen.interval",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
|
||||
duration, ok := params["duration"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("gen.interval requires a duration parameter")
|
||||
}
|
||||
|
||||
durationNum, ok := duration.(float64)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("gen.interval duration must be number")
|
||||
}
|
||||
|
||||
return &Interval{Duration: uint32(durationNum), config: config, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (i *Interval) Id() string {
|
||||
return i.config.Id
|
||||
}
|
||||
|
||||
func (i *Interval) Type() string {
|
||||
return i.config.Type
|
||||
}
|
||||
|
||||
func (i *Interval) Run() error {
|
||||
ticker := time.NewTicker(time.Millisecond * time.Duration(i.Duration))
|
||||
i.ticker = ticker
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-i.ctx.Done():
|
||||
slog.Debug("router context done in module", "id", i.config.Id)
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
if i.router != nil {
|
||||
i.router.HandleInput(i.config.Id, time.Now())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (i *Interval) Output(payload any) error {
|
||||
i.ticker.Reset(time.Millisecond * time.Duration(i.Duration))
|
||||
return nil
|
||||
}
|
||||
118
internal/module/midi-client.go
Normal file
118
internal/module/midi-client.go
Normal file
@@ -0,0 +1,118 @@
|
||||
//go:build cgo
|
||||
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
"gitlab.com/gomidi/midi/v2"
|
||||
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
|
||||
)
|
||||
|
||||
type MIDIClient struct {
|
||||
config config.ModuleConfig
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
InputPort string
|
||||
OutputPort string
|
||||
SendFunc func(midi.Message) error
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
//TODO(jwetzell): find a better namespace than "misc"
|
||||
Type: "misc.midi.client",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
input, ok := params["input"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("misc.midi.client requires a input parameter")
|
||||
}
|
||||
|
||||
inputString, ok := input.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("misc.midi.client input must be a string")
|
||||
}
|
||||
|
||||
output, ok := params["output"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("misc.midi.client requires a output parameter")
|
||||
}
|
||||
|
||||
outputString, ok := output.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("misc.midi.client output must be a string")
|
||||
}
|
||||
|
||||
return &MIDIClient{config: config, InputPort: inputString, OutputPort: outputString, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (mc *MIDIClient) Id() string {
|
||||
return mc.config.Id
|
||||
}
|
||||
|
||||
func (mc *MIDIClient) Type() string {
|
||||
return mc.config.Type
|
||||
}
|
||||
|
||||
func (mc *MIDIClient) Run() error {
|
||||
defer midi.CloseDriver()
|
||||
|
||||
in, err := midi.FindInPort(mc.InputPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("misc.midi.client can't find input port: %s", mc.InputPort)
|
||||
}
|
||||
|
||||
stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) {
|
||||
if mc.router != nil {
|
||||
mc.router.HandleInput(mc.Id(), msg)
|
||||
}
|
||||
}, midi.UseSysEx())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stop()
|
||||
|
||||
out, err := midi.FindOutPort(mc.OutputPort)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("misc.midi.client can't find output port: %s", mc.OutputPort)
|
||||
}
|
||||
|
||||
send, err := midi.SendTo(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mc.SendFunc = send
|
||||
|
||||
<-mc.ctx.Done()
|
||||
slog.Debug("router context done in module", "id", mc.config.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *MIDIClient) Output(payload any) error {
|
||||
if mc.SendFunc == nil {
|
||||
return fmt.Errorf("misc.midi.client output is not setup")
|
||||
}
|
||||
|
||||
payloadMessage, ok := payload.(midi.Message)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("misc.midi.client can only ouptut midi.Message")
|
||||
}
|
||||
|
||||
return mc.SendFunc(payloadMessage)
|
||||
}
|
||||
51
internal/module/module.go
Normal file
51
internal/module/module.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type ModuleError struct {
|
||||
Index int
|
||||
Config config.ModuleConfig
|
||||
Error error
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
Id() string
|
||||
Type() string
|
||||
Run() error
|
||||
Output(any) error
|
||||
}
|
||||
|
||||
type ModuleRegistration struct {
|
||||
Type string `json:"type"`
|
||||
New func(context.Context, config.ModuleConfig, route.RouteIO) (Module, error)
|
||||
}
|
||||
|
||||
func RegisterModule(mod ModuleRegistration) {
|
||||
|
||||
if mod.Type == "" {
|
||||
panic("module type is missing")
|
||||
}
|
||||
if mod.New == nil {
|
||||
panic("missing ModuleInfo.New")
|
||||
}
|
||||
|
||||
moduleRegistryMu.Lock()
|
||||
defer moduleRegistryMu.Unlock()
|
||||
|
||||
if _, ok := ModuleRegistry[string(mod.Type)]; ok {
|
||||
panic(fmt.Sprintf("module already registered: %s", mod.Type))
|
||||
}
|
||||
ModuleRegistry[string(mod.Type)] = mod
|
||||
}
|
||||
|
||||
var (
|
||||
moduleRegistryMu sync.RWMutex
|
||||
ModuleRegistry = make(map[string]ModuleRegistration)
|
||||
)
|
||||
127
internal/module/mqtt-client.go
Normal file
127
internal/module/mqtt-client.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/processing"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type MQTTClient struct {
|
||||
config config.ModuleConfig
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
Broker string
|
||||
ClientID string
|
||||
Topic string
|
||||
client mqtt.Client
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "net.mqtt.client",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
broker, ok := params["broker"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.mqtt.client requires a broker parameter")
|
||||
}
|
||||
|
||||
brokerString, ok := broker.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.mqtt.client broker must be string")
|
||||
}
|
||||
|
||||
topic, ok := params["topic"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.mqtt.client requires a topic parameter")
|
||||
}
|
||||
|
||||
topicString, ok := topic.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.mqtt.client topic must be string")
|
||||
}
|
||||
|
||||
clientId, ok := params["clientId"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.mqtt.client requires a clientId parameter")
|
||||
}
|
||||
|
||||
clientIdString, ok := clientId.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.mqtt.client clientId must be string")
|
||||
}
|
||||
|
||||
return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (mc *MQTTClient) Id() string {
|
||||
return mc.config.Id
|
||||
}
|
||||
|
||||
func (mc *MQTTClient) Type() string {
|
||||
return mc.config.Type
|
||||
}
|
||||
|
||||
func (mc *MQTTClient) Run() error {
|
||||
opts := mqtt.NewClientOptions()
|
||||
opts.AddBroker(mc.Broker)
|
||||
opts.SetClientID(mc.ClientID)
|
||||
opts.SetAutoReconnect(true)
|
||||
opts.SetCleanSession(false)
|
||||
|
||||
opts.OnConnect = func(c mqtt.Client) {
|
||||
token := mc.client.Subscribe(mc.Topic, 1, func(c mqtt.Client, m mqtt.Message) {
|
||||
mc.router.HandleInput(mc.config.Id, m)
|
||||
})
|
||||
token.Wait()
|
||||
}
|
||||
|
||||
mc.client = mqtt.NewClient(opts)
|
||||
|
||||
token := mc.client.Connect()
|
||||
|
||||
token.Wait()
|
||||
err := token.Error()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-mc.ctx.Done()
|
||||
slog.Debug("router context done in module", "id", mc.config.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *MQTTClient) Output(payload any) error {
|
||||
payloadMessage, ok := payload.(processing.MQTTMessage)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("net.mqtt.client is only able to output a MQTTMessage")
|
||||
}
|
||||
|
||||
if mc.client == nil {
|
||||
return fmt.Errorf("net.mqtt.client client is not setup")
|
||||
}
|
||||
|
||||
if !mc.client.IsConnected() {
|
||||
return fmt.Errorf("net.mqtt.client is not connected")
|
||||
}
|
||||
|
||||
token := mc.client.Publish(payloadMessage.Topic, payloadMessage.QoS, payloadMessage.Retained, payloadMessage.Payload)
|
||||
|
||||
token.Wait()
|
||||
|
||||
return token.Error()
|
||||
}
|
||||
113
internal/module/nats-client.go
Normal file
113
internal/module/nats-client.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/processing"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
type NATSClient struct {
|
||||
config config.ModuleConfig
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
URL string
|
||||
Subject string
|
||||
client *nats.Conn
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "net.nats.client",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
url, ok := params["url"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.nats.client requires a url parameter")
|
||||
}
|
||||
|
||||
urlString, ok := url.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.nats.client url must be string")
|
||||
}
|
||||
|
||||
subject, ok := params["subject"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.nats.client requires a subject parameter")
|
||||
}
|
||||
|
||||
subjectString, ok := subject.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.nats.client subject must be string")
|
||||
}
|
||||
|
||||
return &NATSClient{config: config, URL: urlString, Subject: subjectString, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (nc *NATSClient) Id() string {
|
||||
return nc.config.Id
|
||||
}
|
||||
|
||||
func (nc *NATSClient) Type() string {
|
||||
return nc.config.Type
|
||||
}
|
||||
|
||||
func (nc *NATSClient) Run() error {
|
||||
client, err := nats.Connect(nc.URL, nats.RetryOnFailedConnect(true))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nc.client = client
|
||||
|
||||
defer client.Drain()
|
||||
defer client.Close()
|
||||
|
||||
sub, err := nc.client.Subscribe(nc.Subject, func(msg *nats.Msg) {
|
||||
if nc.router != nil {
|
||||
nc.router.HandleInput(nc.config.Id, msg)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
<-nc.ctx.Done()
|
||||
slog.Debug("router context done in module", "id", nc.config.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nc *NATSClient) Output(payload any) error {
|
||||
|
||||
payloadMessage, ok := payload.(processing.NATSMessage)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("net.nats.client is only able to output NATSMessage")
|
||||
}
|
||||
|
||||
if nc.client == nil {
|
||||
return fmt.Errorf("net.nats.client client is not setup")
|
||||
}
|
||||
|
||||
if !nc.client.IsConnected() {
|
||||
return fmt.Errorf("net.nats.client is not connected")
|
||||
}
|
||||
|
||||
err := nc.client.Publish(payloadMessage.Subject, payloadMessage.Payload)
|
||||
|
||||
return err
|
||||
}
|
||||
96
internal/module/psn-client.go
Normal file
96
internal/module/psn-client.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/jwetzell/psn-go"
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type PSNClient struct {
|
||||
config config.ModuleConfig
|
||||
conn *net.UDPConn
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
decoder *psn.Decoder
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "net.psn.client",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
|
||||
return &PSNClient{config: config, decoder: psn.NewDecoder(), ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (pc *PSNClient) Id() string {
|
||||
return pc.config.Id
|
||||
}
|
||||
|
||||
func (pc *PSNClient) Type() string {
|
||||
return pc.config.Type
|
||||
}
|
||||
|
||||
func (pc *PSNClient) Run() error {
|
||||
|
||||
addr, err := net.ResolveUDPAddr("udp", "236.10.10.10:56565")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := net.ListenMulticastUDP("udp", nil, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
pc.conn = client
|
||||
|
||||
buffer := make([]byte, 2048)
|
||||
for {
|
||||
select {
|
||||
case <-pc.ctx.Done():
|
||||
// TODO(jwetzell): cleanup?
|
||||
slog.Debug("router context done in module", "id", pc.config.Id)
|
||||
return nil
|
||||
default:
|
||||
pc.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
|
||||
|
||||
numBytes, _, err := pc.conn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
//NOTE(jwetzell) we hit deadline
|
||||
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if numBytes > 0 {
|
||||
message := buffer[:numBytes]
|
||||
err := pc.decoder.Decode(message)
|
||||
if err != nil {
|
||||
slog.Error("net.psn.client problem decoding psn traffic", "id", pc.config.Id, "error", err)
|
||||
}
|
||||
|
||||
if pc.router != nil {
|
||||
for _, tracker := range pc.decoder.Trackers {
|
||||
pc.router.HandleInput(pc.config.Id, tracker)
|
||||
}
|
||||
} else {
|
||||
slog.Error("net.psn.client has no router", "id", pc.config.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *PSNClient) Output(payload any) error {
|
||||
return fmt.Errorf("net.psn.client output is not implemented")
|
||||
}
|
||||
172
internal/module/serial-client.go
Normal file
172
internal/module/serial-client.go
Normal file
@@ -0,0 +1,172 @@
|
||||
//go:build cgo
|
||||
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/framing"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
"go.bug.st/serial"
|
||||
)
|
||||
|
||||
type SerialClient struct {
|
||||
config config.ModuleConfig
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
Port string
|
||||
Framer framing.Framer
|
||||
Mode *serial.Mode
|
||||
port serial.Port
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
//TODO(jwetzell): find a better namespace than "misc"
|
||||
Type: "misc.serial.client",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
port, ok := params["port"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("misc.serial.client requires a port parameter")
|
||||
}
|
||||
|
||||
portString, ok := port.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("misc.serial.client port must be a string")
|
||||
}
|
||||
|
||||
framingMethod, ok := params["framing"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("misc.serial.client requires a framing method")
|
||||
}
|
||||
|
||||
framingMethodString, ok := framingMethod.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("misc.serial.client framing method must be a string")
|
||||
}
|
||||
|
||||
framer, err := framing.GetFramer(framingMethodString)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buadRate, ok := params["baudRate"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("misc.serial.client requires a baudRate parameter")
|
||||
}
|
||||
|
||||
baudRateNum, ok := buadRate.(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("misc.serial.client baudRate must be a number")
|
||||
}
|
||||
|
||||
mode := serial.Mode{
|
||||
BaudRate: int(baudRateNum),
|
||||
}
|
||||
|
||||
return &SerialClient{config: config, Port: portString, Framer: framer, Mode: &mode, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (mc *SerialClient) Id() string {
|
||||
return mc.config.Id
|
||||
}
|
||||
|
||||
func (mc *SerialClient) Type() string {
|
||||
return mc.config.Type
|
||||
}
|
||||
|
||||
func (mc *SerialClient) SetupPort() error {
|
||||
|
||||
port, err := serial.Open(mc.Port, mc.Mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("misc.serial.client can't open input port: %s", mc.Port)
|
||||
}
|
||||
|
||||
mc.port = port
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *SerialClient) Run() error {
|
||||
|
||||
// TODO(jwetzell): shutdown with router.Context properly
|
||||
go func() {
|
||||
<-mc.ctx.Done()
|
||||
slog.Debug("router context done in module", "id", mc.config.Id)
|
||||
if mc.port != nil {
|
||||
mc.port.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
err := mc.SetupPort()
|
||||
if err != nil {
|
||||
if mc.ctx.Err() != nil {
|
||||
slog.Debug("router context done in module", "id", mc.config.Id)
|
||||
return nil
|
||||
}
|
||||
slog.Error("misc.serial.client", "id", mc.config.Id, "error", err.Error())
|
||||
time.Sleep(time.Second * 2)
|
||||
continue
|
||||
}
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
select {
|
||||
case <-mc.ctx.Done():
|
||||
slog.Debug("router context done in module", "id", mc.config.Id)
|
||||
return nil
|
||||
default:
|
||||
READ:
|
||||
for {
|
||||
select {
|
||||
case <-mc.ctx.Done():
|
||||
slog.Debug("router context done in module", "id", mc.config.Id)
|
||||
return nil
|
||||
default:
|
||||
byteCount, err := mc.port.Read(buffer)
|
||||
|
||||
if err != nil {
|
||||
mc.Framer.Clear()
|
||||
break READ
|
||||
}
|
||||
|
||||
if mc.Framer != nil {
|
||||
if byteCount > 0 {
|
||||
messages := mc.Framer.Decode(buffer[0:byteCount])
|
||||
for _, message := range messages {
|
||||
if mc.router != nil {
|
||||
mc.router.HandleInput(mc.config.Id, message)
|
||||
} else {
|
||||
slog.Error("misc.serial.client has no router", "id", mc.config.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *SerialClient) Output(payload any) error {
|
||||
|
||||
payloadBytes, ok := payload.([]byte)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("misc.serial.client can only ouptut bytes")
|
||||
}
|
||||
|
||||
_, err := mc.port.Write(mc.Framer.Encode(payloadBytes))
|
||||
return err
|
||||
}
|
||||
168
internal/module/tcp-client.go
Normal file
168
internal/module/tcp-client.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/framing"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type TCPClient struct {
|
||||
config config.ModuleConfig
|
||||
framer framing.Framer
|
||||
conn *net.TCPConn
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
Addr *net.TCPAddr
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "net.tcp.client",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
host, ok := params["host"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.client requires a host parameter")
|
||||
}
|
||||
|
||||
hostString, ok := host.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.client host must be string")
|
||||
}
|
||||
|
||||
port, ok := params["port"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.client requires a port parameter")
|
||||
}
|
||||
|
||||
portNum, ok := port.(float64)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.client port must be a number")
|
||||
}
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
framingMethod, ok := params["framing"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.client requires a framing method")
|
||||
}
|
||||
|
||||
framingMethodString, ok := framingMethod.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.client framing method must be a string")
|
||||
}
|
||||
|
||||
framer, err := framing.GetFramer(framingMethodString)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TCPClient{framer: framer, Addr: addr, config: config, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (tc *TCPClient) Id() string {
|
||||
return tc.config.Id
|
||||
}
|
||||
|
||||
func (tc *TCPClient) Type() string {
|
||||
return tc.config.Type
|
||||
}
|
||||
|
||||
func (tc *TCPClient) Run() error {
|
||||
|
||||
// TODO(jwetzell): shutdown with router.Context properly
|
||||
go func() {
|
||||
<-tc.ctx.Done()
|
||||
slog.Debug("router context done in module", "id", tc.config.Id)
|
||||
if tc.conn != nil {
|
||||
tc.conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
err := tc.SetupConn()
|
||||
if err != nil {
|
||||
if tc.ctx.Err() != nil {
|
||||
slog.Debug("router context done in module", "id", tc.config.Id)
|
||||
return nil
|
||||
}
|
||||
slog.Error("net.tcp.client", "id", tc.config.Id, "error", err.Error())
|
||||
time.Sleep(time.Second * 2)
|
||||
continue
|
||||
}
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
select {
|
||||
case <-tc.ctx.Done():
|
||||
slog.Debug("router context done in module", "id", tc.config.Id)
|
||||
return nil
|
||||
default:
|
||||
READ:
|
||||
for {
|
||||
select {
|
||||
case <-tc.ctx.Done():
|
||||
slog.Debug("router context done in module", "id", tc.config.Id)
|
||||
return nil
|
||||
default:
|
||||
byteCount, err := tc.conn.Read(buffer)
|
||||
|
||||
if err != nil {
|
||||
tc.framer.Clear()
|
||||
break READ
|
||||
}
|
||||
|
||||
if tc.framer != nil {
|
||||
if byteCount > 0 {
|
||||
messages := tc.framer.Decode(buffer[0:byteCount])
|
||||
for _, message := range messages {
|
||||
if tc.router != nil {
|
||||
tc.router.HandleInput(tc.config.Id, message)
|
||||
} else {
|
||||
slog.Error("net.tcp.client has no router", "id", tc.config.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *TCPClient) SetupConn() error {
|
||||
client, err := net.DialTCP("tcp", nil, tc.Addr)
|
||||
tc.conn = client
|
||||
return err
|
||||
}
|
||||
|
||||
func (tc *TCPClient) Output(payload any) error {
|
||||
// NOTE(jwetzell): not sure how this would occur but
|
||||
if tc.conn == nil {
|
||||
err := tc.SetupConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
payloadBytes, ok := payload.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("net.tcp.client is only able to output bytes")
|
||||
}
|
||||
_, err := tc.conn.Write(tc.framer.Encode(payloadBytes))
|
||||
return err
|
||||
}
|
||||
218
internal/module/tcp-server.go
Normal file
218
internal/module/tcp-server.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"slices"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/framing"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type TCPServer struct {
|
||||
config config.ModuleConfig
|
||||
Addr *net.TCPAddr
|
||||
Framer framing.Framer
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
quit chan interface{}
|
||||
wg sync.WaitGroup
|
||||
connections []*net.TCPConn
|
||||
connectionsMu sync.RWMutex
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "net.tcp.server",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
port, ok := params["port"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.server requires a port parameter")
|
||||
}
|
||||
|
||||
portNum, ok := port.(float64)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.server port must be a number")
|
||||
}
|
||||
|
||||
framingMethod, ok := params["framing"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.server requires a framing method")
|
||||
}
|
||||
|
||||
framingMethodString, ok := framingMethod.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.server framing method must be a string")
|
||||
}
|
||||
|
||||
framer, err := framing.GetFramer(framingMethodString)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ipString := "0.0.0.0"
|
||||
|
||||
ip, ok := params["ip"]
|
||||
if ok {
|
||||
|
||||
specificIpString, ok := ip.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.tcp.server ip must be a string")
|
||||
}
|
||||
ipString = specificIpString
|
||||
}
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TCPServer{Framer: framer, Addr: addr, config: config, quit: make(chan interface{}), ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ts *TCPServer) Id() string {
|
||||
return ts.config.Id
|
||||
}
|
||||
|
||||
func (ts *TCPServer) Type() string {
|
||||
return ts.config.Type
|
||||
}
|
||||
|
||||
func (ts *TCPServer) handleClient(client *net.TCPConn) {
|
||||
ts.connectionsMu.Lock()
|
||||
ts.connections = append(ts.connections, client)
|
||||
ts.connectionsMu.Unlock()
|
||||
slog.Debug("net.tcp.server connection accepted", "id", ts.config.Id, "remoteAddr", client.RemoteAddr().String())
|
||||
defer client.Close()
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
ClientRead:
|
||||
for {
|
||||
select {
|
||||
case <-ts.quit:
|
||||
return
|
||||
default:
|
||||
client.SetDeadline(time.Now().Add(time.Millisecond * 200))
|
||||
byteCount, err := client.Read(buffer)
|
||||
|
||||
if err != nil {
|
||||
if opErr, ok := err.(*net.OpError); ok {
|
||||
//NOTE(jwetzell) we hit deadline
|
||||
if opErr.Timeout() {
|
||||
continue ClientRead
|
||||
}
|
||||
if errors.Is(opErr, syscall.ECONNRESET) {
|
||||
ts.connectionsMu.Lock()
|
||||
for i := 0; i < len(ts.connections); i++ {
|
||||
if ts.connections[i] == client {
|
||||
ts.connections = slices.Delete(ts.connections, i, i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
slog.Debug("net.tcp.server connection reset", "id", ts.config.Id, "remoteAddr", client.RemoteAddr().String())
|
||||
ts.connectionsMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
if err.Error() == "EOF" {
|
||||
ts.connectionsMu.Lock()
|
||||
for i := 0; i < len(ts.connections); i++ {
|
||||
if ts.connections[i] == client {
|
||||
ts.connections = slices.Delete(ts.connections, i, i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
slog.Debug("net.tcp.server stream ended", "id", ts.config.Id, "remoteAddr", client.RemoteAddr().String())
|
||||
ts.connectionsMu.Unlock()
|
||||
}
|
||||
return
|
||||
}
|
||||
if ts.Framer != nil {
|
||||
if byteCount > 0 {
|
||||
messages := ts.Framer.Decode(buffer[0:byteCount])
|
||||
for _, message := range messages {
|
||||
if ts.router != nil {
|
||||
ts.router.HandleInput(ts.config.Id, message)
|
||||
} else {
|
||||
slog.Error("net.tcp.server has no router", "id", ts.config.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TCPServer) Run() error {
|
||||
listener, err := net.ListenTCP("tcp", ts.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ts.wg.Add(1)
|
||||
|
||||
go func() {
|
||||
<-ts.ctx.Done()
|
||||
close(ts.quit)
|
||||
listener.Close()
|
||||
slog.Debug("router context done in module", "id", ts.config.Id)
|
||||
}()
|
||||
|
||||
AcceptLoop:
|
||||
for {
|
||||
conn, err := listener.AcceptTCP()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ts.quit:
|
||||
break AcceptLoop
|
||||
default:
|
||||
slog.Debug("net.tcp.server problem with listener", "error", err)
|
||||
}
|
||||
} else {
|
||||
ts.wg.Add(1)
|
||||
go func() {
|
||||
ts.handleClient(conn)
|
||||
ts.wg.Done()
|
||||
}()
|
||||
}
|
||||
}
|
||||
ts.wg.Done()
|
||||
ts.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TCPServer) Output(payload any) error {
|
||||
payloadBytes, ok := payload.([]byte)
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("net.tcp.server is only able to output bytes")
|
||||
}
|
||||
ts.connectionsMu.Lock()
|
||||
errorString := ""
|
||||
|
||||
for _, connection := range ts.connections {
|
||||
_, err := connection.Write(payloadBytes)
|
||||
if err != nil {
|
||||
errorString += fmt.Sprintf("%s\n", err.Error())
|
||||
}
|
||||
}
|
||||
ts.connectionsMu.Unlock()
|
||||
|
||||
if errorString == "" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s", errorString)
|
||||
}
|
||||
71
internal/module/timer.go
Normal file
71
internal/module/timer.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type Timer struct {
|
||||
config config.ModuleConfig
|
||||
Duration uint32
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "gen.timer",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
|
||||
duration, ok := params["duration"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("gen.timer requires a duration parameter")
|
||||
}
|
||||
|
||||
durationNum, ok := duration.(float64)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("gen.timer duration must be a number")
|
||||
}
|
||||
|
||||
return &Timer{Duration: uint32(durationNum), config: config, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Timer) Id() string {
|
||||
return t.config.Id
|
||||
}
|
||||
|
||||
func (t *Timer) Type() string {
|
||||
return t.config.Type
|
||||
}
|
||||
|
||||
func (t *Timer) Run() error {
|
||||
t.timer = time.NewTimer(time.Millisecond * time.Duration(t.Duration))
|
||||
defer t.timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-t.ctx.Done():
|
||||
t.timer.Stop()
|
||||
slog.Debug("router context done in module", "id", t.config.Id)
|
||||
return nil
|
||||
case time := <-t.timer.C:
|
||||
if t.router != nil {
|
||||
t.router.HandleInput(t.config.Id, time)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Timer) Output(payload any) error {
|
||||
t.timer.Reset(time.Millisecond * time.Duration(t.Duration))
|
||||
return nil
|
||||
}
|
||||
100
internal/module/udp-client.go
Normal file
100
internal/module/udp-client.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type UDPClient struct {
|
||||
config config.ModuleConfig
|
||||
Addr *net.UDPAddr
|
||||
Port uint16
|
||||
conn *net.UDPConn
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "net.udp.client",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
host, ok := params["host"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.client requires a host parameter")
|
||||
}
|
||||
|
||||
hostString, ok := host.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.client host must be a string")
|
||||
}
|
||||
|
||||
port, ok := params["port"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.client requires a port parameter")
|
||||
}
|
||||
|
||||
portNum, ok := port.(float64)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.client port must be a number")
|
||||
}
|
||||
|
||||
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UDPClient{Addr: addr, config: config, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *UDPClient) Id() string {
|
||||
return uc.config.Id
|
||||
}
|
||||
|
||||
func (uc *UDPClient) Type() string {
|
||||
return uc.config.Type
|
||||
}
|
||||
|
||||
func (uc *UDPClient) Run() error {
|
||||
|
||||
client, err := net.DialUDP("udp", nil, uc.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.conn = client
|
||||
<-uc.ctx.Done()
|
||||
slog.Debug("router context done in module", "id", uc.config.Id)
|
||||
if uc.conn != nil {
|
||||
uc.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *UDPClient) Output(payload any) error {
|
||||
|
||||
payloadBytes, ok := payload.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("net.udp.client is only able to output bytes")
|
||||
}
|
||||
if uc.conn != nil {
|
||||
_, err := uc.conn.Write(payloadBytes)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("net.udp.client client is not setup")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
122
internal/module/udp-multicast.go
Normal file
122
internal/module/udp-multicast.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type UDPMulticast struct {
|
||||
config config.ModuleConfig
|
||||
conn *net.UDPConn
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
Addr *net.UDPAddr
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "net.udp.multicast",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
ip, ok := params["ip"]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.client requires am ip parameter")
|
||||
}
|
||||
|
||||
ipString, ok := ip.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.client ip must be a string")
|
||||
}
|
||||
|
||||
port, ok := params["port"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.client requires a port parameter")
|
||||
}
|
||||
|
||||
portNum, ok := port.(float64)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.client port must be a number")
|
||||
}
|
||||
|
||||
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UDPMulticast{config: config, Addr: addr, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (um *UDPMulticast) Id() string {
|
||||
return um.config.Id
|
||||
}
|
||||
|
||||
func (um *UDPMulticast) Type() string {
|
||||
return um.config.Type
|
||||
}
|
||||
|
||||
func (um *UDPMulticast) Run() error {
|
||||
|
||||
client, err := net.ListenMulticastUDP("udp", nil, um.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
um.conn = client
|
||||
|
||||
buffer := make([]byte, 2048)
|
||||
for {
|
||||
select {
|
||||
case <-um.ctx.Done():
|
||||
// TODO(jwetzell): cleanup?
|
||||
slog.Debug("router context done in module", "id", um.config.Id)
|
||||
return nil
|
||||
default:
|
||||
um.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
|
||||
|
||||
numBytes, _, err := um.conn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
//NOTE(jwetzell) we hit deadline
|
||||
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if numBytes > 0 {
|
||||
message := buffer[:numBytes]
|
||||
|
||||
if um.router != nil {
|
||||
um.router.HandleInput(um.config.Id, message)
|
||||
} else {
|
||||
slog.Error("net.udp.multicast has no router", "id", um.config.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (um *UDPMulticast) Output(payload any) error {
|
||||
|
||||
payloadBytes, ok := payload.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("net.udp.multicast can only output bytes")
|
||||
}
|
||||
|
||||
if um.conn == nil {
|
||||
return fmt.Errorf("net.udp.multicast connection is not setup")
|
||||
}
|
||||
|
||||
_, err := um.conn.Write(payloadBytes)
|
||||
return err
|
||||
}
|
||||
109
internal/module/udp-server.go
Normal file
109
internal/module/udp-server.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package module
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/route"
|
||||
)
|
||||
|
||||
type UDPServer struct {
|
||||
Addr *net.UDPAddr
|
||||
config config.ModuleConfig
|
||||
ctx context.Context
|
||||
router route.RouteIO
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterModule(ModuleRegistration{
|
||||
Type: "net.udp.server",
|
||||
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
|
||||
params := config.Params
|
||||
port, ok := params["port"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.server requires a port parameter")
|
||||
}
|
||||
|
||||
portNum, ok := port.(float64)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.server port must be a number")
|
||||
}
|
||||
|
||||
ipString := "0.0.0.0"
|
||||
|
||||
ip, ok := params["ip"]
|
||||
if ok {
|
||||
|
||||
specificIpString, ok := ip.(string)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("net.udp.server ip must be a string")
|
||||
}
|
||||
ipString = specificIpString
|
||||
}
|
||||
|
||||
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
|
||||
if err != nil {
|
||||
log.Fatalf("error resolving UDP address: %v", err)
|
||||
}
|
||||
|
||||
return &UDPServer{Addr: addr, config: config, ctx: ctx, router: router}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (us *UDPServer) Id() string {
|
||||
return us.config.Id
|
||||
}
|
||||
|
||||
func (us *UDPServer) Type() string {
|
||||
return us.config.Id
|
||||
}
|
||||
|
||||
func (us *UDPServer) Run() error {
|
||||
|
||||
listener, err := net.ListenUDP("udp", us.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer listener.Close()
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
select {
|
||||
case <-us.ctx.Done():
|
||||
// TODO(jwetzell): cleanup?
|
||||
slog.Debug("router context done in module", "id", us.config.Id)
|
||||
return nil
|
||||
default:
|
||||
listener.SetDeadline(time.Now().Add(time.Millisecond * 200))
|
||||
|
||||
numBytes, _, err := listener.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
//NOTE(jwetzell) we hit deadline
|
||||
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
message := buffer[:numBytes]
|
||||
if us.router != nil {
|
||||
us.router.HandleInput(us.config.Id, message)
|
||||
} else {
|
||||
slog.Error("net.udp.server has no router", "id", us.config.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (us *UDPServer) Output(payload any) error {
|
||||
return fmt.Errorf("net.udp.server output is not implemented")
|
||||
}
|
||||
86
internal/route/route.go
Normal file
86
internal/route/route.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jwetzell/showbridge-go/internal/config"
|
||||
"github.com/jwetzell/showbridge-go/internal/processing"
|
||||
)
|
||||
|
||||
type RouteError struct {
|
||||
Index int
|
||||
Config config.RouteConfig
|
||||
Error error
|
||||
}
|
||||
|
||||
type RouteIOError struct {
|
||||
Index int
|
||||
Error error
|
||||
}
|
||||
|
||||
type RouteIO interface {
|
||||
HandleInput(sourceId string, payload any) []RouteIOError
|
||||
HandleOutput(sourceId string, destinationId string, payload any) error
|
||||
}
|
||||
|
||||
type Route interface {
|
||||
Input() string
|
||||
Output() string
|
||||
HandleInput(ctx context.Context, sourceId string, payload any, router RouteIO) error
|
||||
HandleOutput(ctx context.Context, sourceId string, payload any, router RouteIO) error
|
||||
}
|
||||
|
||||
type ProcessorRoute struct {
|
||||
input string
|
||||
processors []processing.Processor
|
||||
output string
|
||||
}
|
||||
|
||||
func NewRoute(config config.RouteConfig) (Route, error) {
|
||||
processors := []processing.Processor{}
|
||||
|
||||
if len(config.Processors) > 0 {
|
||||
for _, processorDecl := range config.Processors {
|
||||
processorInfo, ok := processing.ProcessorRegistry[processorDecl.Type]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("problem loading processor registration for processor type: %s", processorDecl.Type)
|
||||
}
|
||||
|
||||
processor, err := processorInfo.New(processorDecl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
processors = append(processors, processor)
|
||||
}
|
||||
}
|
||||
|
||||
return &ProcessorRoute{input: config.Input, processors: processors, output: config.Output}, nil
|
||||
}
|
||||
|
||||
func (r *ProcessorRoute) Input() string {
|
||||
return r.input
|
||||
}
|
||||
|
||||
func (r *ProcessorRoute) Output() string {
|
||||
return r.output
|
||||
}
|
||||
|
||||
func (r *ProcessorRoute) HandleInput(ctx context.Context, sourceId string, payload any, router RouteIO) error {
|
||||
var err error
|
||||
for _, processor := range r.processors {
|
||||
payload, err = processor.Process(ctx, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//NOTE(jwetzell) nil payload will result in the route being "terminated"
|
||||
if payload == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return r.HandleOutput(ctx, sourceId, payload, router)
|
||||
}
|
||||
|
||||
func (r *ProcessorRoute) HandleOutput(ctx context.Context, sourceId string, payload any, router RouteIO) error {
|
||||
return router.HandleOutput(sourceId, r.output, payload)
|
||||
}
|
||||
Reference in New Issue
Block a user