75 Commits

Author SHA1 Message Date
Joel Wetzell
9cb7abcc8f add commit to version 2025-12-16 20:45:05 -06:00
Joel Wetzell
991111deba add version flag to cli command 2025-12-16 20:38:26 -06:00
Joel Wetzell
9b1ca9da96 Merge pull request #17 from jwetzell/feat/midi-message-create
add midi.message.create processor
2025-12-16 20:09:57 -06:00
Joel Wetzell
b726745aa2 add alternate casings for type 2025-12-16 20:08:32 -06:00
Joel Wetzell
a8bcf7a785 add ControlChange and ProgramChange 2025-12-16 20:02:16 -06:00
Joel Wetzell
32d8633402 add midi processor to create messages 2025-12-16 19:54:21 -06:00
Joel Wetzell
f3aaa86a1c Merge pull request #16 from jwetzell/feat/split-midi
split midi.client into input/output
2025-12-16 19:43:20 -06:00
Joel Wetzell
25b06ffc6d split midi.client into input/output 2025-12-16 19:24:20 -06:00
Joel Wetzell
ca3d662df7 Merge pull request #15 from jwetzell/dependabot/go_modules/gitlab.com/gomidi/midi/v2-2.3.18
Bump gitlab.com/gomidi/midi/v2 from 2.3.16 to 2.3.18
2025-12-15 13:53:09 -06:00
dependabot[bot]
86fe006af8 Bump gitlab.com/gomidi/midi/v2 from 2.3.16 to 2.3.18
Bumps [gitlab.com/gomidi/midi/v2](https://gitlab.com/gomidi/midi) from 2.3.16 to 2.3.18.
- [Commits](https://gitlab.com/gomidi/midi/compare/v2.3.16...v2.3.18)

---
updated-dependencies:
- dependency-name: gitlab.com/gomidi/midi/v2
  dependency-version: 2.3.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 18:11:35 +00:00
Joel Wetzell
d94232871e add launch.json 2025-12-14 12:14:03 -06:00
Joel Wetzell
5c8605104d update README 2025-12-13 13:59:54 -06:00
Joel Wetzell
e7b43b6637 Merge pull request #14 from jwetzell/feat/sip-server
add some SIP modules
2025-12-13 13:37:20 -06:00
Joel Wetzell
2221207036 update to main line of diago/sipgo 2025-12-13 13:35:33 -06:00
Joel Wetzell
dca1535f5a cleanup some error handling 2025-12-13 13:35:08 -06:00
Joel Wetzell
8b2eaf3ef4 add output to SIP call server 2025-12-13 12:59:24 -06:00
Joel Wetzell
c19837df1e add sip module that emits on every call 2025-12-13 12:33:52 -06:00
Joel Wetzell
783c333b46 fix property name 2025-12-13 12:33:00 -06:00
Joel Wetzell
2497c9c8e4 Merge pull request #13 from jwetzell/feat/midi-processors
add more MIDI processors
2025-12-13 08:52:47 -06:00
Joel Wetzell
54a8164dd3 add midi message unpack to pul values out of common MIDI types 2025-12-13 08:51:29 -06:00
Joel Wetzell
2c6a2f5a36 add midi message type filter 2025-12-13 08:50:52 -06:00
Joel Wetzell
92f91cf260 add dialed number to sip.dtmf.server output 2025-12-12 21:37:09 -06:00
Joel Wetzell
5c94cddc74 set mor loggers in SIP libraries 2025-12-11 21:04:07 -06:00
Joel Wetzell
7a0e945ecd move sip server under own namespace 2025-12-11 20:50:32 -06:00
Joel Wetzell
1eaabf2e75 move packages to correct place in go.mod 2025-12-11 20:50:32 -06:00
Joel Wetzell
955dcca8c6 override transport layer logger with dummy logger 2025-12-11 20:50:32 -06:00
Joel Wetzell
d312baeb6e add real dumb SIP server 2025-12-11 20:50:32 -06:00
Joel Wetzell
00f78b5a50 split out from net and misc module namespaces 2025-12-11 19:34:57 -06:00
Joel Wetzell
b59da597ba use Id function instead of accessing config directly 2025-12-10 07:44:55 -06:00
Joel Wetzell
8ca105a0b6 implement mqtt.Message for internal MQTTMessage type 2025-12-10 07:31:18 -06:00
Joel Wetzell
e027f22f8b Add MIT License to the project 2025-12-09 08:30:14 -06:00
Joel Wetzell
55f5767d47 Merge pull request #11 from jwetzell/feat/raw-framing-default
use RAW framer if no framer specified
2025-12-08 22:44:11 -06:00
Joel Wetzell
66435fc2ba use RAW framer if no framer specified 2025-12-08 22:21:34 -06:00
Joel Wetzell
a85b0e64e0 add RAW framer for passthrough 2025-12-08 19:31:42 -06:00
Joel Wetzell
202d1aea1f add SetupConn method to net.udp.client 2025-12-08 19:22:28 -06:00
Joel Wetzell
3a7b484c76 up buffer size for net.udp.server 2025-12-08 18:03:33 -06:00
Joel Wetzell
ffb39b1034 add tests for framers 2025-12-08 18:03:09 -06:00
Joel Wetzell
bb78b7b08b add tests for string.split 2025-12-07 14:14:46 -06:00
Joel Wetzell
48ac2640d0 add tests for error cases 2025-12-07 14:14:37 -06:00
Joel Wetzell
25324a4dcd Merge pull request #10 from jwetzell/chore/processor-tests
add some basic processor tests
2025-12-07 11:31:05 -06:00
Joel Wetzell
bebcd56d70 add release not config for more labels 2025-12-07 11:28:56 -06:00
Joel Wetzell
ce8e18dd4f fix labeler config 2025-12-07 11:27:15 -06:00
Joel Wetzell
f4021d858b add basic tests for uint.parse 2025-12-07 11:20:50 -06:00
Joel Wetzell
008d7665bd add basic tests for int.parse 2025-12-07 11:20:43 -06:00
Joel Wetzell
917f728f14 add basic tests for float.parse 2025-12-07 11:20:37 -06:00
Joel Wetzell
e3b6783a98 add missing err checking in freed.create 2025-12-07 11:13:10 -06:00
Joel Wetzell
5e7ebd1bf1 rename framing to framer 2025-12-07 10:42:51 -06:00
Joel Wetzell
0bd43ca0c3 rename processing to processor 2025-12-07 10:41:14 -06:00
Joel Wetzell
2e7feede28 fix naming conflict on import 2025-12-07 10:35:11 -06:00
Joel Wetzell
a5deea6447 move route and module into internal 2025-12-07 10:34:58 -06:00
Joel Wetzell
3f6914282b reorder functions 2025-12-06 23:53:32 -06:00
Joel Wetzell
7a01ce6691 convert Route to interface 2025-12-06 23:50:44 -06:00
Joel Wetzell
5be40a5abf add TODO 2025-12-06 23:48:39 -06:00
Joel Wetzell
388851f8bd inject router on HandleInput 2025-12-06 23:08:05 -06:00
Joel Wetzell
9a06949b5b move router to module New func 2025-12-06 23:00:45 -06:00
Joel Wetzell
8d8347fc4d fix import 2025-12-06 23:00:28 -06:00
Joel Wetzell
07108918f1 move config to internal 2025-12-06 22:42:38 -06:00
Joel Wetzell
2c6502b622 add output to net.udp.multicast 2025-12-06 22:36:55 -06:00
Joel Wetzell
4c0f7c1723 move framing creation into shared place 2025-12-06 16:02:35 -06:00
Joel Wetzell
04103cc6ca Merge pull request #9 from jwetzell/feat/serial-client
add serial client module
2025-12-06 14:00:07 -06:00
Joel Wetzell
66097f7297 add serial client module 2025-12-06 13:55:41 -06:00
Joel Wetzell
3eb3ccca0c cleanup copy/paste 2025-12-06 11:01:30 -06:00
Joel Wetzell
55452d9dd0 load udp addr on init in net.udp.server 2025-12-06 11:00:42 -06:00
Joel Wetzell
ed4f7dc1f7 switch to using only TCPConn in net.tcp.server 2025-12-06 10:58:18 -06:00
Joel Wetzell
190bdd8b44 reuse the same UDP client for every output 2025-12-06 10:49:36 -06:00
Joel Wetzell
a7f889f6b5 Merge pull request #8 from jwetzell/feat/udp-multicast
add net.udp.multicast module
2025-12-06 10:36:47 -06:00
Joel Wetzell
5c5111a25e add net.udp.multicast module 2025-12-06 10:35:25 -06:00
Joel Wetzell
b31729fafe switch net.udp.client conn to pointer 2025-12-06 09:02:41 -06:00
Joel Wetzell
dff5430eb4 switch net.tcp.client conn to pointer 2025-12-06 09:02:30 -06:00
Joel Wetzell
eaca0dbf86 Merge pull request #7 from jwetzell/feat/tcp-server-output
support output from net.tcp.server
2025-12-06 08:59:21 -06:00
Joel Wetzell
aa3a1032f3 logging tweaks 2025-12-06 08:39:34 -06:00
Joel Wetzell
51a62f7fb2 cleanup 2025-12-06 08:23:40 -06:00
Joel Wetzell
2c8efcea4b add output to TCP server 2025-12-05 23:18:37 -06:00
Joel Wetzell
df1882b8f7 update psn-go 2025-12-04 23:11:30 -06:00
Joel Wetzell
1c8346cf65 cleanup error messages 2025-12-04 16:35:03 -06:00
83 changed files with 2996 additions and 936 deletions

20
.github/labeler.yml vendored
View File

@@ -1,10 +1,22 @@
framing: config:
- changed-files: - changed-files:
- any-glob-to-any-file: 'internal/framing/**' - any-glob-to-any-file: 'internal/config/**'
processing: framer:
- changed-files: - changed-files:
- any-glob-to-any-file: 'internal/processing/**' - any-glob-to-any-file: 'internal/framer/**'
module:
- changed-files:
- any-glob-to-any-file: 'internal/module/**'
processor:
- changed-files:
- any-glob-to-any-file: 'internal/processor/**'
route:
- changed-files:
- any-glob-to-any-file: 'internal/route/**'
cli: cli:
- changed-files: - changed-files:

17
.github/release.yml vendored
View File

@@ -5,12 +5,21 @@ changelog:
labels: labels:
- dependencies - dependencies
categories: categories:
- title: Framing 🖼 - title: Config
labels: labels:
- framing - config
- title: Processing 🏭 - title: Framer 🖼️
labels: labels:
- processing - framer
- title: Module 📦
labels:
- module
- title: Processor 🏭
labels:
- processor
- title: Route 🛣️
labels:
- route
- title: CLI ⌨️ - title: CLI ⌨️
labels: labels:
- cmd - cmd

16
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch showbridge",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "cmd/showbridge"
}
]
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Joel Wetzell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -24,6 +24,10 @@ Simple protocol router _/s_
- client - client
- MIDI - MIDI
- client (not included in pre-built binaries yet) - client (not included in pre-built binaries yet)
- [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol)
- call server
- [DTMF](https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling) server

View File

@@ -14,6 +14,8 @@ builds:
goarch: goarch:
- "amd64" - "amd64"
- "arm64" - "arm64"
ldflags:
- '-s -w -X main.version={{.RawVersion}}-{{.ShortCommit}}'
archives: archives:
- formats: [tar.gz] - formats: [tar.gz]

View File

