move route and module into internal

This commit is contained in:
Joel Wetzell
2025-12-07 10:34:58 -06:00
parent 3f6914282b
commit a5deea6447
17 changed files with 172 additions and 122 deletions

View 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
}

View 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")
}

View 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
}

View 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
View 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)
)

View 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()
}

View 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
}

View 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")
}

View 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
}

View 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
}

View 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
View 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
}

View 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
}

View 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
}

View 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
View 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)
}