From 66097f729798bff7012c6f8b23eac4e4931f2e4f Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sat, 6 Dec 2025 13:55:41 -0600 Subject: [PATCH] add serial client module --- go.mod | 2 + go.sum | 4 ++ serial-client.go | 181 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 serial-client.go diff --git a/go.mod b/go.mod index 0591501..bd09602 100644 --- a/go.mod +++ b/go.mod @@ -11,11 +11,13 @@ require ( github.com/nats-io/nats.go v1.47.0 github.com/urfave/cli/v3 v3.6.1 gitlab.com/gomidi/midi/v2 v2.3.16 + go.bug.st/serial v1.6.4 modernc.org/quickjs v0.17.0 sigs.k8s.io/yaml v1.6.0 ) require ( + github.com/creack/goselect v0.1.2 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect diff --git a/go.sum b/go.sum index c53757a..b796e10 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -42,6 +44,8 @@ github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= gitlab.com/gomidi/midi/v2 v2.3.16 h1:yufWSENyjnJ4LFQa9BerzUm4E4aLfTyzw5nmnCteO0c= gitlab.com/gomidi/midi/v2 v2.3.16/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= diff --git a/serial-client.go b/serial-client.go new file mode 100644 index 0000000..9e77f0a --- /dev/null +++ b/serial-client.go @@ -0,0 +1,181 @@ +//go:build cgo + +package showbridge + +import ( + "fmt" + "log/slog" + "time" + + "github.com/jwetzell/showbridge-go/internal/framing" + "go.bug.st/serial" +) + +type SerialClient struct { + config ModuleConfig + router *Router + 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(config ModuleConfig) (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") + } + + var framer framing.Framer + + switch framingMethodString { + case "CR": + framer = framing.NewByteSeparatorFramer([]byte{'\r'}) + case "LF": + framer = framing.NewByteSeparatorFramer([]byte{'\n'}) + case "CRLF": + framer = framing.NewByteSeparatorFramer([]byte{'\r', '\n'}) + case "SLIP": + framer = framing.NewSlipFramer() + default: + return nil, fmt.Errorf("unknown framing method: %s", framingMethodString) + } + + 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}, nil + }, + }) +} + +func (mc *SerialClient) Id() string { + return mc.config.Id +} + +func (mc *SerialClient) Type() string { + return mc.config.Type +} + +func (mc *SerialClient) RegisterRouter(router *Router) { + mc.router = router +} + +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.router.Context.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.router.Context.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.router.Context.Done(): + slog.Debug("router context done in module", "id", mc.config.Id) + return nil + default: + READ: + for { + select { + case <-mc.router.Context.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 +}