@@ -8,13 +8,19 @@ import (
"os/signal" "os/signal"
"github.com/jwetzell/showbridge-go" "github.com/jwetzell/showbridge-go"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
var (
version = "dev"
)
func main() { func main() {
cmd := &cli.Command{ cmd := &cli.Command{
Name: "showbridge", Name: "showbridge",
Version: version,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "config", Name: "config",
@@ -54,19 +60,19 @@ func main() {
} }
func readConfig(configPath string) (showbridge.Config, error) { func readConfig(configPath string) (config.Config, error) {
cfg := config.Config{}
configBytes, err := os.ReadFile(configPath) configBytes, err := os.ReadFile(configPath)
if err != nil { if err != nil {
return showbridge.Config{}, err return config.Config{}, err
} }
config := showbridge.Config{} err = yaml.Unmarshal(configBytes, &cfg)
err = yaml.Unmarshal(configBytes, &config)
if err != nil { if err != nil {
return showbridge.Config{}, err return config.Config{}, err
} }
return config, nil return cfg, nil
} }

View File

@@ -1,6 +0,0 @@
package showbridge
type Config struct {
Modules []ModuleConfig `json:"modules"`
Routes []RouteConfig `json:"routes"`
}

25
go.mod
View File

@@ -4,33 +4,50 @@ go 1.25.3
require ( require (
github.com/eclipse/paho.mqtt.golang v1.5.1 github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111
github.com/emiago/sipgo v1.0.1-alpha.0.20251212165843-9c9bcdf9126f
github.com/expr-lang/expr v1.17.6 github.com/expr-lang/expr v1.17.6
github.com/jwetzell/free-d-go v0.1.0 github.com/jwetzell/free-d-go v0.1.0
github.com/jwetzell/osc-go v0.1.0 github.com/jwetzell/osc-go v0.1.0
github.com/jwetzell/psn-go v0.2.1 github.com/jwetzell/psn-go v0.3.0
github.com/nats-io/nats.go v1.47.0 github.com/nats-io/nats.go v1.47.0
github.com/urfave/cli/v3 v3.6.1 github.com/urfave/cli/v3 v3.6.1
gitlab.com/gomidi/midi/v2 v2.3.16 gitlab.com/gomidi/midi/v2 v2.3.18
go.bug.st/serial v1.6.4
modernc.org/quickjs v0.17.0 modernc.org/quickjs v0.17.0
sigs.k8s.io/yaml v1.6.0 sigs.k8s.io/yaml v1.6.0
) )
require ( require (
github.com/creack/goselect v0.1.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-audio/riff v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.16 // indirect
github.com/pion/rtp v1.8.26 // indirect
github.com/pion/srtp/v3 v3.0.9 // indirect
github.com/pion/transport/v3 v3.1.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/zaf/g711 v1.4.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.42.0 // indirect golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/net v0.44.0 // indirect golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.39.0 // indirect
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect
modernc.org/libc v1.66.10 // indirect modernc.org/libc v1.66.10 // indirect
modernc.org/libquickjs v0.12.2 // indirect modernc.org/libquickjs v0.12.2 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect

65
go.sum
View File

@@ -1,27 +1,47 @@
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111 h1:jqhOZbH40pf2jiUhGaBNk334wOtNYvAaXg/mHOXhy/Y=
github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111/go.mod h1:3vLCCq8/G/Ei5I64IHtrmBTag+nPLcgXcKeN1KkLtuc=
github.com/emiago/sipgo v1.0.1-alpha.0.20251212165843-9c9bcdf9126f h1:n3wpx7ZyJBr0popXyATnTUZp/AdLIHjKbnKzr4ruF7g=
github.com/emiago/sipgo v1.0.1-alpha.0.20251212165843-9c9bcdf9126f/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec= github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/jwetzell/free-d-go v0.1.0 h1:xHt6dvyit98X+OC3jVzV0aLidxbyzi3vI9QiYkteEtA= github.com/jwetzell/free-d-go v0.1.0 h1:xHt6dvyit98X+OC3jVzV0aLidxbyzi3vI9QiYkteEtA=
github.com/jwetzell/free-d-go v0.1.0/go.mod h1:KmrkooRARRaxJTBSPvwt/6IMAIaHH1R8bSA8cwbbELw= github.com/jwetzell/free-d-go v0.1.0/go.mod h1:KmrkooRARRaxJTBSPvwt/6IMAIaHH1R8bSA8cwbbELw=
github.com/jwetzell/osc-go v0.1.0 h1:EXxup5VWBErHot2Ri4MFToPf6KCzLDTbCt2x6GLfw8I= github.com/jwetzell/osc-go v0.1.0 h1:EXxup5VWBErHot2Ri4MFToPf6KCzLDTbCt2x6GLfw8I=
github.com/jwetzell/osc-go v0.1.0/go.mod h1:xLz0jTwebSxtx1TkKN1YVdeRqvpFNweDhTut5TE393A= github.com/jwetzell/osc-go v0.1.0/go.mod h1:xLz0jTwebSxtx1TkKN1YVdeRqvpFNweDhTut5TE393A=
github.com/jwetzell/psn-go v0.2.1 h1:pNG6XNfVRTb4qctH6pJjRJ1ReYGnGgNRA4H7tNbmzRU= github.com/jwetzell/psn-go v0.3.0 h1:WVpCEmExYE8a+I5hQak5jNJJp2x35VdGX/VuMUKPmhY=
github.com/jwetzell/psn-go v0.2.1/go.mod h1:bcEAeti4sQM375buujb3mIfmUstD4Aby18gq3ENb6+o= github.com/jwetzell/psn-go v0.3.0/go.mod h1:bcEAeti4sQM375buujb3mIfmUstD4Aby18gq3ENb6+o=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
@@ -32,16 +52,34 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= 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= github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
gitlab.com/gomidi/midi/v2 v2.3.16 h1:yufWSENyjnJ4LFQa9BerzUm4E4aLfTyzw5nmnCteO0c= github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
gitlab.com/gomidi/midi/v2 v2.3.16/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw= github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
gitlab.com/gomidi/midi/v2 v2.3.18 h1:sj2fOhtvOe+zI8YJe8qTxLw5zv0ntULLUDwcFOaZQbI=
gitlab.com/gomidi/midi/v2 v2.3.18/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 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 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= go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
@@ -54,17 +92,22 @@ golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 h1:xeVptzkP8BuJhoIjNizd2bRHfq9KB9HfOLZu90T04XM=
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=

23
internal/config/config.go Normal file
View File

@@ -0,0 +1,23 @@
package config
type Config struct {
Modules []ModuleConfig `json:"modules"`
Routes []RouteConfig `json:"routes"`
}
type ModuleConfig struct {
Id string `json:"id"`
Type string `json:"type"`
Params map[string]any `json:"params"`
}
type RouteConfig struct {
Input string `json:"input"`
Processors []ProcessorConfig `json:"processors"`
Output string `json:"output"`
}
type ProcessorConfig struct {
Type string `json:"type"`
Params map[string]any `json:"params"`
}

29
internal/framer/framer.go Normal file
View File

@@ -0,0 +1,29 @@
package framer
import (
"fmt"
)
type Framer interface {
Decode([]byte) [][]byte
Encode([]byte) []byte
Clear()
Buffer() []byte
}
func GetFramer(framingType string) (Framer, error) {
switch framingType {
case "CR":
return NewByteSeparatorFramer([]byte{'\r'}), nil
case "LF":
return NewByteSeparatorFramer([]byte{'\n'}), nil
case "CRLF":
return NewByteSeparatorFramer([]byte{'\r', '\n'}), nil
case "SLIP":
return NewSlipFramer(), nil
case "RAW":
return NewRawFramer(), nil
default:
return nil, fmt.Errorf("unknown framing method: %s", framingType)
}
}

23
internal/framer/raw.go Normal file
View File

@@ -0,0 +1,23 @@
package framer
type RawFramer struct{}
func NewRawFramer() *RawFramer {
return &RawFramer{}
}
func (rf *RawFramer) Decode(data []byte) [][]byte {
return [][]byte{data}
}
func (rf *RawFramer) Encode(data []byte) []byte {
return data
}
func (rf *RawFramer) Clear() {
// NOTE(jwetzell): no internal state to clear
}
func (rf *RawFramer) Buffer() []byte {
return []byte{}
}

View File

@@ -1,4 +1,4 @@
package framing package framer
import ( import (
"bytes" "bytes"
@@ -35,3 +35,7 @@ func (bsf *ByteSeparatorFramer) Encode(data []byte) []byte {
func (bsf *ByteSeparatorFramer) Clear() { func (bsf *ByteSeparatorFramer) Clear() {
bsf.buffer = []byte{} bsf.buffer = []byte{}
} }
func (bsf *ByteSeparatorFramer) Buffer() []byte {
return bsf.buffer
}

View File

@@ -0,0 +1,69 @@
package framer_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/framer"
)
func TestGoodSeparatorFramer(t *testing.T) {
tests := []struct {
name string
framer framer.Framer
input []byte
expected [][]byte
buffer []byte
}{
{
name: "new line separator",
framer: framer.NewByteSeparatorFramer([]byte{0x0a}),
input: []byte("Hello\nWorld\nThis is a test\n"),
expected: [][]byte{
[]byte("Hello"),
[]byte("World"),
[]byte("This is a test"),
},
buffer: []byte{},
},
{
name: "CRLF separator",
framer: framer.NewByteSeparatorFramer([]byte{0x0d, 0x0a}),
input: []byte("Hello\r\nWorld\r\nThis is a test\r\n"),
expected: [][]byte{
[]byte("Hello"),
[]byte("World"),
[]byte("This is a test"),
},
buffer: []byte{},
},
{
name: "extra data after separator",
framer: framer.NewByteSeparatorFramer([]byte{0x0d, 0x0a}),
input: []byte("Hello\r\nWorld\r\nThis is a test\r\nextra"),
expected: [][]byte{
[]byte("Hello"),
[]byte("World"),
[]byte("This is a test"),
},
buffer: []byte("extra"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frames := test.framer.Decode(test.input)
if len(frames) != len(test.expected) {
t.Errorf("separator framer got %d frames, expected %d", len(frames), len(test.expected))
}
for i, frame := range frames {
if !slices.Equal(frame, test.expected[i]) {
t.Errorf("separator framer frame %d got %s, expected %s", i, frame, test.expected[i])
}
}
if !slices.Equal(test.framer.Buffer(), test.buffer) {
t.Errorf("separator framer buffer got %s, expected %s", test.framer.Buffer(), test.buffer)
}
})
}
}

View File

@@ -1,4 +1,4 @@
package framing package framer
type SlipFramer struct { type SlipFramer struct {
buffer []byte buffer []byte
@@ -74,3 +74,7 @@ func (sf *SlipFramer) Encode(data []byte) []byte {
func (sf *SlipFramer) Clear() { func (sf *SlipFramer) Clear() {
sf.buffer = []byte{} sf.buffer = []byte{}
} }
func (sf *SlipFramer) Buffer() []byte {
return sf.buffer
}

View File

@@ -0,0 +1,45 @@
package framer_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/framer"
)
func TestGoodSLIPFramer(t *testing.T) {
tests := []struct {
name string
framer framer.Framer
input []byte
expected [][]byte
buffer []byte
}{
{
name: "OSC SLIP messages",
framer: framer.NewSlipFramer(),
input: []byte{0xc0, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xc0},
expected: [][]byte{
{0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00},
},
buffer: []byte{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frames := test.framer.Decode(test.input)
if len(frames) != len(test.expected) {
t.Errorf("SLIP framer got %d frames, expected %d", len(frames), len(test.expected))
}
for i, frame := range frames {
if !slices.Equal(frame, test.expected[i]) {
t.Errorf("SLIP framer frame %d got %s, expected %s", i, frame, test.expected[i])
}
}
if !slices.Equal(test.framer.Buffer(), test.buffer) {
t.Errorf("SLIP framer buffer got %s, expected %s", test.framer.Buffer(), test.buffer)
}
})
}
}

View File

@@ -1,7 +0,0 @@
package framing
type Framer interface {
Decode([]byte) [][]byte
Encode([]byte) []byte
Clear()
}

View File

@@ -1,24 +1,29 @@
package showbridge package module
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"time" "time"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type HTTPClient struct { type HTTPClient struct {
config ModuleConfig config config.ModuleConfig
router *Router ctx context.Context
client *http.Client client *http.Client
router route.RouteIO
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.http.client", Type: "http.client",
New: func(config ModuleConfig) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
return &HTTPClient{config: config}, nil return &HTTPClient{config: config, ctx: ctx, router: router}, nil
}, },
}) })
} }
@@ -31,18 +36,14 @@ func (hc *HTTPClient) Type() string {
return hc.config.Type return hc.config.Type
} }
func (hc *HTTPClient) RegisterRouter(router *Router) {
hc.router = router
}
func (hc *HTTPClient) Run() error { func (hc *HTTPClient) Run() error {
hc.client = &http.Client{ hc.client = &http.Client{
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
} }
<-hc.router.Context.Done() <-hc.ctx.Done()
slog.Debug("router context done in module", "id", hc.config.Id) slog.Debug("router context done in module", "id", hc.Id())
return nil return nil
} }
@@ -51,11 +52,11 @@ func (hc *HTTPClient) Output(payload any) error {
payloadRequest, ok := payload.(*http.Request) payloadRequest, ok := payload.(*http.Request)
if !ok { if !ok {
return fmt.Errorf("net.http.client is only able to output an http.Request") return fmt.Errorf("http.client is only able to output an http.Request")
} }
if hc.client == nil { if hc.client == nil {
return fmt.Errorf("net.http.client client is nil") return fmt.Errorf("http.client client is nil")
} }
response, err := hc.client.Do(payloadRequest) response, err := hc.client.Do(payloadRequest)
@@ -65,7 +66,7 @@ func (hc *HTTPClient) Output(payload any) error {
} }
if hc.router != nil { if hc.router != nil {
hc.router.HandleInput(hc.config.Id, response) hc.router.HandleInput(hc.Id(), response)
} }
return nil return nil

View File

@@ -1,16 +1,21 @@
package showbridge package module
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type HTTPServer struct { type HTTPServer struct {
config ModuleConfig config config.ModuleConfig
Port uint16 Port uint16
router *Router ctx context.Context
router route.RouteIO
} }
type ResponseData struct { type ResponseData struct {
@@ -20,21 +25,21 @@ type ResponseData struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.http.server", Type: "http.server",
New: func(config ModuleConfig) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] port, ok := params["port"]
if !ok { if !ok {
return nil, fmt.Errorf("net.http.server requires a port parameter") return nil, fmt.Errorf("http.server requires a port parameter")
} }
portNum, ok := port.(float64) portNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("net.http.server port must be uint16") return nil, fmt.Errorf("http.server port must be uint16")
} }
return &HTTPServer{Port: uint16(portNum), config: config}, nil return &HTTPServer{Port: uint16(portNum), config: config, ctx: ctx, router: router}, nil
}, },
}) })
} }
@@ -47,10 +52,6 @@ func (hs *HTTPServer) Type() string {
return hs.config.Type return hs.config.Type
} }
func (hs *HTTPServer) RegisterRouter(router *Router) {
hs.router = router
}
func (hs *HTTPServer) HandleDefault(w http.ResponseWriter, r *http.Request) { func (hs *HTTPServer) HandleDefault(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -60,7 +61,7 @@ func (hs *HTTPServer) HandleDefault(w http.ResponseWriter, r *http.Request) {
} }
if hs.router != nil { if hs.router != nil {
routingErrors := hs.router.HandleInput(hs.config.Id, r) routingErrors := hs.router.HandleInput(hs.Id(), r)
if routingErrors != nil { if routingErrors != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
response.Status = "error" response.Status = "error"
@@ -87,21 +88,22 @@ func (hs *HTTPServer) Run() error {
} }
go func() { go func() {
<-hs.router.Context.Done() <-hs.ctx.Done()
slog.Debug("router context done in module", "id", hs.config.Id) slog.Debug("router context done in module", "id", hs.Id())
httpServer.Close() httpServer.Close()
}() }()
err := httpServer.ListenAndServe() err := httpServer.ListenAndServe()
slog.Debug("net.http.server closed", "id", hs.config.Id) slog.Debug("http.server closed", "id", hs.Id())
// TODO(jwetzell): handle server closed error differently
if err != nil { if err != nil {
return err return err
} }
<-hs.router.Context.Done() <-hs.ctx.Done()
return nil return nil
} }
func (hs *HTTPServer) Output(payload any) error { func (hs *HTTPServer) Output(payload any) error {
return fmt.Errorf("net.http.server output is not implemented") return fmt.Errorf("http.server output is not implemented")
} }

View File

@@ -1,36 +1,41 @@
package showbridge package module
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"time" "time"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type Interval struct { type Interval struct {
config ModuleConfig config config.ModuleConfig
Duration uint32 Duration uint32
router *Router ctx context.Context
router route.RouteIO
ticker *time.Ticker ticker *time.Ticker
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "gen.interval", Type: "gen.interval",
New: func(config ModuleConfig) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params params := config.Params
duration, ok := params["duration"] duration, ok := params["duration"]
if !ok { if !ok {
return nil, fmt.Errorf("interval requires a duration parameter") return nil, fmt.Errorf("gen.interval requires a duration parameter")
} }
durationNum, ok := duration.(float64) durationNum, ok := duration.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("interval duration must be number") return nil, fmt.Errorf("gen.interval duration must be number")
} }
return &Interval{Duration: uint32(durationNum), config: config}, nil return &Interval{Duration: uint32(durationNum), config: config, ctx: ctx, router: router}, nil
}, },
}) })
} }
@@ -43,22 +48,19 @@ func (i *Interval) Type() string {
return i.config.Type return i.config.Type
} }
func (i *Interval) RegisterRouter(router *Router) {
i.router = router
}
func (i *Interval) Run() error { func (i *Interval) Run() error {
ticker := time.NewTicker(time.Millisecond * time.Duration(i.Duration)) ticker := time.NewTicker(time.Millisecond * time.Duration(i.Duration))
i.ticker = ticker i.ticker = ticker
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-i.router.Context.Done(): case <-i.ctx.Done():
slog.Debug("router context done in module", "id", i.config.Id) slog.Debug("router context done in module", "id", i.Id())
return nil return nil
case t := <-ticker.C: case <-ticker.C:
if i.router != nil { if i.router != nil {
i.router.HandleInput(i.config.Id, t) i.router.HandleInput(i.Id(), time.Now())
} }
} }
} }

View File

@@ -0,0 +1,81 @@
//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 MIDIInput struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
Port string
SendFunc func(midi.Message) error
}
func init() {
RegisterModule(ModuleRegistration{
Type: "midi.input",
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("midi.input requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, fmt.Errorf("midi.input port must be a string")
}
return &MIDIInput{config: config, Port: portString, ctx: ctx, router: router}, nil
},
})
}
func (mc *MIDIInput) Id() string {
return mc.config.Id
}
func (mc *MIDIInput) Type() string {
return mc.config.Type
}
func (mc *MIDIInput) Run() error {
defer midi.CloseDriver()
in, err := midi.FindInPort(mc.Port)
if err != nil {
return fmt.Errorf("midi.input can't find input port: %s", mc.Port)
}
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()
<-mc.ctx.Done()
slog.Debug("router context done in module", "id", mc.Id())
return nil
}
func (mc *MIDIInput) Output(payload any) error {
return fmt.Errorf("midi.input output is not implemented")
}

View File

@@ -0,0 +1,89 @@
//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 MIDIOutput struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
Port string
SendFunc func(midi.Message) error
}
func init() {
RegisterModule(ModuleRegistration{
//TODO(jwetzell): find a better namespace than "misc"
Type: "midi.output",
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("midi.output requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, fmt.Errorf("midi.output port must be a string")
}
return &MIDIOutput{config: config, Port: portString, ctx: ctx, router: router}, nil
},
})
}
func (mc *MIDIOutput) Id() string {
return mc.config.Id
}
func (mc *MIDIOutput) Type() string {
return mc.config.Type
}
func (mc *MIDIOutput) Run() error {
defer midi.CloseDriver()
out, err := midi.FindOutPort(mc.Port)
if err != nil {
return fmt.Errorf("midi.output can't find output port: %s", mc.Port)
}
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.Id())
return nil
}
func (mc *MIDIOutput) Output(payload any) error {
if mc.SendFunc == nil {
return fmt.Errorf("midi.output output is not setup")
}
payloadMessage, ok := payload.(midi.Message)
if !ok {
return fmt.Errorf("midi.output can only ouptut midi.Message")
}
return mc.SendFunc(payloadMessage)
}

View File

@@ -1,33 +1,30 @@
package showbridge package module
import ( import (
"context"
"fmt" "fmt"
"sync" "sync"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type ModuleError struct { type ModuleError struct {
Index int Index int
Config ModuleConfig Config config.ModuleConfig
Error error Error error
} }
type Module interface { type Module interface {
Id() string Id() string
Type() string Type() string
RegisterRouter(*Router)
Run() error Run() error
Output(any) error Output(any) error
} }
type ModuleConfig struct {
Id string `json:"id"`
Type string `json:"type"`
Params map[string]any `json:"params"`
}
type ModuleRegistration struct { type ModuleRegistration struct {
Type string `json:"type"` Type string `json:"type"`
New func(ModuleConfig) (Module, error) New func(context.Context, config.ModuleConfig, route.RouteIO) (Module, error)
} }
func RegisterModule(mod ModuleRegistration) { func RegisterModule(mod ModuleRegistration) {
@@ -42,13 +39,13 @@ func RegisterModule(mod ModuleRegistration) {
moduleRegistryMu.Lock() moduleRegistryMu.Lock()
defer moduleRegistryMu.Unlock() defer moduleRegistryMu.Unlock()
if _, ok := moduleRegistry[string(mod.Type)]; ok { if _, ok := ModuleRegistry[string(mod.Type)]; ok {
panic(fmt.Sprintf("module already registered: %s", mod.Type)) panic(fmt.Sprintf("module already registered: %s", mod.Type))
} }
moduleRegistry[string(mod.Type)] = mod ModuleRegistry[string(mod.Type)] = mod
} }
var ( var (
moduleRegistryMu sync.RWMutex moduleRegistryMu sync.RWMutex
moduleRegistry = make(map[string]ModuleRegistration) ModuleRegistry = make(map[string]ModuleRegistration)
) )

View File

@@ -1,16 +1,19 @@
package showbridge package module
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jwetzell/showbridge-go/internal/processing" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type MQTTClient struct { type MQTTClient struct {
config ModuleConfig config config.ModuleConfig
router *Router ctx context.Context
router route.RouteIO
Broker string Broker string
ClientID string ClientID string
Topic string Topic string
@@ -19,46 +22,46 @@ type MQTTClient struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.mqtt.client", Type: "mqtt.client",
New: func(config ModuleConfig) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params params := config.Params
broker, ok := params["broker"] broker, ok := params["broker"]
if !ok { if !ok {
return nil, fmt.Errorf("net.mqtt.client requires a broker parameter") return nil, fmt.Errorf("mqtt.client requires a broker parameter")
} }
brokerString, ok := broker.(string) brokerString, ok := broker.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.mqtt.client host must be string") return nil, fmt.Errorf("mqtt.client broker must be string")
} }
topic, ok := params["topic"] topic, ok := params["topic"]
if !ok { if !ok {
return nil, fmt.Errorf("net.mqtt.client requires a topic parameter") return nil, fmt.Errorf("mqtt.client requires a topic parameter")
} }
topicString, ok := topic.(string) topicString, ok := topic.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.mqtt.client host must be string") return nil, fmt.Errorf("mqtt.client topic must be string")
} }
clientId, ok := params["clientId"] clientId, ok := params["clientId"]
if !ok { if !ok {
return nil, fmt.Errorf("net.mqtt.client requires a clientId parameter") return nil, fmt.Errorf("mqtt.client requires a clientId parameter")
} }
clientIdString, ok := clientId.(string) clientIdString, ok := clientId.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.mqtt.client host must be string") return nil, fmt.Errorf("mqtt.client clientId must be string")
} }
return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString}, nil return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, ctx: ctx, router: router}, nil
}, },
}) })
} }
@@ -71,10 +74,6 @@ func (mc *MQTTClient) Type() string {
return mc.config.Type return mc.config.Type
} }
func (mc *MQTTClient) RegisterRouter(router *Router) {
mc.router = router
}
func (mc *MQTTClient) Run() error { func (mc *MQTTClient) Run() error {
opts := mqtt.NewClientOptions() opts := mqtt.NewClientOptions()
opts.AddBroker(mc.Broker) opts.AddBroker(mc.Broker)
@@ -84,7 +83,7 @@ func (mc *MQTTClient) Run() error {
opts.OnConnect = func(c mqtt.Client) { opts.OnConnect = func(c mqtt.Client) {
token := mc.client.Subscribe(mc.Topic, 1, func(c mqtt.Client, m mqtt.Message) { token := mc.client.Subscribe(mc.Topic, 1, func(c mqtt.Client, m mqtt.Message) {
mc.router.HandleInput(mc.config.Id, m) mc.router.HandleInput(mc.Id(), m)
}) })
token.Wait() token.Wait()
} }
@@ -99,27 +98,29 @@ func (mc *MQTTClient) Run() error {
return err return err
} }
<-mc.router.Context.Done() <-mc.ctx.Done()
slog.Debug("router context done in module", "id", mc.config.Id) slog.Debug("router context done in module", "id", mc.Id())
return nil return nil
} }
func (mc *MQTTClient) Output(payload any) error { func (mc *MQTTClient) Output(payload any) error {
payloadMessage, ok := payload.(processing.MQTTMessage) payloadMessage, ok := payload.(mqtt.Message)
fmt.Printf("payload type: %T\n", payload)
if !ok { if !ok {
return fmt.Errorf("net.mqtt.client is only able to output MQTTMessage") return fmt.Errorf("mqtt.client is only able to output a MQTTMessage")
} }
if mc.client == nil { if mc.client == nil {
return fmt.Errorf("net.mqtt.client client is not setup") return fmt.Errorf("mqtt.client client is not setup")
} }
if !mc.client.IsConnected() { if !mc.client.IsConnected() {
return fmt.Errorf("net.mqtt.client is not connected") return fmt.Errorf("mqtt.client is not connected")
} }
token := mc.client.Publish(payloadMessage.Topic, payloadMessage.QoS, payloadMessage.Retained, payloadMessage.Payload) token := mc.client.Publish(payloadMessage.Topic(), payloadMessage.Qos(), payloadMessage.Retained(), payloadMessage.Payload())
token.Wait() token.Wait()

View File

@@ -1,16 +1,20 @@
package showbridge package module
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"github.com/jwetzell/showbridge-go/internal/processing" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
) )
type NATSClient struct { type NATSClient struct {
config ModuleConfig config config.ModuleConfig
router *Router ctx context.Context
router route.RouteIO
URL string URL string
Subject string Subject string
client *nats.Conn client *nats.Conn
@@ -18,34 +22,34 @@ type NATSClient struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.nats.client", Type: "nats.client",
New: func(config ModuleConfig) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params params := config.Params
url, ok := params["url"] url, ok := params["url"]
if !ok { if !ok {
return nil, fmt.Errorf("net.nats.client requires a url parameter") return nil, fmt.Errorf("nats.client requires a url parameter")
} }
urlString, ok := url.(string) urlString, ok := url.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.nats.client url must be string") return nil, fmt.Errorf("nats.client url must be string")
} }
subject, ok := params["subject"] subject, ok := params["subject"]
if !ok { if !ok {
return nil, fmt.Errorf("net.nats.client requires a subject parameter") return nil, fmt.Errorf("nats.client requires a subject parameter")
} }
subjectString, ok := subject.(string) subjectString, ok := subject.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.nats.client subject must be string") return nil, fmt.Errorf("nats.client subject must be string")
} }
return &NATSClient{config: config, URL: urlString, Subject: subjectString}, nil return &NATSClient{config: config, URL: urlString, Subject: subjectString, ctx: ctx, router: router}, nil
}, },
}) })
} }
@@ -58,10 +62,6 @@ func (nc *NATSClient) Type() string {
return nc.config.Type return nc.config.Type
} }
func (nc *NATSClient) RegisterRouter(router *Router) {
nc.router = router
}
func (nc *NATSClient) Run() error { func (nc *NATSClient) Run() error {
client, err := nats.Connect(nc.URL, nats.RetryOnFailedConnect(true)) client, err := nats.Connect(nc.URL, nats.RetryOnFailedConnect(true))
@@ -76,7 +76,7 @@ func (nc *NATSClient) Run() error {
sub, err := nc.client.Subscribe(nc.Subject, func(msg *nats.Msg) { sub, err := nc.client.Subscribe(nc.Subject, func(msg *nats.Msg) {
if nc.router != nil { if nc.router != nil {
nc.router.HandleInput(nc.config.Id, msg) nc.router.HandleInput(nc.Id(), msg)
} }
}) })
@@ -86,25 +86,25 @@ func (nc *NATSClient) Run() error {
defer sub.Unsubscribe() defer sub.Unsubscribe()
<-nc.router.Context.Done() <-nc.ctx.Done()
slog.Debug("router context done in module", "id", nc.config.Id) slog.Debug("router context done in module", "id", nc.Id())
return nil return nil
} }
func (nc *NATSClient) Output(payload any) error { func (nc *NATSClient) Output(payload any) error {
payloadMessage, ok := payload.(processing.NATSMessage) payloadMessage, ok := payload.(processor.NATSMessage)
if !ok { if !ok {
return fmt.Errorf("net.nats.client is only able to output NATSMessage") return fmt.Errorf("nats.client is only able to output NATSMessage")
} }
if nc.client == nil { if nc.client == nil {
return fmt.Errorf("net.nats.client client is not setup") return fmt.Errorf("nats.client client is not setup")
} }
if !nc.client.IsConnected() { if !nc.client.IsConnected() {
return fmt.Errorf("net.nats.client is not connected") return fmt.Errorf("nats.client is not connected")
} }
err := nc.client.Publish(payloadMessage.Subject, payloadMessage.Payload) err := nc.client.Publish(payloadMessage.Subject, payloadMessage.Payload)

View File

@@ -1,27 +1,31 @@
package showbridge package module
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
"time" "time"
"github.com/jwetzell/psn-go" "github.com/jwetzell/psn-go"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type PSNClient struct { type PSNClient struct {
config ModuleConfig config config.ModuleConfig
conn *net.UDPConn conn *net.UDPConn
router *Router ctx context.Context
router route.RouteIO
decoder *psn.Decoder decoder *psn.Decoder
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.psn.client", Type: "psn.client",
New: func(config ModuleConfig) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
return &PSNClient{config: config, decoder: psn.NewDecoder()}, nil return &PSNClient{config: config, decoder: psn.NewDecoder(), ctx: ctx, router: router}, nil
}, },
}) })
} }
@@ -34,10 +38,6 @@ func (pc *PSNClient) Type() string {
return pc.config.Type return pc.config.Type
} }
func (pc *PSNClient) RegisterRouter(router *Router) {
pc.router = router
}
func (pc *PSNClient) Run() error { func (pc *PSNClient) Run() error {
addr, err := net.ResolveUDPAddr("udp", "236.10.10.10:56565") addr, err := net.ResolveUDPAddr("udp", "236.10.10.10:56565")
@@ -56,9 +56,9 @@ func (pc *PSNClient) Run() error {
buffer := make([]byte, 2048) buffer := make([]byte, 2048)
for { for {
select { select {
case <-pc.router.Context.Done(): case <-pc.ctx.Done():
// TODO(jwetzell): cleanup? // TODO(jwetzell): cleanup?
slog.Debug("router context done in module", "id", pc.config.Id) slog.Debug("router context done in module", "id", pc.Id())
return nil return nil
default: default:
pc.conn.SetDeadline(time.Now().Add(time.Millisecond * 200)) pc.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -76,15 +76,15 @@ func (pc *PSNClient) Run() error {
message := buffer[:numBytes] message := buffer[:numBytes]
err := pc.decoder.Decode(message) err := pc.decoder.Decode(message)
if err != nil { if err != nil {
slog.Error("net.psn.client problem decoding psn traffic", "id", pc.config.Id, "error", err) slog.Error("psn.client problem decoding psn traffic", "id", pc.Id(), "error", err)
} }
if pc.router != nil { if pc.router != nil {
for _, tracker := range pc.decoder.Trackers { for _, tracker := range pc.decoder.Trackers {
pc.router.HandleInput(pc.config.Id, tracker) pc.router.HandleInput(pc.Id(), tracker)
} }
} else { } else {
slog.Error("net.psn.client has no router", "id", pc.config.Id) slog.Error("psn.client has no router", "id", pc.Id())
} }
} }
} }
@@ -92,5 +92,5 @@ func (pc *PSNClient) Run() error {
} }
func (pc *PSNClient) Output(payload any) error { func (pc *PSNClient) Output(payload any) error {
return fmt.Errorf("net.psn.client output is not implemented") return fmt.Errorf("psn.client output is not implemented")
} }

View File

@@ -0,0 +1,174 @@
//go:build cgo
package module
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/framer"
"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 framer.Framer
Mode *serial.Mode
port serial.Port
}
func init() {
RegisterModule(ModuleRegistration{
//TODO(jwetzell): find a better namespace than "misc"
Type: "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("serial.client requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, fmt.Errorf("serial.client port must be a string")
}
framingMethod := "RAW"
framingMethodRaw, ok := params["framing"]
if ok {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, fmt.Errorf("serial.client framing method must be a string")
}
framingMethod = framingMethodString
}
framer, err := framer.GetFramer(framingMethod)
if err != nil {
return nil, err
}
buadRate, ok := params["baudRate"]
if !ok {
return nil, fmt.Errorf("serial.client requires a baudRate parameter")
}
baudRateNum, ok := buadRate.(float64)
if !ok {
return nil, fmt.Errorf("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("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.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.Id())
return nil
}
slog.Error("serial.client", "id", mc.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.Id())
return nil
default:
READ:
for {
select {
case <-mc.ctx.Done():
slog.Debug("router context done in module", "id", mc.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.Id(), message)
} else {
slog.Error("serial.client has no router", "id", mc.Id())
}
}
}
}
}
}
}
}
}
func (mc *SerialClient) Output(payload any) error {
payloadBytes, ok := payload.([]byte)
if !ok {
return fmt.Errorf("serial.client can only ouptut bytes")
}
_, err := mc.port.Write(mc.Framer.Encode(payloadBytes))
return err
}

View File

@@ -0,0 +1,185 @@
package module
import (
"context"
"fmt"
"io"
"log/slog"
"time"
"github.com/emiago/diago"
"github.com/emiago/diago/media"
"github.com/emiago/sipgo"
"github.com/emiago/sipgo/sip"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
)
type SIPCallServer struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
IP string
Port int
Transport string
UserAgent string
dg *diago.Diago
}
type SIPCallMessage struct {
To string
}
func init() {
RegisterModule(ModuleRegistration{
Type: "sip.call.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params
portNum := 5060
port, ok := params["port"]
if ok {
specificPortNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("sip.call.server port must be a number")
}
portNum = int(specificPortNum)
}
ipString := "0.0.0.0"
ip, ok := params["ip"]
if ok {
specificIpString, ok := ip.(string)
if !ok {
return nil, fmt.Errorf("sip.call.server ip must be a string")
}
ipString = specificIpString
}
transportString := "udp"
transport, ok := params["transport"]
if ok {
specificTransportString, ok := transport.(string)
if !ok {
return nil, fmt.Errorf("sip.call.server transport must be a string")
}
transportString = specificTransportString
}
userAgentString := "showbridge"
userAgent, ok := params["userAgent"]
if ok {
specificTransportString, ok := userAgent.(string)
if !ok {
return nil, fmt.Errorf("sip.call.server userAgent must be a string")
}
userAgentString = specificTransportString
}
return &SIPCallServer{config: config, ctx: ctx, router: router, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString}, nil
},
})
}
func (sds *SIPCallServer) Id() string {
return sds.config.Id
}
func (sds *SIPCallServer) Type() string {
return sds.config.Type
}
func (sds *SIPCallServer) Run() error {
diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
ua, _ := sipgo.NewUA(
sipgo.WithUserAgent(sds.UserAgent),
sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)),
sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)),
)
defer ua.Close()
sip.SetDefaultLogger(diagoLogger)
media.SetDefaultLogger(diagoLogger)
dg := diago.NewDiago(ua, diago.WithLogger(diagoLogger), diago.WithTransport(
diago.Transport{
Transport: sds.Transport,
BindHost: sds.IP,
BindPort: sds.Port,
},
))
go func() {
dg.Serve(sds.ctx, func(inDialog *diago.DialogServerSession) {
sds.HandleCall(inDialog)
})
}()
sds.dg = dg
<-sds.ctx.Done()
slog.Debug("router context done in module", "id", sds.Id())
return nil
}
func (sds *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) {
inDialog.Trying()
inDialog.Ringing()
inDialog.Answer()
sds.router.HandleInput(sds.Id(), SIPCallMessage{
To: inDialog.ToUser(),
})
<-inDialog.Context().Done()
}
func (sds *SIPCallServer) Output(payload any) error {
payloadMsg, ok := payload.(string)
if !ok {
return fmt.Errorf("sip.call.server output payload must be of type string")
}
if sds.dg == nil {
return fmt.Errorf("sip.call.server diago is not initialized")
}
var uri sip.Uri
err := sip.ParseUri(payloadMsg, &uri)
if err != nil {
return fmt.Errorf("sip.call.server output payload is not a valid SIP URI: %v", err)
}
outDialog, err := sds.dg.NewDialog(uri, diago.NewDialogOptions{
Transport: sds.Transport,
})
if err != nil {
return fmt.Errorf("sip.call.server failed to create new dialog: %v", err)
}
err = outDialog.Invite(sds.ctx, diago.InviteClientOptions{})
if err != nil {
return fmt.Errorf("sip.call.server failed to send invite: %v", err)
}
err = outDialog.Ack(sds.ctx)
if err != nil {
return fmt.Errorf("sip.call.server failed to send ack: %v", err)
}
// TODO(jwetzell): make this configurable
// NOTE(jwetzell): wait 5 seconds before hanging up the call
time.Sleep(5 * time.Second)
err = outDialog.Hangup(sds.ctx)
if err != nil {
return fmt.Errorf("sip.call.server failed to hangup call: %v", err)
}
return nil
}

View File

@@ -0,0 +1,163 @@
package module
import (
"context"
"fmt"
"io"
"log/slog"
"strings"
"time"
"github.com/emiago/diago"
"github.com/emiago/diago/media"
"github.com/emiago/sipgo"
"github.com/emiago/sipgo/sip"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
)
type SIPDTMFServer struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
IP string
Port int
Transport string
Separator string
}
type SIPDTMFMessage struct {
To string
Digits string
}
func init() {
RegisterModule(ModuleRegistration{
Type: "sip.dtmf.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params
portNum := 5060
port, ok := params["port"]
if ok {
specificPortNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("sip.dtmf.server port must be a number")
}
portNum = int(specificPortNum)
}
ipString := "0.0.0.0"
ip, ok := params["ip"]
if ok {
specificIpString, ok := ip.(string)
if !ok {
return nil, fmt.Errorf("sip.dtmf.server ip must be a string")
}
ipString = specificIpString
}
transportString := "udp"
transport, ok := params["transport"]
if ok {
specificTransportString, ok := transport.(string)
if !ok {
return nil, fmt.Errorf("sip.dtmf.server transport must be a string")
}
transportString = specificTransportString
}
separator, ok := params["separator"]
if !ok {
return nil, fmt.Errorf("sip.dtmf.server requires a separator parameter")
}
separatorString, ok := separator.(string)
if !ok {
return nil, fmt.Errorf("sip.dtmf.server separator must be a string")
}
if len(separatorString) != 1 {
return nil, fmt.Errorf("sip.dtmf.server separator must be a single character")
}
if !strings.ContainsRune("0123456789*#ABCD", rune(separatorString[0])) {
return nil, fmt.Errorf("sip.dtmf.server separator must be a valid DTMF character")
}
return &SIPDTMFServer{config: config, ctx: ctx, router: router, IP: ipString, Port: int(portNum), Transport: transportString, Separator: separatorString}, nil
},
})
}
func (sds *SIPDTMFServer) Id() string {
return sds.config.Id
}
func (sds *SIPDTMFServer) Type() string {
return sds.config.Type
}
func (sds *SIPDTMFServer) Run() error {
diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
ua, _ := sipgo.NewUA(
sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)),
sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)),
)
defer ua.Close()
sip.SetDefaultLogger(diagoLogger)
media.SetDefaultLogger(diagoLogger)
dg := diago.NewDiago(ua, diago.WithLogger(diagoLogger), diago.WithTransport(
diago.Transport{
Transport: sds.Transport,
BindHost: sds.IP,
BindPort: sds.Port,
},
))
err := dg.Serve(sds.ctx, func(inDialog *diago.DialogServerSession) {
sds.HandleCall(inDialog)
})
if err != nil {
return err
}
<-sds.ctx.Done()
slog.Debug("router context done in module", "id", sds.Id())
return nil
}
func (sds *SIPDTMFServer) HandleCall(inDialog *diago.DialogServerSession) error {
inDialog.Trying()
inDialog.Ringing()
inDialog.Answer()
reader := inDialog.AudioReaderDTMF()
userString := ""
return reader.Listen(func(dtmf rune) error {
if dtmf == rune(sds.Separator[0]) {
if sds.router != nil {
sds.router.HandleInput(sds.Id(), SIPDTMFMessage{
To: inDialog.ToUser(),
Digits: userString,
})
}
userString = ""
} else {
userString += string(dtmf)
}
return nil
}, 5*time.Second)
}
func (sds *SIPDTMFServer) Output(payload any) error {
return fmt.Errorf("sip.dtmf.server output is not implemented")
}

View File

@@ -1,26 +1,30 @@
package showbridge package module
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
"time" "time"
"github.com/jwetzell/showbridge-go/internal/framing" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/framer"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type TCPClient struct { type TCPClient struct {
config ModuleConfig config config.ModuleConfig
framer framing.Framer framer framer.Framer
conn net.Conn conn *net.TCPConn
router *Router ctx context.Context
router route.RouteIO
Addr *net.TCPAddr Addr *net.TCPAddr
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.tcp.client", Type: "net.tcp.client",
New: func(config ModuleConfig) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params params := config.Params
host, ok := params["host"] host, ok := params["host"]
@@ -31,7 +35,7 @@ func init() {
hostString, ok := host.(string) hostString, ok := host.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.tcp.client host must be uint16") return nil, fmt.Errorf("net.tcp.client host must be string")
} }
port, ok := params["port"] port, ok := params["port"]
@@ -42,7 +46,7 @@ func init() {
portNum, ok := port.(float64) portNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("net.tcp.client port must be uint16") return nil, fmt.Errorf("net.tcp.client port must be a number")
} }
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", hostString, uint16(portNum))) addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
@@ -50,33 +54,26 @@ func init() {
return nil, err return nil, err
} }
framingMethod, ok := params["framing"] framingMethod := "RAW"
if !ok {
return nil, fmt.Errorf("net.tcp.client requires a framing method")
}
framingMethodString, ok := framingMethod.(string) framingMethodRaw, ok := params["framing"]
if ok {
framingMethodString, ok := framingMethodRaw.(string)
if !ok { if !ok {
return nil, fmt.Errorf("tcp framing method must be a string") return nil, fmt.Errorf("misc.serial.client framing method must be a string")
}
framingMethod = framingMethodString
} }
var framer framing.Framer framer, err := framer.GetFramer(framingMethod)
switch framingMethodString { if err != nil {
case "CR": return nil, err
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)
} }
return &TCPClient{framer: framer, Addr: addr, config: config}, nil return &TCPClient{framer: framer, Addr: addr, config: config, ctx: ctx, router: router}, nil
}, },
}) })
} }
@@ -89,16 +86,12 @@ func (tc *TCPClient) Type() string {
return tc.config.Type return tc.config.Type
} }
func (tc *TCPClient) RegisterRouter(router *Router) {
tc.router = router
}
func (tc *TCPClient) Run() error { func (tc *TCPClient) Run() error {
// TODO(jwetzell): shutdown with router.Context properly // TODO(jwetzell): shutdown with router.Context properly
go func() { go func() {
<-tc.router.Context.Done() <-tc.ctx.Done()
slog.Debug("router context done in module", "id", tc.config.Id) slog.Debug("router context done in module", "id", tc.Id())
if tc.conn != nil { if tc.conn != nil {
tc.conn.Close() tc.conn.Close()
} }
@@ -107,26 +100,26 @@ func (tc *TCPClient) Run() error {
for { for {
err := tc.SetupConn() err := tc.SetupConn()
if err != nil { if err != nil {
if tc.router.Context.Err() != nil { if tc.ctx.Err() != nil {
slog.Debug("router context done in module", "id", tc.config.Id) slog.Debug("router context done in module", "id", tc.Id())
return nil return nil
} }
slog.Error("net.tcp.client", "id", tc.config.Id, "error", err.Error()) slog.Error("net.tcp.client", "id", tc.Id(), "error", err.Error())
time.Sleep(time.Second * 2) time.Sleep(time.Second * 2)
continue continue
} }
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
select { select {
case <-tc.router.Context.Done(): case <-tc.ctx.Done():
slog.Debug("router context done in module", "id", tc.config.Id) slog.Debug("router context done in module", "id", tc.Id())
return nil return nil
default: default:
READ: READ:
for { for {
select { select {
case <-tc.router.Context.Done(): case <-tc.ctx.Done():
slog.Debug("router context done in module", "id", tc.config.Id) slog.Debug("router context done in module", "id", tc.Id())
return nil return nil
default: default:
byteCount, err := tc.conn.Read(buffer) byteCount, err := tc.conn.Read(buffer)
@@ -141,9 +134,9 @@ func (tc *TCPClient) Run() error {
messages := tc.framer.Decode(buffer[0:byteCount]) messages := tc.framer.Decode(buffer[0:byteCount])
for _, message := range messages { for _, message := range messages {
if tc.router != nil { if tc.router != nil {
tc.router.HandleInput(tc.config.Id, message) tc.router.HandleInput(tc.Id(), message)
} else { } else {
slog.Error("net.tcp.client has no router", "id", tc.config.Id) slog.Error("net.tcp.client has no router", "id", tc.Id())
} }
} }
} }

View File

@@ -0,0 +1,220 @@
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/framer"
"github.com/jwetzell/showbridge-go/internal/route"
)
type TCPServer struct {
config config.ModuleConfig
Addr *net.TCPAddr
Framer framer.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 := "RAW"
framingMethodRaw, ok := params["framing"]
if ok {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, fmt.Errorf("misc.serial.client framing method must be a string")
}
framingMethod = framingMethodString
}
framer, err := framer.GetFramer(framingMethod)
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.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.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.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.Id(), message)
} else {
slog.Error("net.tcp.server has no router", "id", ts.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.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)
}

View File

@@ -1,36 +1,41 @@
package showbridge package module
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"time" "time"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type Timer struct { type Timer struct {
config ModuleConfig config config.ModuleConfig
Duration uint32 Duration uint32
router *Router ctx context.Context
router route.RouteIO
timer *time.Timer timer *time.Timer
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "gen.timer", Type: "gen.timer",
New: func(config ModuleConfig) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params params := config.Params
duration, ok := params["duration"] duration, ok := params["duration"]
if !ok { if !ok {
return nil, fmt.Errorf("timer requires a duration parameter") return nil, fmt.Errorf("gen.timer requires a duration parameter")
} }
durationNum, ok := duration.(float64) durationNum, ok := duration.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("timer duration must be number") return nil, fmt.Errorf("gen.timer duration must be a number")
} }
return &Timer{Duration: uint32(durationNum), config: config}, nil return &Timer{Duration: uint32(durationNum), config: config, ctx: ctx, router: router}, nil
}, },
}) })
} }
@@ -43,22 +48,18 @@ func (t *Timer) Type() string {
return t.config.Type return t.config.Type
} }
func (t *Timer) RegisterRouter(router *Router) {
t.router = router
}
func (t *Timer) Run() error { func (t *Timer) Run() error {
t.timer = time.NewTimer(time.Millisecond * time.Duration(t.Duration)) t.timer = time.NewTimer(time.Millisecond * time.Duration(t.Duration))
defer t.timer.Stop() defer t.timer.Stop()
for { for {
select { select {
case <-t.router.Context.Done(): case <-t.ctx.Done():
t.timer.Stop() t.timer.Stop()
slog.Debug("router context done in module", "id", t.config.Id) slog.Debug("router context done in module", "id", t.Id())
return nil return nil
case time := <-t.timer.C: case time := <-t.timer.C:
if t.router != nil { if t.router != nil {
t.router.HandleInput(t.config.Id, time) t.router.HandleInput(t.Id(), time)
} }
} }
} }

View File

@@ -0,0 +1,105 @@
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) SetupConn() error {
client, err := net.DialUDP("udp", nil, uc.Addr)
uc.conn = client
return err
}
func (uc *UDPClient) Run() error {
err := uc.SetupConn()
if err != nil {
return err
}
<-uc.ctx.Done()
slog.Debug("router context done in module", "id", uc.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.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.Id(), message)
} else {
slog.Error("net.udp.multicast has no router", "id", um.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

@@ -1,24 +1,28 @@
package showbridge package module
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"log/slog" "log/slog"
"net" "net"
"time" "time"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type UDPServer struct { type UDPServer struct {
Ip string Addr *net.UDPAddr
Port uint16 config config.ModuleConfig
config ModuleConfig ctx context.Context
router *Router router route.RouteIO
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.udp.server", Type: "net.udp.server",
New: func(config ModuleConfig) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] port, ok := params["port"]
if !ok { if !ok {
@@ -28,7 +32,7 @@ func init() {
portNum, ok := port.(float64) portNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.server port must be uint16") return nil, fmt.Errorf("net.udp.server port must be a number")
} }
ipString := "0.0.0.0" ipString := "0.0.0.0"
@@ -39,12 +43,17 @@ func init() {
specificIpString, ok := ip.(string) specificIpString, ok := ip.(string)
if !ok { if !ok {
return nil, fmt.Errorf("tcp ip method must be a string") return nil, fmt.Errorf("net.udp.server ip must be a string")
} }
ipString = specificIpString ipString = specificIpString
} }
return &UDPServer{Ip: ipString, Port: uint16(portNum), config: config}, nil 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
}, },
}) })
} }
@@ -57,30 +66,22 @@ func (us *UDPServer) Type() string {
return us.config.Id return us.config.Id
} }
func (us *UDPServer) RegisterRouter(router *Router) {
us.router = router
}
func (us *UDPServer) Run() error { func (us *UDPServer) Run() error {
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", us.Ip, us.Port)) listener, err := net.ListenUDP("udp", us.Addr)
if err != nil {
log.Fatalf("error resolving UDP address: %v", err)
}
listener, err := net.ListenUDP("udp", addr)
if err != nil { if err != nil {
return err return err
} }
defer listener.Close() defer listener.Close()
buffer := make([]byte, 1024) // TODO(jwetzell): make buffer size configurable
buffer := make([]byte, 65535)
for { for {
select { select {
case <-us.router.Context.Done(): case <-us.ctx.Done():
// TODO(jwetzell): cleanup? // TODO(jwetzell): cleanup?
slog.Debug("router context done in module", "id", us.config.Id) slog.Debug("router context done in module", "id", us.Id())
return nil return nil
default: default:
listener.SetDeadline(time.Now().Add(time.Millisecond * 200)) listener.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -95,9 +96,9 @@ func (us *UDPServer) Run() error {
} }
message := buffer[:numBytes] message := buffer[:numBytes]
if us.router != nil { if us.router != nil {
us.router.HandleInput(us.config.Id, message) us.router.HandleInput(us.Id(), message)
} else { } else {
slog.Error("net.udp.server has no router", "id", us.config.Id) slog.Error("net.udp.server has no router", "id", us.Id())
} }
} }
} }

View File

@@ -1,41 +0,0 @@
package processing_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/processing"
)
func TestGoodStringDecode(t *testing.T) {
stringDecoder := processing.StringDecode{}
tests := []struct {
processor processing.Processor
name string
payload any
expected string
}{
{
processor: &stringDecoder,
name: "hello",
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
expected: "hello",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
gotString, ok := got.(string)
if !ok {
t.Errorf("string.decode returned a %T payload: %s", got, got)
}
if err != nil {
t.Errorf("string.decode failed: %s", err)
}
if gotString != test.expected {
t.Errorf("string.decode got %s, expected %s", got, test.expected)
}
})
}
}

View File

@@ -1,42 +0,0 @@
package processing_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/processing"
)
func TestGoodStringEncode(t *testing.T) {
stringEncoder := processing.StringEncode{}
tests := []struct {
processor processing.Processor
name string
payload any
expected []byte
}{
{
processor: &stringEncoder,
name: "hello",
payload: "hello",
expected: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
gotBytes, ok := got.([]byte)
if !ok {
t.Errorf("string.encode returned a %T payload: %s", got, got)
}
if err != nil {
t.Errorf("string.encode failed: %s", err)
}
if !slices.Equal(gotBytes, test.expected) {
t.Errorf("string.encode got %s, expected %s", got, test.expected)
}
})
}
}

View File

@@ -1,13 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type DebugLog struct { type DebugLog struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (dl *DebugLog) Process(ctx context.Context, payload any) (any, error) { func (dl *DebugLog) Process(ctx context.Context, payload any) (any, error) {
@@ -22,7 +24,7 @@ func (dl *DebugLog) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "debug.log", Type: "debug.log",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &DebugLog{config: config}, nil return &DebugLog{config: config}, nil
}, },
}) })

View File

@@ -1,13 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"strconv" "strconv"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type FloatParse struct { type FloatParse struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (fp *FloatParse) Process(ctx context.Context, payload any) (any, error) { func (fp *FloatParse) Process(ctx context.Context, payload any) (any, error) {
@@ -32,7 +34,7 @@ func (fp *FloatParse) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "float.parse", Type: "float.parse",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &FloatParse{config: config}, nil return &FloatParse{config: config}, nil
}, },
}) })

View File

@@ -0,0 +1,85 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestGoodFloatParse(t *testing.T) {
floatParser := processor.FloatParse{}
tests := []struct {
processor processor.Processor
name string
payload any
expected float64
}{
{
name: "positive number",
payload: "12345.67",
expected: 12345.67,
},
{
name: "negative number",
payload: "-12345.67",
expected: -12345.67,
},
{
name: "zero",
payload: "0",
expected: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := floatParser.Process(t.Context(), test.payload)
gotFloat, ok := got.(float64)
if !ok {
t.Errorf("float.parse returned a %T payload: %s", got, got)
}
if err != nil {
t.Errorf("float.parse failed: %s", err)
}
if gotFloat != test.expected {
t.Errorf("float.parse got %f, expected %f", gotFloat, test.expected)
}
})
}
}
func TestBadFloatParse(t *testing.T) {
floatParser := processor.FloatParse{}
tests := []struct {
processor processor.Processor
name string
payload any
errorString string
}{
{
name: "non-string input",
payload: []byte{0x01},
errorString: "float.parse processor only accepts a string",
},
{
name: "not float string",
payload: "abcd",
errorString: "strconv.ParseFloat: parsing \"abcd\": invalid syntax",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := floatParser.Process(t.Context(), test.payload)
if err == nil {
t.Errorf("float.parse expected to fail but succeeded, got: %v", got)
}
if err.Error() != test.errorString {
t.Errorf("float.parse got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -1,4 +1,4 @@
package processing package processor
import ( import (
"bytes" "bytes"
@@ -8,10 +8,11 @@ import (
"text/template" "text/template"
freeD "github.com/jwetzell/free-d-go" freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type FreeDCreate struct { type FreeDCreate struct {
config ProcessorConfig config config.ProcessorConfig
Id *template.Template Id *template.Template
Pan *template.Template Pan *template.Template
Tilt *template.Template Tilt *template.Template
@@ -51,6 +52,10 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
panNum, err := strconv.ParseFloat(panString, 32) panNum, err := strconv.ParseFloat(panString, 32)
if err != nil {
return nil, err
}
var tiltBuffer bytes.Buffer var tiltBuffer bytes.Buffer
err = fc.Tilt.Execute(&tiltBuffer, payload) err = fc.Tilt.Execute(&tiltBuffer, payload)
@@ -62,6 +67,10 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
tiltNum, err := strconv.ParseFloat(tiltString, 32) tiltNum, err := strconv.ParseFloat(tiltString, 32)
if err != nil {
return nil, err
}
var rollBuffer bytes.Buffer var rollBuffer bytes.Buffer
err = fc.Tilt.Execute(&rollBuffer, payload) err = fc.Tilt.Execute(&rollBuffer, payload)
@@ -77,10 +86,6 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
return nil, err return nil, err
} }
if err != nil {
return nil, err
}
var posXBuffer bytes.Buffer var posXBuffer bytes.Buffer
err = fc.PosX.Execute(&posXBuffer, payload) err = fc.PosX.Execute(&posXBuffer, payload)
@@ -178,7 +183,7 @@ func (fc *FreeDCreate) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "freed.create", Type: "freed.create",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
// TODO(jwetzell): make some params optional // TODO(jwetzell): make some params optional
params := config.Params params := config.Params

View File

@@ -1,4 +1,4 @@
package processing package processor
import ( import (
"context" "context"
@@ -6,10 +6,11 @@ import (
"log/slog" "log/slog"
freeD "github.com/jwetzell/free-d-go" freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type FreeDDecode struct { type FreeDDecode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (fdd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) { func (fdd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) {
@@ -33,7 +34,7 @@ func (fdd *FreeDDecode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "freed.decode", Type: "freed.decode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &FreeDDecode{config: config}, nil return &FreeDDecode{config: config}, nil
}, },
}) })

View File

@@ -1,14 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
freeD "github.com/jwetzell/free-d-go" freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type FreeDEncode struct { type FreeDEncode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (fde *FreeDEncode) Process(ctx context.Context, payload any) (any, error) { func (fde *FreeDEncode) Process(ctx context.Context, payload any) (any, error) {
@@ -29,7 +30,7 @@ func (fde *FreeDEncode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "freed.encode", Type: "freed.encode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &FreeDEncode{config: config}, nil return &FreeDEncode{config: config}, nil
}, },
}) })

View File

@@ -1,4 +1,4 @@
package processing package processor
import ( import (
"bytes" "bytes"
@@ -6,10 +6,12 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"text/template" "text/template"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type HTTPRequestCreate struct { type HTTPRequestCreate struct {
config ProcessorConfig config config.ProcessorConfig
Method string Method string
URL *template.Template URL *template.Template
} }
@@ -42,7 +44,7 @@ func (hre *HTTPRequestCreate) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "http.request.create", Type: "http.request.create",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
method, ok := params["method"] method, ok := params["method"]

View File

@@ -1,14 +1,16 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type HTTPRequestEncode struct { type HTTPRequestEncode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (hre *HTTPRequestEncode) Process(ctx context.Context, payload any) (any, error) { func (hre *HTTPRequestEncode) Process(ctx context.Context, payload any) (any, error) {
@@ -33,7 +35,7 @@ func (hre *HTTPRequestEncode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "http.request.encode", Type: "http.request.encode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &HTTPRequestEncode{config: config}, nil return &HTTPRequestEncode{config: config}, nil
}, },
}) })

View File

@@ -1,14 +1,16 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type HTTPRequestFilter struct { type HTTPRequestFilter struct {
config ProcessorConfig config config.ProcessorConfig
Path *regexp.Regexp Path *regexp.Regexp
Method string Method string
} }
@@ -41,7 +43,7 @@ func (hrf *HTTPRequestFilter) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "http.request.filter", Type: "http.request.filter",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
path, ok := params["path"] path, ok := params["path"]

View File

@@ -1,14 +1,16 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type HTTPResponseEncode struct { type HTTPResponseEncode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (hre *HTTPResponseEncode) Process(ctx context.Context, payload any) (any, error) { func (hre *HTTPResponseEncode) Process(ctx context.Context, payload any) (any, error) {
@@ -34,7 +36,7 @@ func (hre *HTTPResponseEncode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "http.response.encode", Type: "http.response.encode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &HTTPResponseEncode{config: config}, nil return &HTTPResponseEncode{config: config}, nil
}, },
}) })

View File

@@ -1,13 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"strconv" "strconv"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type IntParse struct { type IntParse struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (ip *IntParse) Process(ctx context.Context, payload any) (any, error) { func (ip *IntParse) Process(ctx context.Context, payload any) (any, error) {
@@ -32,7 +34,7 @@ func (ip *IntParse) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "int.parse", Type: "int.parse",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &IntParse{config: config}, nil return &IntParse{config: config}, nil
}, },
}) })

View File

@@ -0,0 +1,85 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestGoodIntParse(t *testing.T) {
intParser := processor.IntParse{}
tests := []struct {
processor processor.Processor
name string
payload any
expected int64
}{
{
name: "positive number",
payload: "12345",
expected: 12345,
},
{
name: "negative number",
payload: "-12345",
expected: -12345,
},
{
name: "zero",
payload: "0",
expected: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := intParser.Process(t.Context(), test.payload)
gotInt, ok := got.(int64)
if !ok {
t.Errorf("int.parse returned a %T payload: %s", got, got)
}
if err != nil {
t.Errorf("int.parse failed: %s", err)
}
if gotInt != test.expected {
t.Errorf("int.parse got %d, expected %d", gotInt, test.expected)
}
})
}
}
func TestBadIntParse(t *testing.T) {
intParser := processor.IntParse{}
tests := []struct {
processor processor.Processor
name string
payload any
errorString string
}{
{
name: "non-string input",
payload: []byte{0x01},
errorString: "int.parse processor only accepts a string",
},
{
name: "not int string",
payload: "123.46",
errorString: "strconv.ParseInt: parsing \"123.46\": invalid syntax",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := intParser.Process(t.Context(), test.payload)
if err == nil {
t.Errorf("int.parse expected to fail but succeeded, got: %v", got)
}
if err.Error() != test.errorString {
t.Errorf("int.parse got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,374 @@
//go:build cgo
package processor
import (
"bytes"
"context"
"fmt"
"strconv"
"text/template"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
type MIDIMessageCreate struct {
config config.ProcessorConfig
ProcessFunc func(ctx context.Context, payload any) (any, error)
}
func (mmd *MIDIMessageCreate) Process(ctx context.Context, payload any) (any, error) {
return mmd.ProcessFunc(ctx, payload)
}
func (mmd *MIDIMessageCreate) Type() string {
return mmd.config.Type
}
func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
params := config.Params
channel, ok := params["channel"]
if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn channel must be a string")
}
channelTemplate, err := template.New("channel").Parse(channelString)
if err != nil {
return nil, err
}
note, ok := params["note"]
if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn requires a note parameter")
}
noteString, ok := note.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn note must be a string")
}
noteTemplate, err := template.New("note").Parse(noteString)
if err != nil {
return nil, err
}
velocity, ok := params["velocity"]
if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn requires a velocity parameter")
}
velocityString, ok := velocity.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn velocity must be a string")
}
velocityTemplate, err := template.New("velocity").Parse(velocityString)
if err != nil {
return nil, err
}
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) {
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, payload)
if err != nil {
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var noteBuffer bytes.Buffer
err = noteTemplate.Execute(&noteBuffer, payload)
if err != nil {
return nil, err
}
noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8)
var velocityBuffer bytes.Buffer
err = velocityTemplate.Execute(&velocityBuffer, payload)
if err != nil {
return nil, err
}
velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8)
payloadMessage := midi.NoteOn(uint8(channelValue), uint8(noteValue), uint8(velocityValue))
return payloadMessage, nil
}}, nil
}
func newMidiNoteOffCreate(config config.ProcessorConfig) (Processor, error) {
params := config.Params
channel, ok := params["channel"]
if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn channel must be a string")
}
channelTemplate, err := template.New("channel").Parse(channelString)
if err != nil {
return nil, err
}
note, ok := params["note"]
if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn requires a note parameter")
}
noteString, ok := note.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn note must be a string")
}
noteTemplate, err := template.New("note").Parse(noteString)
if err != nil {
return nil, err
}
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) {
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, payload)
if err != nil {
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var noteBuffer bytes.Buffer
err = noteTemplate.Execute(&noteBuffer, payload)
if err != nil {
return nil, err
}
noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8)
payloadMessage := midi.NoteOff(uint8(channelValue), uint8(noteValue))
return payloadMessage, nil
}}, nil
}
func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error) {
params := config.Params
channel, ok := params["channel"]
if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange channel must be a string")
}
channelTemplate, err := template.New("channel").Parse(channelString)
if err != nil {
return nil, err
}
controller, ok := params["controller"]
if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange requires a controller parameter")
}
controllerString, ok := controller.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange controller must be a string")
}
controllerTemplate, err := template.New("controller").Parse(controllerString)
if err != nil {
return nil, err
}
value, ok := params["value"]
if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange requires a value parameter")
}
valueString, ok := value.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange value must be a string")
}
valueTemplate, err := template.New("value").Parse(valueString)
if err != nil {
return nil, err
}
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) {
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, payload)
if err != nil {
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var controllerBuffer bytes.Buffer
err = controllerTemplate.Execute(&controllerBuffer, payload)
if err != nil {
return nil, err
}
controllerValue, err := strconv.ParseUint(controllerBuffer.String(), 10, 8)
var valueBuffer bytes.Buffer
err = valueTemplate.Execute(&valueBuffer, payload)
if err != nil {
return nil, err
}
valueValue, err := strconv.ParseUint(valueBuffer.String(), 10, 8)
payloadMessage := midi.ControlChange(uint8(channelValue), uint8(controllerValue), uint8(valueValue))
return payloadMessage, nil
}}, nil
}
func newMidiProgramChangeCreate(config config.ProcessorConfig) (Processor, error) {
params := config.Params
channel, ok := params["channel"]
if !ok {
return nil, fmt.Errorf("midi.message.create ProgramChange requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create ProgramChange channel must be a string")
}
channelTemplate, err := template.New("channel").Parse(channelString)
if err != nil {
return nil, err
}
program, ok := params["program"]
if !ok {
return nil, fmt.Errorf("midi.message.create ProgramChange requires a program parameter")
}
programString, ok := program.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create ProgramChange program must be a string")
}
programTemplate, err := template.New("program").Parse(programString)
if err != nil {
return nil, err
}
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) {
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, payload)
if err != nil {
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var programBuffer bytes.Buffer
err = programTemplate.Execute(&programBuffer, payload)
if err != nil {
return nil, err
}
programValue, err := strconv.ParseUint(programBuffer.String(), 10, 8)
payloadMessage := midi.ProgramChange(uint8(channelValue), uint8(programValue))
return payloadMessage, nil
}}, nil
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "midi.message.create",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
msgType, ok := params["type"]
if !ok {
return nil, fmt.Errorf("midi.message.create requires a type parameter")
}
msgTypeString, ok := msgType.(string)
if !ok {
return nil, fmt.Errorf("midi.message.create type parameter must be a string")
}
switch msgTypeString {
case "NoteOn", "noteon", "note_on":
return newMidiNoteOnCreate(config)
case "NoteOff", "noteoff", "note_off":
return newMidiNoteOffCreate(config)
case "ControlChange", "controlchange", "control_change":
return newMidiControlChangeCreate(config)
case "ProgramChange", "programchange", "program_change":
return newMidiProgramChangeCreate(config)
default:
return nil, fmt.Errorf("midi.message.create does not support type %s", msgTypeString)
}
},
})
}

View File

@@ -1,16 +1,17 @@
//go:build cgo //go:build cgo
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
) )
type MIDIMessageDecode struct { type MIDIMessageDecode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (mmd *MIDIMessageDecode) Process(ctx context.Context, payload any) (any, error) { func (mmd *MIDIMessageDecode) Process(ctx context.Context, payload any) (any, error) {
@@ -32,7 +33,7 @@ func (mmd *MIDIMessageDecode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "midi.message.decode", Type: "midi.message.decode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &MIDIMessageDecode{config: config}, nil return &MIDIMessageDecode{config: config}, nil
}, },
}) })

View File

@@ -1,16 +1,17 @@
//go:build cgo //go:build cgo
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
) )
type MIDIMessageEncode struct { type MIDIMessageEncode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (mme *MIDIMessageEncode) Process(ctx context.Context, payload any) (any, error) { func (mme *MIDIMessageEncode) Process(ctx context.Context, payload any) (any, error) {
@@ -30,7 +31,7 @@ func (mme *MIDIMessageEncode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "midi.message.encode", Type: "midi.message.encode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &MIDIMessageEncode{config: config}, nil return &MIDIMessageEncode{config: config}, nil
}, },
}) })

View File

@@ -0,0 +1,55 @@
//go:build cgo
package processor
import (
"context"
"fmt"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
type MIDIMessageFilter struct {
config config.ProcessorConfig
MIDIType string
}
func (mmf *MIDIMessageFilter) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := payload.(midi.Message)
if !ok {
return nil, fmt.Errorf("midi.message.filter processor only accepts an midi.Message")
}
if payloadMessage.Type().String() != mmf.MIDIType {
return nil, nil
}
return payloadMessage, nil
}
func (mmf *MIDIMessageFilter) Type() string {
return mmf.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "midi.message.filter",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
midiType, ok := params["type"]
if !ok {
return nil, fmt.Errorf("midi.message.filter requires a type parameter")
}
midiTypeString, ok := midiType.(string)
if !ok {
return nil, fmt.Errorf("midi.message.filter type must be a string")
}
return &MIDIMessageFilter{config: config, MIDIType: midiTypeString}, nil
},
})
}

View File

@@ -0,0 +1,90 @@
//go:build cgo
package processor
import (
"context"
"fmt"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
type MIDIMessageUnpack struct {
config config.ProcessorConfig
}
type MIDINoteOn struct {
Channel uint8
Note uint8
Velocity uint8
}
type MIDINoteOff struct {
Channel uint8
Note uint8
Velocity uint8
}
type MIDIControlChange struct {
Channel uint8
Control uint8
Value uint8
}
type MIDIProgramChange struct {
Channel uint8
Program uint8
}
type MIDIPitchBend struct {
Channel uint8
Relative int16
Absolute uint16
}
func (mmu *MIDIMessageUnpack) Process(ctx context.Context, payload any) (any, error) {
payloadMidi, ok := payload.(midi.Message)
if !ok {
return nil, fmt.Errorf("midi.message.unpack processor only accepts a midi.Message")
}
switch payloadMidi.Type() {
case midi.NoteOnMsg:
noteOnMsg := MIDINoteOn{}
payloadMidi.GetNoteOn(&noteOnMsg.Channel, &noteOnMsg.Note, &noteOnMsg.Velocity)
return noteOnMsg, nil
case midi.NoteOffMsg:
noteOffMsg := MIDINoteOff{}
payloadMidi.GetNoteOff(&noteOffMsg.Channel, &noteOffMsg.Note, &noteOffMsg.Velocity)
return noteOffMsg, nil
case midi.ControlChangeMsg:
controlChangeMsg := MIDIControlChange{}
payloadMidi.GetControlChange(&controlChangeMsg.Channel, &controlChangeMsg.Control, &controlChangeMsg.Value)
return controlChangeMsg, nil
case midi.ProgramChangeMsg:
programChangeMsg := MIDIProgramChange{}
payloadMidi.GetProgramChange(&programChangeMsg.Channel, &programChangeMsg.Program)
return programChangeMsg, nil
case midi.PitchBendMsg:
pitchBendMsg := MIDIPitchBend{}
payloadMidi.GetPitchBend(&pitchBendMsg.Channel, &pitchBendMsg.Relative, &pitchBendMsg.Absolute)
return pitchBendMsg, nil
default:
return nil, fmt.Errorf("midi.message.unpack message type not supported %v", payloadMidi.Type())
}
}
func (mmu *MIDIMessageUnpack) Type() string {
return mmu.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "midi.message.unpack",
New: func(config config.ProcessorConfig) (Processor, error) {
return &MIDIMessageUnpack{config: config}, nil
},
})
}

View File

@@ -1,32 +1,62 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type MQTTMessage struct { type MQTTMessage struct {
Topic string topic string
QoS byte qos byte
Payload any payload []byte
Retained bool retained bool
} }
type MQTTMessageCreate struct { type MQTTMessageCreate struct {
config ProcessorConfig config config.ProcessorConfig
Topic string Topic string
QoS byte QoS byte
Retained bool Retained bool
Payload any Payload []byte
} }
func (mm MQTTMessage) Duplicate() bool {
// TODO(jwetzell): implement?
return false
}
func (mm MQTTMessage) Qos() byte {
return mm.qos
}
func (mm MQTTMessage) Retained() bool {
return mm.retained
}
func (mm MQTTMessage) Topic() string {
return mm.topic
}
func (mm MQTTMessage) MessageID() uint16 {
// TODO(jwetzell): implement?
return 0
}
func (mm MQTTMessage) Payload() []byte {
return mm.payload
}
func (mm MQTTMessage) Ack() {}
func (mmc *MQTTMessageCreate) Process(ctx context.Context, payload any) (any, error) { func (mmc *MQTTMessageCreate) Process(ctx context.Context, payload any) (any, error) {
message := MQTTMessage{ message := MQTTMessage{
Topic: mmc.Topic, topic: mmc.Topic,
QoS: mmc.QoS, qos: mmc.QoS,
Retained: mmc.Retained, retained: mmc.Retained,
Payload: mmc.Payload, payload: mmc.Payload,
} }
return message, nil return message, nil
@@ -39,7 +69,7 @@ func (mmc *MQTTMessageCreate) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "mqtt.message.create", Type: "mqtt.message.create",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
topic, ok := params["topic"] topic, ok := params["topic"]
@@ -84,7 +114,19 @@ func init() {
return nil, fmt.Errorf("mqtt.message.create requires an payload parameter") return nil, fmt.Errorf("mqtt.message.create requires an payload parameter")
} }
return &MQTTMessageCreate{config: config, Topic: topicString, QoS: byte(qosByte), Retained: retainedBool, Payload: payload}, nil if payloadBytes, ok := payload.([]byte); ok {
return &MQTTMessageCreate{config: config, Topic: topicString, QoS: byte(qosByte), Retained: retainedBool, Payload: payloadBytes}, nil
}
payloadString, ok := payload.(string)
if !ok {
return nil, fmt.Errorf("mqtt.message.create payload must be a string or byte array")
}
payloadBytes := []byte(payloadString)
return &MQTTMessageCreate{config: config, Topic: topicString, QoS: byte(qosByte), Retained: retainedBool, Payload: payloadBytes}, nil
}, },
}) })
} }

View File

@@ -1,14 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type MQTTMessageEncode struct { type MQTTMessageEncode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (mme *MQTTMessageEncode) Process(ctx context.Context, payload any) (any, error) { func (mme *MQTTMessageEncode) Process(ctx context.Context, payload any) (any, error) {
@@ -28,7 +29,7 @@ func (mme *MQTTMessageEncode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "mqtt.message.encode", Type: "mqtt.message.encode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &MQTTMessageEncode{config: config}, nil return &MQTTMessageEncode{config: config}, nil
}, },
}) })

View File

@@ -1,10 +1,12 @@
package processing package processor
import ( import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"text/template" "text/template"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type NATSMessage struct { type NATSMessage struct {
@@ -13,7 +15,7 @@ type NATSMessage struct {
} }
type NATSMessageCreate struct { type NATSMessageCreate struct {
config ProcessorConfig config config.ProcessorConfig
Subject string Subject string
Payload *template.Template Payload *template.Template
} }
@@ -44,7 +46,7 @@ func (nmc *NATSMessageCreate) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "nats.message.create", Type: "nats.message.create",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
// TODO(jwetzell): support template for subject // TODO(jwetzell): support template for subject
subject, ok := params["subject"] subject, ok := params["subject"]

View File

@@ -1,14 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
) )
type NATSMessageEncode struct { type NATSMessageEncode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (nme *NATSMessageEncode) Process(ctx context.Context, payload any) (any, error) { func (nme *NATSMessageEncode) Process(ctx context.Context, payload any) (any, error) {
@@ -28,7 +29,7 @@ func (nme *NATSMessageEncode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "nats.message.encode", Type: "nats.message.encode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &NATSMessageEncode{config: config}, nil return &NATSMessageEncode{config: config}, nil
}, },
}) })

View File

@@ -1,4 +1,4 @@
package processing package processor
import ( import (
"bytes" "bytes"
@@ -9,10 +9,11 @@ import (
"text/template" "text/template"
"github.com/jwetzell/osc-go" "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type OSCMessageCreate struct { type OSCMessageCreate struct {
config ProcessorConfig config config.ProcessorConfig
Address *template.Template Address *template.Template
Args []*template.Template Args []*template.Template
Types string Types string
@@ -76,7 +77,7 @@ func (o *OSCMessageCreate) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "osc.message.create", Type: "osc.message.create",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
address, ok := params["address"] address, ok := params["address"]

View File

@@ -1,14 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
osc "github.com/jwetzell/osc-go" osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type OSCMessageDecode struct { type OSCMessageDecode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (o *OSCMessageDecode) Process(ctx context.Context, payload any) (any, error) { func (o *OSCMessageDecode) Process(ctx context.Context, payload any) (any, error) {
@@ -40,7 +41,7 @@ func (o *OSCMessageDecode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "osc.message.decode", Type: "osc.message.decode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &OSCMessageDecode{config: config}, nil return &OSCMessageDecode{config: config}, nil
}, },
}) })

View File

@@ -1,14 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
osc "github.com/jwetzell/osc-go" osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type OSCMessageEncode struct { type OSCMessageEncode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (o *OSCMessageEncode) Process(ctx context.Context, payload any) (any, error) { func (o *OSCMessageEncode) Process(ctx context.Context, payload any) (any, error) {
@@ -29,7 +30,7 @@ func (o *OSCMessageEncode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "osc.message.encode", Type: "osc.message.encode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &OSCMessageEncode{config: config}, nil return &OSCMessageEncode{config: config}, nil
}, },
}) })

View File

@@ -1,4 +1,4 @@
package processing package processor
import ( import (
"context" "context"
@@ -7,10 +7,11 @@ import (
"strings" "strings"
"github.com/jwetzell/osc-go" "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type OSCMessageFilter struct { type OSCMessageFilter struct {
config ProcessorConfig config config.ProcessorConfig
Address *regexp.Regexp Address *regexp.Regexp
} }
@@ -36,7 +37,7 @@ func (o *OSCMessageFilter) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "osc.message.filter", Type: "osc.message.filter",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
address, ok := params["address"] address, ok := params["address"]

View File

@@ -1,4 +1,4 @@
package processing package processor
import ( import (
"bytes" "bytes"
@@ -7,10 +7,11 @@ import (
"text/template" "text/template"
osc "github.com/jwetzell/osc-go" osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type OSCMessageTransform struct { type OSCMessageTransform struct {
config ProcessorConfig config config.ProcessorConfig
Address *template.Template Address *template.Template
} }
@@ -51,7 +52,7 @@ func (o *OSCMessageTransform) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "osc.message.transform", Type: "osc.message.transform",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
address, ok := params["address"] address, ok := params["address"]

View File

@@ -1,9 +1,11 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"sync" "sync"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type Processor interface { type Processor interface {
@@ -11,14 +13,9 @@ type Processor interface {
Process(context.Context, any) (any, error) Process(context.Context, any) (any, error)
} }
type ProcessorConfig struct {
Type string `json:"type"`
Params map[string]any `json:"params"`
}
type ProcessorRegistration struct { type ProcessorRegistration struct {
Type string `json:"type"` Type string `json:"type"`
New func(ProcessorConfig) (Processor, error) New func(config.ProcessorConfig) (Processor, error)
} }
func RegisterProcessor(processor ProcessorRegistration) { func RegisterProcessor(processor ProcessorRegistration) {

View File

@@ -1,4 +1,4 @@
package processing package processor
import ( import (
"context" "context"
@@ -6,11 +6,12 @@ import (
"github.com/expr-lang/expr" "github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm" "github.com/expr-lang/expr/vm"
"github.com/jwetzell/showbridge-go/internal/config"
) )
// NOTE(jwetzell): see language definition https://expr-lang.org/docs/language-definition // NOTE(jwetzell): see language definition https://expr-lang.org/docs/language-definition
type ScriptExpr struct { type ScriptExpr struct {
config ProcessorConfig config config.ProcessorConfig
Program *vm.Program Program *vm.Program
} }
@@ -31,7 +32,7 @@ func (se *ScriptExpr) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "script.expr", Type: "script.expr",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
expression, ok := params["expression"] expression, ok := params["expression"]

View File

@@ -1,15 +1,16 @@
package processing package processor
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/config"
"modernc.org/quickjs" "modernc.org/quickjs"
) )
type ScriptJS struct { type ScriptJS struct {
config ProcessorConfig config config.ProcessorConfig
Program string Program string
} }
@@ -67,7 +68,7 @@ func (sj *ScriptJS) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "script.js", Type: "script.js",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
program, ok := params["program"] program, ok := params["program"]

View File

@@ -1,14 +1,16 @@
package processing package processor
import ( import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"text/template" "text/template"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type StringCreate struct { type StringCreate struct {
config ProcessorConfig config config.ProcessorConfig
Template *template.Template Template *template.Template
} }
@@ -32,7 +34,7 @@ func (sc *StringCreate) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "string.create", Type: "string.create",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
tmpl, ok := params["template"] tmpl, ok := params["template"]

View File

@@ -1,12 +1,14 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type StringDecode struct { type StringDecode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (sd *StringDecode) Process(ctx context.Context, payload any) (any, error) { func (sd *StringDecode) Process(ctx context.Context, payload any) (any, error) {
@@ -28,7 +30,7 @@ func (sd *StringDecode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "string.decode", Type: "string.decode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &StringDecode{config: config}, nil return &StringDecode{config: config}, nil
}, },
}) })

View File

@@ -0,0 +1,71 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestGoodStringDecode(t *testing.T) {
stringDecoder := processor.StringDecode{}
tests := []struct {
processor processor.Processor
name string
payload any
expected string
}{
{
processor: &stringDecoder,
name: "hello",
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
expected: "hello",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
gotString, ok := got.(string)
if !ok {
t.Errorf("string.decode returned a %T payload: %s", got, got)
}
if err != nil {
t.Errorf("string.decode failed: %s", err)
}
if gotString != test.expected {
t.Errorf("string.decode got %s, expected %s", got, test.expected)
}
})
}
}
func TestBadStringDecode(t *testing.T) {
stringDecoder := processor.StringDecode{}
tests := []struct {
processor processor.Processor
name string
payload any
errorString string
}{
{
processor: &stringDecoder,
name: "non-[]byte input",
payload: "hello",
errorString: "string.decode processor only accepts a []byte",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
if err == nil {
t.Errorf("string.decode expected to fail but got payload: %s", got)
}
if err.Error() != test.errorString {
t.Errorf("string.decode got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -1,12 +1,14 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type StringEncode struct { type StringEncode struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (se *StringEncode) Process(ctx context.Context, payload any) (any, error) { func (se *StringEncode) Process(ctx context.Context, payload any) (any, error) {
@@ -28,7 +30,7 @@ func (se *StringEncode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "string.encode", Type: "string.encode",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &StringEncode{config: config}, nil return &StringEncode{config: config}, nil
}, },
}) })

View File

@@ -0,0 +1,72 @@
package processor_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestGoodStringEncode(t *testing.T) {
stringEncoder := processor.StringEncode{}
tests := []struct {
processor processor.Processor
name string
payload any
expected []byte
}{
{
processor: &stringEncoder,
name: "hello",
payload: "hello",
expected: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
gotBytes, ok := got.([]byte)
if !ok {
t.Errorf("string.encode returned a %T payload: %s", got, got)
}
if err != nil {
t.Errorf("string.encode failed: %s", err)
}
if !slices.Equal(gotBytes, test.expected) {
t.Errorf("string.encode got %s, expected %s", got, test.expected)
}
})
}
}
func TestBadStringEncode(t *testing.T) {
stringEncoder := processor.StringEncode{}
tests := []struct {
processor processor.Processor
name string
payload any
errorString string
}{
{
processor: &stringEncoder,
name: "non-string input",
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
errorString: "string.encode processor only accepts a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
if err == nil {
t.Errorf("string.encode expected to fail but got payload: %s", got)
}
if err.Error() != test.errorString {
t.Errorf("string.encode got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -1,13 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"regexp" "regexp"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type StringFilter struct { type StringFilter struct {
config ProcessorConfig config config.ProcessorConfig
Pattern *regexp.Regexp Pattern *regexp.Regexp
} }
@@ -32,7 +34,7 @@ func (se *StringFilter) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "string.filter", Type: "string.filter",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
pattern, ok := params["pattern"] pattern, ok := params["pattern"]

View File

@@ -1,13 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type StringSplit struct { type StringSplit struct {
config ProcessorConfig config config.ProcessorConfig
Separator string Separator string
} }
@@ -30,7 +32,7 @@ func (se *StringSplit) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "string.split", Type: "string.split",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
separator, ok := params["separator"] separator, ok := params["separator"]

View File

@@ -0,0 +1,70 @@
package processor_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestGoodStringSplit(t *testing.T) {
tests := []struct {
processor processor.Processor
name string
payload any
expected []string
}{
{
processor: &processor.StringSplit{Separator: ","},
name: "comma separated",
payload: "part1,part2,part3",
expected: []string{"part1", "part2", "part3"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
gotStrings, ok := got.([]string)
if !ok {
t.Errorf("string.split returned a %T payload: %s", got, got)
}
if err != nil {
t.Errorf("string.split failed: %s", err)
}
if !slices.Equal(gotStrings, test.expected) {
t.Errorf("string.split got %s, expected %s", got, test.expected)
}
})
}
}
func TestBasStringSplit(t *testing.T) {
tests := []struct {
processor processor.Processor
name string
payload any
errorString string
}{
{
processor: &processor.StringSplit{Separator: ","},
name: "hello",
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
errorString: "string.split only accepts a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
if err == nil {
t.Errorf("string.split expected error but got none, payload: %s", got)
}
if err.Error() != test.errorString {
t.Errorf("string.split got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -1,13 +1,15 @@
package processing package processor
import ( import (
"context" "context"
"fmt" "fmt"
"strconv" "strconv"
"github.com/jwetzell/showbridge-go/internal/config"
) )
type UintParse struct { type UintParse struct {
config ProcessorConfig config config.ProcessorConfig
} }
func (up *UintParse) Process(ctx context.Context, payload any) (any, error) { func (up *UintParse) Process(ctx context.Context, payload any) (any, error) {
@@ -32,7 +34,7 @@ func (up *UintParse) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "uint.parse", Type: "uint.parse",
New: func(config ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &UintParse{config: config}, nil return &UintParse{config: config}, nil
}, },
}) })

View File

@@ -0,0 +1,80 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestGoodUintParse(t *testing.T) {
uintParser := processor.UintParse{}
tests := []struct {
processor processor.Processor
name string
payload any
expected uint64
}{
{
name: "positive number",
payload: "12345",
expected: 12345,
},
{
name: "zero",
payload: "0",
expected: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := uintParser.Process(t.Context(), test.payload)
gotUint, ok := got.(uint64)
if !ok {
t.Errorf("uint.parse returned a %T payload: %s", got, got)
}
if err != nil {
t.Errorf("uint.parse failed: %s", err)
}
if gotUint != test.expected {
t.Errorf("uint.parse got %d, expected %d", gotUint, test.expected)
}
})
}
}
func TestBadUintParse(t *testing.T) {
uintParser := processor.UintParse{}
tests := []struct {
processor processor.Processor
name string
payload any
errorString string
}{
{
name: "non-string input",
payload: []byte{0x01},
errorString: "uint.parse processor only accepts a string",
},
{
name: "not uint string",
payload: "-1234",
errorString: "strconv.ParseUint: parsing \"-1234\": invalid syntax",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := uintParser.Process(t.Context(), test.payload)
if err == nil {
t.Errorf("uint.parse expected to fail but succeeded, got: %v", got)
}
if err.Error() != test.errorString {
t.Errorf("uint.parse got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

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/processor"
)
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 []processor.Processor
output string
}
func NewRoute(config config.RouteConfig) (Route, error) {
processors := []processor.Processor{}
if len(config.Processors) > 0 {
for _, processorDecl := range config.Processors {
processorInfo, ok := processor.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)
}

View File

@@ -1,118 +0,0 @@
//go:build cgo
package showbridge
import (
"fmt"
"log/slog"
"gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)
type MIDIClient struct {
config ModuleConfig
router *Router
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(config ModuleConfig) (Module, error) {
params := config.Params
input, ok := params["input"]
if !ok {
return nil, fmt.Errorf("net.mqtt.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("net.mqtt.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}, nil
},
})
}
func (mc *MIDIClient) Id() string {
return mc.config.Id
}
func (mc *MIDIClient) Type() string {
return mc.config.Type
}
func (mc *MIDIClient) RegisterRouter(router *Router) {
mc.router = router
}
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.router.Context.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)
}

View File

@@ -1,67 +0,0 @@
package showbridge
import (
"fmt"
"github.com/jwetzell/showbridge-go/internal/processing"
)
type RouteError struct {
Index int
Config RouteConfig
Error error
}
type Route struct {
index int
Input string
Processors []processing.Processor
Output string
router *Router
}
type RouteConfig struct {
Input string `json:"input"`
Processors []processing.ProcessorConfig `json:"processors"`
Output string `json:"output"`
}
func NewRoute(index int, config RouteConfig, router *Router) (*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 &Route{Input: config.Input, Processors: processors, Output: config.Output, router: router, index: index}, nil
}
func (r *Route) HandleInput(sourceId string, payload any) error {
var err error
for _, processor := range r.Processors {
payload, err = processor.Process(r.router.Context, 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(sourceId, payload)
}
func (r *Route) HandleOutput(sourceId string, payload any) error {
return r.router.HandleOutput(sourceId, r.Output, payload)
}

View File

@@ -6,23 +6,21 @@ import (
"log/slog" "log/slog"
"os" "os"
"sync" "sync"
)
type RoutingError struct { "github.com/jwetzell/showbridge-go/internal/config"
Index int "github.com/jwetzell/showbridge-go/internal/module"
Error error "github.com/jwetzell/showbridge-go/internal/route"
} )
type Router struct { type Router struct {
contextCancel context.CancelFunc contextCancel context.CancelFunc
Context context.Context Context context.Context
ModuleInstances []Module ModuleInstances []module.Module
RouteInstances []*Route RouteInstances []route.Route
moduleWait sync.WaitGroup moduleWait sync.WaitGroup
} }
func NewRouter(ctx context.Context, config Config) (*Router, []ModuleError, []RouteError) { func NewRouter(ctx context.Context, config config.Config) (*Router, []module.ModuleError, []route.RouteError) {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo, Level: slog.LevelInfo,
})) }))
@@ -35,20 +33,20 @@ func NewRouter(ctx context.Context, config Config) (*Router, []ModuleError, []Ro
router := Router{ router := Router{
Context: routerContext, Context: routerContext,
contextCancel: cancel, contextCancel: cancel,
ModuleInstances: []Module{}, ModuleInstances: []module.Module{},
RouteInstances: []*Route{}, RouteInstances: []route.Route{},
} }
var moduleErrors []ModuleError var moduleErrors []module.ModuleError
for moduleIndex, moduleDecl := range config.Modules { for moduleIndex, moduleDecl := range config.Modules {
moduleInfo, ok := moduleRegistry[moduleDecl.Type] moduleInfo, ok := module.ModuleRegistry[moduleDecl.Type]
if !ok { if !ok {
if moduleErrors == nil { if moduleErrors == nil {
moduleErrors = []ModuleError{} moduleErrors = []module.ModuleError{}
} }
moduleErrors = append(moduleErrors, ModuleError{ moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex, Index: moduleIndex,
Config: moduleDecl, Config: moduleDecl,
Error: fmt.Errorf("module type not defined"), Error: fmt.Errorf("module type not defined"),
@@ -61,9 +59,9 @@ func NewRouter(ctx context.Context, config Config) (*Router, []ModuleError, []Ro
if moduleInstance.Id() == moduleDecl.Id { if moduleInstance.Id() == moduleDecl.Id {
moduleInstanceExists = true moduleInstanceExists = true
if moduleErrors == nil { if moduleErrors == nil {
moduleErrors = []ModuleError{} moduleErrors = []module.ModuleError{}
} }
moduleErrors = append(moduleErrors, ModuleError{ moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex, Index: moduleIndex,
Config: moduleDecl, Config: moduleDecl,
Error: fmt.Errorf("duplicate module id"), Error: fmt.Errorf("duplicate module id"),
@@ -73,12 +71,12 @@ func NewRouter(ctx context.Context, config Config) (*Router, []ModuleError, []Ro
} }
if !moduleInstanceExists { if !moduleInstanceExists {
moduleInstance, err := moduleInfo.New(moduleDecl) moduleInstance, err := moduleInfo.New(router.Context, moduleDecl, &router)
if err != nil { if err != nil {
if moduleErrors == nil { if moduleErrors == nil {
moduleErrors = []ModuleError{} moduleErrors = []module.ModuleError{}
} }
moduleErrors = append(moduleErrors, ModuleError{ moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex, Index: moduleIndex,
Config: moduleDecl, Config: moduleDecl,
Error: err, Error: err,
@@ -91,26 +89,21 @@ func NewRouter(ctx context.Context, config Config) (*Router, []ModuleError, []Ro
} }
var routeErrors []RouteError var routeErrors []route.RouteError
for routeIndex, routeDecl := range config.Routes { for routeIndex, routeDecl := range config.Routes {
route, err := NewRoute(routeIndex, routeDecl, &router) routeInstance, err := route.NewRoute(routeDecl)
if err != nil { if err != nil {
if routeErrors == nil { if routeErrors == nil {
routeErrors = []RouteError{} routeErrors = []route.RouteError{}
} }
routeErrors = append(routeErrors, RouteError{ routeErrors = append(routeErrors, route.RouteError{
Index: routeIndex, Index: routeIndex,
Config: routeDecl, Config: routeDecl,
Error: err, Error: err,
}) })
continue continue
} }
router.RouteInstances = append(router.RouteInstances, route) router.RouteInstances = append(router.RouteInstances, routeInstance)
}
for _, moduleInstance := range router.ModuleInstances {
slog.Debug("registering router with module", "id", moduleInstance.Id())
moduleInstance.RegisterRouter(&router)
} }
return &router, moduleErrors, routeErrors return &router, moduleErrors, routeErrors
@@ -119,7 +112,6 @@ func NewRouter(ctx context.Context, config Config) (*Router, []ModuleError, []Ro
func (r *Router) Run() { func (r *Router) Run() {
slog.Info("running router") slog.Info("running router")
for _, moduleInstance := range r.ModuleInstances { for _, moduleInstance := range r.ModuleInstances {
moduleInstance.RegisterRouter(r)
r.moduleWait.Add(1) r.moduleWait.Add(1)
go func() { go func() {
err := moduleInstance.Run() err := moduleInstance.Run()
@@ -138,16 +130,16 @@ func (r *Router) Stop() {
r.contextCancel() r.contextCancel()
} }
func (r *Router) HandleInput(sourceId string, payload any) []RoutingError { func (r *Router) HandleInput(sourceId string, payload any) []route.RouteIOError {
var routingErrors []RoutingError var routingErrors []route.RouteIOError
for routeIndex, route := range r.RouteInstances { for routeIndex, routeInstance := range r.RouteInstances {
if route.Input == sourceId { if routeInstance.Input() == sourceId {
err := route.HandleInput(sourceId, payload) err := routeInstance.HandleInput(r.Context, sourceId, payload, r)
if err != nil { if err != nil {
if routingErrors == nil { if routingErrors == nil {
routingErrors = []RoutingError{} routingErrors = []route.RouteIOError{}
} }
routingErrors = append(routingErrors, RoutingError{ routingErrors = append(routingErrors, route.RouteIOError{
Index: routeIndex, Index: routeIndex,
Error: err, Error: err,
}) })

View File

@@ -1,171 +0,0 @@
package showbridge
import (
"fmt"
"log/slog"
"net"
"sync"
"time"
"github.com/jwetzell/showbridge-go/internal/framing"
)
type TCPServer struct {
config ModuleConfig
Ip string
Port uint16
framingMethod string
router *Router
quit chan interface{}
wg sync.WaitGroup
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.tcp.server",
New: func(config ModuleConfig) (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 uint16")
}
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("tcp framing method must be a string")
}
ipString := "0.0.0.0"
ip, ok := params["ip"]
if ok {
specificIpString, ok := ip.(string)
if !ok {
return nil, fmt.Errorf("tcp ip method must be a string")
}
ipString = specificIpString
}
return &TCPServer{framingMethod: framingMethodString, Port: uint16(portNum), Ip: ipString, config: config, quit: make(chan interface{})}, nil
},
})
}
func (ts *TCPServer) Id() string {
return ts.config.Id
}
func (ts *TCPServer) Type() string {
return ts.config.Type
}
func (ts *TCPServer) RegisterRouter(router *Router) {
ts.router = router
}
func (ts *TCPServer) handleClient(client net.Conn) {
slog.Debug("connection accepted", "id", ts.config.Id, "remoteAddr", client.RemoteAddr().String())
defer client.Close()
var framer framing.Framer
switch ts.framingMethod {
case "LF":
framer = framing.NewByteSeparatorFramer([]byte{'\n'})
case "CR":
framer = framing.NewByteSeparatorFramer([]byte{'\r'})
case "CRLF":
framer = framing.NewByteSeparatorFramer([]byte{'\r', '\n'})
case "SLIP":
framer = framing.NewSlipFramer()
}
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 {
//NOTE(jwetzell) we hit deadline
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
continue ClientRead
}
if err.Error() == "EOF" {
slog.Debug("connection closed", "id", ts.config.Id, "remoteAddr", client.RemoteAddr().String())
}
return
}
if framer != nil {
if byteCount > 0 {
messages := framer.Decode(buffer[0:byteCount])
for _, message := range messages {
if ts.router != nil {
ts.router.HandleInput(ts.config.Id, message)
} else {
slog.Error("tcp-server has no router", "id", ts.config.Id)
}
}
}
}
}
}
}
func (ts *TCPServer) Run() error {
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ts.Ip, ts.Port))
if err != nil {
return err
}
ts.wg.Add(1)
go func() {
<-ts.router.Context.Done()
close(ts.quit)
listener.Close()
slog.Debug("router context done in module", "id", ts.config.Id)
}()
AcceptLoop:
for {
conn, err := listener.Accept()
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 {
return fmt.Errorf("net.tcp.server output is not implemented")
}

View File

@@ -1,97 +0,0 @@
package showbridge
import (
"fmt"
"log/slog"
"net"
)
type UDPClient struct {
config ModuleConfig
Host string
Port uint16
conn net.Conn
router *Router
addr *net.UDPAddr
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.udp.client",
New: func(config ModuleConfig) (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 uint16")
}
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")
}
return &UDPClient{Host: hostString, Port: uint16(portNum), config: config}, nil
},
})
}
func (uc *UDPClient) Id() string {
return uc.config.Id
}
func (uc *UDPClient) Type() string {
return uc.config.Type
}
func (uc *UDPClient) RegisterRouter(router *Router) {
uc.router = router
}
func (uc *UDPClient) Run() error {
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", uc.Host, uc.Port))
if err != nil {
return err
}
uc.addr = addr
<-uc.router.Context.Done()
slog.Debug("router context done in module", "id", uc.config.Id)
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")
}
client, err := net.DialUDP("udp", nil, uc.addr)
if err != nil {
return err
}
uc.conn = client
_, err = uc.conn.Write(payloadBytes)
if err != nil {
return err
}
return nil
}