68 Commits

Author SHA1 Message Date
Joel Wetzell
168265f0a9 move docker building to main release workflow 2025-12-24 14:05:40 -06:00
Joel Wetzell
caee7b269c fix docker job name 2025-12-24 13:26:28 -06:00
Joel Wetzell
b0cc47236f add debug flag and working directory to launch.json 2025-12-24 13:26:09 -06:00
Joel Wetzell
f6c2b1d9ac Merge pull request #24 from jwetzell/feat/docker
add docker image
2025-12-24 13:24:29 -06:00
Joel Wetzell
f977b845be add github action to build docker image 2025-12-24 13:22:52 -06:00
Joel Wetzell
0166383978 set workdir 2025-12-24 13:22:45 -06:00
Joel Wetzell
66cfaa3091 update Go version in dockerfile 2025-12-24 13:22:34 -06:00
Joel Wetzell
0904d2fcb8 add more to dockerignore 2025-12-24 13:21:53 -06:00
Joel Wetzell
6a13c38e77 add dockerfile 2025-12-24 12:44:25 -06:00
Joel Wetzell
1a8ccfc64a align variable name 2025-12-24 10:54:30 -06:00
Joel Wetzell
2e3bb408c3 Merge pull request #23 from jwetzell/feat/configurable-number-parsing
make base and bitsize configurable for number parsers
2025-12-24 10:35:06 -06:00
Joel Wetzell
3d75165a61 cleanout TODO 2025-12-24 10:34:20 -06:00
Joel Wetzell
50f755f914 make base and bitsize configurable for number parsers 2025-12-24 10:34:06 -06:00
Joel Wetzell
b10e296d0a Merge pull request #22 from jwetzell/feat/udp-server-buffersize
allow configuring buffer size for udp server
2025-12-24 10:27:14 -06:00
Joel Wetzell
91c44420cb allow configuring buffer size for udp server 2025-12-24 10:24:24 -06:00
Joel Wetzell
c1b2fa714e update midi module section 2025-12-23 15:06:23 -06:00
Joel Wetzell
ff1949ce69 use errors.New when not formatting 2025-12-23 15:00:30 -06:00
Joel Wetzell
76583b1fb8 Merge pull request #20 from jwetzell/dependabot/go_modules/github.com/expr-lang/expr-1.17.7
Bump github.com/expr-lang/expr from 1.17.6 to 1.17.7
2025-12-23 14:29:05 -06:00
dependabot[bot]
e6433878b5 Bump github.com/expr-lang/expr from 1.17.6 to 1.17.7
Bumps [github.com/expr-lang/expr](https://github.com/expr-lang/expr) from 1.17.6 to 1.17.7.
- [Release notes](https://github.com/expr-lang/expr/releases)
- [Commits](https://github.com/expr-lang/expr/compare/v1.17.6...v1.17.7)

---
updated-dependencies:
- dependency-name: github.com/expr-lang/expr
  dependency-version: 1.17.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-23 20:27:34 +00:00
Joel Wetzell
3ad067cbb8 Merge pull request #19 from jwetzell/dependabot/go_modules/github.com/emiago/sipgo-1.0.1
Bump github.com/emiago/sipgo from 1.0.1-alpha.0.20251212165843-9c9bcdf9126f to 1.0.1
2025-12-23 14:26:28 -06:00
Joel Wetzell
30b406d601 Merge pull request #21 from jwetzell/dependabot/go_modules/github.com/nats-io/nats.go-1.48.0
Bump github.com/nats-io/nats.go from 1.47.0 to 1.48.0
2025-12-23 14:25:25 -06:00
dependabot[bot]
7315d40015 Bump github.com/nats-io/nats.go from 1.47.0 to 1.48.0
Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.47.0 to 1.48.0.
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.47.0...v1.48.0)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats.go
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 17:33:17 +00:00
dependabot[bot]
e7105b8b39 Bump github.com/emiago/sipgo
Bumps [github.com/emiago/sipgo](https://github.com/emiago/sipgo) from 1.0.1-alpha.0.20251212165843-9c9bcdf9126f to 1.0.1.
- [Release notes](https://github.com/emiago/sipgo/releases)
- [Commits](https://github.com/emiago/sipgo/commits/v1.0.1)

---
updated-dependencies:
- dependency-name: github.com/emiago/sipgo
  dependency-version: 1.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 17:33:08 +00:00
Joel Wetzell
ec40194ecb add TODO 2025-12-21 15:13:06 -06:00
Joel Wetzell
85d8dc5787 cleanup error messages 2025-12-20 14:18:56 -06:00
Joel Wetzell
b27bfb1a6d don't wrap string.filter pattern 2025-12-20 10:59:33 -06:00
Joel Wetzell
fb9c0bc657 cleanup error messages 2025-12-20 07:50:22 -06:00
Joel Wetzell
d32d23041b add usage to cmd 2025-12-19 22:30:18 -06:00
Joel Wetzell
b4149df00a setup slog inside cmd with debug and json flag 2025-12-19 22:27:05 -06:00
Joel Wetzell
61bd4b64f5 setup loggers inside modules with attributes 2025-12-19 22:26:25 -06:00
Joel Wetzell
38d73881c9 return error don't just log it 2025-12-19 22:25:26 -06:00
Joel Wetzell
3138bdfcdb remove TODO's 2025-12-19 12:32:13 -06:00
Joel Wetzell
ba862300b2 fix config example 2025-12-19 09:02:05 -06:00
Joel Wetzell
e414bf336e Merge pull request #18 from jwetzell/test/script-processors
add basic tests for script processors
2025-12-18 20:06:21 -06:00
Joel Wetzell
da95fb5c75 add basic test for script.js 2025-12-18 20:04:52 -06:00
Joel Wetzell
1fdc30aa9e add basic test for script.expr 2025-12-18 20:04:41 -06:00
Joel Wetzell
128920dff9 fix typo in string.split test 2025-12-18 20:03:50 -06:00
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
65 changed files with 1826 additions and 461 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
Dockerfile
.github
.vscode
README.md
LICENSE

View File

@@ -3,7 +3,7 @@ name: showbridge release
on:
push:
tags:
- "*"
- "v*"
permissions:
contents: write
@@ -29,3 +29,40 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker:
runs-on: ubuntu-latest
needs: goreleaser
steps:
- name: Check out repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: |
jwetzell/showbridge
- name: Build and push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
push: true
context: ./
file: ./Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64,linux/arm/v7

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

@@ -0,0 +1,18 @@
{
// 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",
"args": ["--debug"],
"cwd": "./"
}
]
}

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
ARG GO_VERSION=1.25.5
FROM golang:${GO_VERSION}-alpine AS build
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build ./cmd/showbridge
FROM scratch
WORKDIR /app
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /build/showbridge /app/showbridge
ENTRYPOINT [ "/app/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

@@ -22,8 +22,13 @@ Simple protocol router _/s_
- client
- [PosiStageNet](https://posistage.net/)
- client
- MIDI
- client (not included in pre-built binaries yet)
- MIDI (not included in pre-built binaries yet)
- input
- output
- [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:
- "amd64"
- "arm64"
ldflags:
- '-s -w -X main.version={{.RawVersion}}-{{.ShortCommit}}'
archives:
- formats: [tar.gz]

View File

@@ -2,7 +2,7 @@ package main
import (
"context"
"fmt"
"errors"
"log/slog"
"os"
"os/signal"
@@ -13,32 +13,73 @@ import (
"sigs.k8s.io/yaml"
)
var (
version = "dev"
)
func main() {
cmd := &cli.Command{
Name: "showbridge",
Usage: "Simple protocol router /s",
Version: version,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Value: "./config.yaml",
Usage: "path to config file",
},
&cli.BoolFlag{
Name: "debug",
Value: false,
Usage: "set log level to DEBUG",
},
&cli.BoolFlag{
Name: "json",
Value: false,
Usage: "log using JSON",
},
},
Action: func(ctx context.Context, c *cli.Command) error {
configPath := c.String("config")
if configPath == "" {
return fmt.Errorf("config value cannot be empty")
return errors.New("config value cannot be empty")
}
config, err := readConfig(configPath)
if err != nil {
return err
}
logLevel := slog.LevelInfo
if c.Bool("debug") {
logLevel = slog.LevelDebug
}
logHandlerOptions := &slog.HandlerOptions{
Level: logLevel,
}
logOutput := os.Stderr
var logHandler slog.Handler = slog.NewTextHandler(logOutput, logHandlerOptions)
if c.Bool("json") {
logHandler = slog.NewJSONHandler(logOutput, logHandlerOptions)
}
logger := slog.New(logHandler)
slog.SetDefault(logger)
router, moduleErrors, routeErrors := showbridge.NewRouter(ctx, config)
for _, moduleError := range moduleErrors {
slog.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error)
logger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error)
}
for _, routeError := range routeErrors {
slog.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error)
logger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error)
}
router.Run()
return nil

View File

@@ -1,6 +1,6 @@
modules:
- id: http
type: net.http.server
type: http.server
params:
port: 3000
- id: udp

25
go.mod
View File

@@ -4,13 +4,15 @@ go 1.25.3
require (
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/expr-lang/expr v1.17.6
github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111
github.com/emiago/sipgo v1.0.1
github.com/expr-lang/expr v1.17.7
github.com/jwetzell/free-d-go v0.1.0
github.com/jwetzell/osc-go v0.1.0
github.com/jwetzell/psn-go v0.3.0
github.com/nats-io/nats.go v1.47.0
github.com/nats-io/nats.go v1.48.0
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
sigs.k8s.io/yaml v1.6.0
@@ -19,20 +21,33 @@ require (
require (
github.com/creack/goselect v0.1.2 // 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/gorilla/websocket v1.5.3 // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // 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/zaf/g711 v1.4.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/sync v0.19.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/libquickjs v0.12.2 // indirect
modernc.org/mathutil v1.7.1 // indirect

65
go.sum
View File

@@ -6,16 +6,30 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
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/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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 h1:8eCZ6L/VX3isyByyv1RrBoQ5GyBoRXBHkNMYjwacRfk=
github.com/emiago/sipgo v1.0.1/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
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/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/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/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/go.mod h1:KmrkooRARRaxJTBSPvwt/6IMAIaHH1R8bSA8cwbbELw=
github.com/jwetzell/osc-go v0.1.0 h1:EXxup5VWBErHot2Ri4MFToPf6KCzLDTbCt2x6GLfw8I=
@@ -24,26 +38,46 @@ github.com/jwetzell/psn-go v0.3.0 h1:WVpCEmExYE8a+I5hQak5jNJJp2x35VdGX/VuMUKPmhY
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/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/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/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
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/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/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/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/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
gitlab.com/gomidi/midi/v2 v2.3.16 h1:yufWSENyjnJ4LFQa9BerzUm4E4aLfTyzw5nmnCteO0c=
gitlab.com/gomidi/midi/v2 v2.3.16/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw=
github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
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=
@@ -58,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/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
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.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
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/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 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/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/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=

View File

@@ -2,7 +2,7 @@ package module
import (
"context"
"fmt"
"errors"
"log/slog"
"net/http"
"time"
@@ -16,14 +16,15 @@ type HTTPClient struct {
ctx context.Context
client *http.Client
router route.RouteIO
logger *slog.Logger
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.http.client",
Type: "http.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
return &HTTPClient{config: config, ctx: ctx, router: router}, nil
return &HTTPClient{config: config, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -43,7 +44,7 @@ func (hc *HTTPClient) Run() error {
}
<-hc.ctx.Done()
slog.Debug("router context done in module", "id", hc.config.Id)
hc.logger.Debug("router context done in module")
return nil
}
@@ -52,11 +53,11 @@ func (hc *HTTPClient) Output(payload any) error {
payloadRequest, ok := payload.(*http.Request)
if !ok {
return fmt.Errorf("net.http.client is only able to output an http.Request")
return errors.New("http.client is only able to output an http.Request")
}
if hc.client == nil {
return fmt.Errorf("net.http.client client is nil")
return errors.New("http.client client is nil")
}
response, err := hc.client.Do(payloadRequest)
@@ -66,7 +67,7 @@ func (hc *HTTPClient) Output(payload any) error {
}
if hc.router != nil {
hc.router.HandleInput(hc.config.Id, response)
hc.router.HandleInput(hc.Id(), response)
}
return nil

View File

@@ -3,6 +3,7 @@ package module
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
@@ -16,6 +17,7 @@ type HTTPServer struct {
Port uint16
ctx context.Context
router route.RouteIO
logger *slog.Logger
}
type ResponseData struct {
@@ -25,21 +27,21 @@ type ResponseData struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "net.http.server",
Type: "http.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.http.server requires a port parameter")
return nil, errors.New("http.server requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("net.http.server port must be uint16")
return nil, errors.New("http.server port must be uint16")
}
return &HTTPServer{Port: uint16(portNum), config: config, ctx: ctx, router: router}, nil
return &HTTPServer{Port: uint16(portNum), config: config, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -61,7 +63,7 @@ func (hs *HTTPServer) HandleDefault(w http.ResponseWriter, r *http.Request) {
}
if hs.router != nil {
routingErrors := hs.router.HandleInput(hs.config.Id, r)
routingErrors := hs.router.HandleInput(hs.Id(), r)
if routingErrors != nil {
w.WriteHeader(http.StatusInternalServerError)
response.Status = "error"
@@ -89,12 +91,11 @@ func (hs *HTTPServer) Run() error {
go func() {
<-hs.ctx.Done()
slog.Debug("router context done in module", "id", hs.config.Id)
hs.logger.Debug("router context done in module")
httpServer.Close()
}()
err := httpServer.ListenAndServe()
slog.Debug("net.http.server closed", "id", hs.config.Id)
// TODO(jwetzell): handle server closed error differently
if err != nil {
return err
@@ -105,5 +106,5 @@ func (hs *HTTPServer) Run() error {
}
func (hs *HTTPServer) Output(payload any) error {
return fmt.Errorf("net.http.server output is not implemented")
return errors.New("http.server output is not implemented")
}

View File

@@ -2,7 +2,7 @@ package module
import (
"context"
"fmt"
"errors"
"log/slog"
"time"
@@ -16,6 +16,7 @@ type Interval struct {
ctx context.Context
router route.RouteIO
ticker *time.Ticker
logger *slog.Logger
}
func init() {
@@ -26,16 +27,16 @@ func init() {
duration, ok := params["duration"]
if !ok {
return nil, fmt.Errorf("gen.interval requires a duration parameter")
return nil, errors.New("gen.interval requires a duration parameter")
}
durationNum, ok := duration.(float64)
if !ok {
return nil, fmt.Errorf("gen.interval duration must be number")
return nil, errors.New("gen.interval duration must be number")
}
return &Interval{Duration: uint32(durationNum), config: config, ctx: ctx, router: router}, nil
return &Interval{Duration: uint32(durationNum), config: config, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -56,11 +57,11 @@ func (i *Interval) Run() error {
for {
select {
case <-i.ctx.Done():
slog.Debug("router context done in module", "id", i.config.Id)
i.logger.Debug("router context done in module")
return nil
case <-ticker.C:
if i.router != nil {
i.router.HandleInput(i.config.Id, time.Now())
i.router.HandleInput(i.Id(), time.Now())
}
}
}

View File

@@ -1,118 +0,0 @@
//go:build cgo
package module
import (
"context"
"fmt"
"log/slog"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
"gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)
type MIDIClient struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
InputPort string
OutputPort string
SendFunc func(midi.Message) error
}
func init() {
RegisterModule(ModuleRegistration{
//TODO(jwetzell): find a better namespace than "misc"
Type: "misc.midi.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params
input, ok := params["input"]
if !ok {
return nil, fmt.Errorf("misc.midi.client requires a input parameter")
}
inputString, ok := input.(string)
if !ok {
return nil, fmt.Errorf("misc.midi.client input must be a string")
}
output, ok := params["output"]
if !ok {
return nil, fmt.Errorf("misc.midi.client requires a output parameter")
}
outputString, ok := output.(string)
if !ok {
return nil, fmt.Errorf("misc.midi.client output must be a string")
}
return &MIDIClient{config: config, InputPort: inputString, OutputPort: outputString, ctx: ctx, router: router}, nil
},
})
}
func (mc *MIDIClient) Id() string {
return mc.config.Id
}
func (mc *MIDIClient) Type() string {
return mc.config.Type
}
func (mc *MIDIClient) Run() error {
defer midi.CloseDriver()
in, err := midi.FindInPort(mc.InputPort)
if err != nil {
return fmt.Errorf("misc.midi.client can't find input port: %s", mc.InputPort)
}
stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) {
if mc.router != nil {
mc.router.HandleInput(mc.Id(), msg)
}
}, midi.UseSysEx())
if err != nil {
return err
}
defer stop()
out, err := midi.FindOutPort(mc.OutputPort)
if err != nil {
return fmt.Errorf("misc.midi.client can't find output port: %s", mc.OutputPort)
}
send, err := midi.SendTo(out)
if err != nil {
return err
}
mc.SendFunc = send
<-mc.ctx.Done()
slog.Debug("router context done in module", "id", mc.config.Id)
return nil
}
func (mc *MIDIClient) Output(payload any) error {
if mc.SendFunc == nil {
return fmt.Errorf("misc.midi.client output is not setup")
}
payloadMessage, ok := payload.(midi.Message)
if !ok {
return fmt.Errorf("misc.midi.client can only ouptut midi.Message")
}
return mc.SendFunc(payloadMessage)
}

View File

@@ -0,0 +1,83 @@
//go:build cgo
package module
import (
"context"
"errors"
"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
logger *slog.Logger
}
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, errors.New("midi.input requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, errors.New("midi.input port must be a string")
}
return &MIDIInput{config: config, Port: portString, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
func (mi *MIDIInput) Id() string {
return mi.config.Id
}
func (mi *MIDIInput) Type() string {
return mi.config.Type
}
func (mi *MIDIInput) Run() error {
defer midi.CloseDriver()
in, err := midi.FindInPort(mi.Port)
if err != nil {
return fmt.Errorf("midi.input can't find input port: %s", mi.Port)
}
stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) {
if mi.router != nil {
mi.router.HandleInput(mi.Id(), msg)
}
}, midi.UseSysEx())
if err != nil {
return err
}
defer stop()
<-mi.ctx.Done()
mi.logger.Debug("router context done in module")
return nil
}
func (mi *MIDIInput) Output(payload any) error {
return errors.New("midi.input output is not implemented")
}

View File

@@ -0,0 +1,90 @@
//go:build cgo
package module
import (
"context"
"errors"
"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
logger *slog.Logger
}
func init() {
RegisterModule(ModuleRegistration{
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, errors.New("midi.output requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, errors.New("midi.output port must be a string")
}
return &MIDIOutput{config: config, Port: portString, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
func (mo *MIDIOutput) Id() string {
return mo.config.Id
}
func (mo *MIDIOutput) Type() string {
return mo.config.Type
}
func (mo *MIDIOutput) Run() error {
defer midi.CloseDriver()
out, err := midi.FindOutPort(mo.Port)
if err != nil {
return fmt.Errorf("midi.output can't find output port: %s", mo.Port)
}
send, err := midi.SendTo(out)
if err != nil {
return err
}
mo.SendFunc = send
<-mo.ctx.Done()
mo.logger.Debug("router context done in module")
return nil
}
func (mo *MIDIOutput) Output(payload any) error {
if mo.SendFunc == nil {
return errors.New("midi.output output is not setup")
}
payloadMessage, ok := payload.(midi.Message)
if !ok {
return errors.New("midi.output can only ouptut midi.Message")
}
return mo.SendFunc(payloadMessage)
}

View File

@@ -2,12 +2,12 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
@@ -19,50 +19,51 @@ type MQTTClient struct {
ClientID string
Topic string
client mqtt.Client
logger *slog.Logger
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.mqtt.client",
Type: "mqtt.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params
broker, ok := params["broker"]
if !ok {
return nil, fmt.Errorf("net.mqtt.client requires a broker parameter")
return nil, errors.New("mqtt.client requires a broker parameter")
}
brokerString, ok := broker.(string)
if !ok {
return nil, fmt.Errorf("net.mqtt.client broker must be string")
return nil, errors.New("mqtt.client broker must be string")
}
topic, ok := params["topic"]
if !ok {
return nil, fmt.Errorf("net.mqtt.client requires a topic parameter")
return nil, errors.New("mqtt.client requires a topic parameter")
}
topicString, ok := topic.(string)
if !ok {
return nil, fmt.Errorf("net.mqtt.client topic must be string")
return nil, errors.New("mqtt.client topic must be string")
}
clientId, ok := params["clientId"]
if !ok {
return nil, fmt.Errorf("net.mqtt.client requires a clientId parameter")
return nil, errors.New("mqtt.client requires a clientId parameter")
}
clientIdString, ok := clientId.(string)
if !ok {
return nil, fmt.Errorf("net.mqtt.client clientId must be string")
return nil, errors.New("mqtt.client clientId must be string")
}
return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, ctx: ctx, router: router}, nil
return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -84,7 +85,7 @@ func (mc *MQTTClient) Run() error {
opts.OnConnect = func(c mqtt.Client) {
token := mc.client.Subscribe(mc.Topic, 1, func(c mqtt.Client, m mqtt.Message) {
mc.router.HandleInput(mc.config.Id, m)
mc.router.HandleInput(mc.Id(), m)
})
token.Wait()
}
@@ -100,26 +101,28 @@ func (mc *MQTTClient) Run() error {
}
<-mc.ctx.Done()
slog.Debug("router context done in module", "id", mc.config.Id)
mc.logger.Debug("router context done in module")
return nil
}
func (mc *MQTTClient) Output(payload any) error {
payloadMessage, ok := payload.(processor.MQTTMessage)
payloadMessage, ok := payload.(mqtt.Message)
fmt.Printf("payload type: %T\n", payload)
if !ok {
return fmt.Errorf("net.mqtt.client is only able to output a MQTTMessage")
return errors.New("mqtt.client is only able to output a MQTTMessage")
}
if mc.client == nil {
return fmt.Errorf("net.mqtt.client client is not setup")
return errors.New("mqtt.client client is not setup")
}
if !mc.client.IsConnected() {
return fmt.Errorf("net.mqtt.client is not connected")
return errors.New("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()

View File

@@ -2,7 +2,7 @@ package module
import (
"context"
"fmt"
"errors"
"log/slog"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -18,38 +18,39 @@ type NATSClient struct {
URL string
Subject string
client *nats.Conn
logger *slog.Logger
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.nats.client",
Type: "nats.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params
url, ok := params["url"]
if !ok {
return nil, fmt.Errorf("net.nats.client requires a url parameter")
return nil, errors.New("nats.client requires a url parameter")
}
urlString, ok := url.(string)
if !ok {
return nil, fmt.Errorf("net.nats.client url must be string")
return nil, errors.New("nats.client url must be string")
}
subject, ok := params["subject"]
if !ok {
return nil, fmt.Errorf("net.nats.client requires a subject parameter")
return nil, errors.New("nats.client requires a subject parameter")
}
subjectString, ok := subject.(string)
if !ok {
return nil, fmt.Errorf("net.nats.client subject must be string")
return nil, errors.New("nats.client subject must be string")
}
return &NATSClient{config: config, URL: urlString, Subject: subjectString, ctx: ctx, router: router}, nil
return &NATSClient{config: config, URL: urlString, Subject: subjectString, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -76,7 +77,7 @@ func (nc *NATSClient) Run() error {
sub, err := nc.client.Subscribe(nc.Subject, func(msg *nats.Msg) {
if nc.router != nil {
nc.router.HandleInput(nc.config.Id, msg)
nc.router.HandleInput(nc.Id(), msg)
}
})
@@ -87,7 +88,7 @@ func (nc *NATSClient) Run() error {
defer sub.Unsubscribe()
<-nc.ctx.Done()
slog.Debug("router context done in module", "id", nc.config.Id)
nc.logger.Debug("router context done in module")
return nil
}
@@ -96,15 +97,15 @@ func (nc *NATSClient) Output(payload any) error {
payloadMessage, ok := payload.(processor.NATSMessage)
if !ok {
return fmt.Errorf("net.nats.client is only able to output NATSMessage")
return errors.New("nats.client is only able to output NATSMessage")
}
if nc.client == nil {
return fmt.Errorf("net.nats.client client is not setup")
return errors.New("nats.client client is not setup")
}
if !nc.client.IsConnected() {
return fmt.Errorf("net.nats.client is not connected")
return errors.New("nats.client is not connected")
}
err := nc.client.Publish(payloadMessage.Subject, payloadMessage.Payload)

View File

@@ -18,14 +18,15 @@ type PSNClient struct {
ctx context.Context
router route.RouteIO
decoder *psn.Decoder
logger *slog.Logger
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.psn.client",
Type: "psn.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
return &PSNClient{config: config, decoder: psn.NewDecoder(), ctx: ctx, router: router}, nil
return &PSNClient{config: config, decoder: psn.NewDecoder(), ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -58,7 +59,7 @@ func (pc *PSNClient) Run() error {
select {
case <-pc.ctx.Done():
// TODO(jwetzell): cleanup?
slog.Debug("router context done in module", "id", pc.config.Id)
pc.logger.Debug("router context done in module")
return nil
default:
pc.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -76,15 +77,15 @@ func (pc *PSNClient) Run() error {
message := buffer[:numBytes]
err := pc.decoder.Decode(message)
if err != nil {
slog.Error("net.psn.client problem decoding psn traffic", "id", pc.config.Id, "error", err)
pc.logger.Error("psn.client problem decoding psn traffic", "error", err)
}
if pc.router != nil {
for _, tracker := range pc.decoder.Trackers {
pc.router.HandleInput(pc.config.Id, tracker)
pc.router.HandleInput(pc.Id(), tracker)
}
} else {
slog.Error("net.psn.client has no router", "id", pc.config.Id)
pc.logger.Error("psn.client has no router")
}
}
}
@@ -92,5 +93,5 @@ func (pc *PSNClient) Run() 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

@@ -4,6 +4,7 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
@@ -22,24 +23,24 @@ type SerialClient struct {
Framer framer.Framer
Mode *serial.Mode
port serial.Port
logger *slog.Logger
}
func init() {
RegisterModule(ModuleRegistration{
//TODO(jwetzell): find a better namespace than "misc"
Type: "misc.serial.client",
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("misc.serial.client requires a port parameter")
return nil, errors.New("serial.client requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, fmt.Errorf("misc.serial.client port must be a string")
return nil, errors.New("serial.client port must be a string")
}
framingMethod := "RAW"
@@ -50,7 +51,7 @@ func init() {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, fmt.Errorf("misc.serial.client framing method must be a string")
return nil, errors.New("serial.client framing method must be a string")
}
framingMethod = framingMethodString
}
@@ -63,94 +64,94 @@ func init() {
buadRate, ok := params["baudRate"]
if !ok {
return nil, fmt.Errorf("misc.serial.client requires a baudRate parameter")
return nil, errors.New("serial.client requires a baudRate parameter")
}
baudRateNum, ok := buadRate.(float64)
if !ok {
return nil, fmt.Errorf("misc.serial.client baudRate must be a number")
return nil, errors.New("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
return &SerialClient{config: config, Port: portString, Framer: framer, Mode: &mode, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
func (mc *SerialClient) Id() string {
return mc.config.Id
func (sc *SerialClient) Id() string {
return sc.config.Id
}
func (mc *SerialClient) Type() string {
return mc.config.Type
func (sc *SerialClient) Type() string {
return sc.config.Type
}
func (mc *SerialClient) SetupPort() error {
func (sc *SerialClient) SetupPort() error {
port, err := serial.Open(mc.Port, mc.Mode)
port, err := serial.Open(sc.Port, sc.Mode)
if err != nil {
return fmt.Errorf("misc.serial.client can't open input port: %s", mc.Port)
return fmt.Errorf("serial.client can't open input port: %s", sc.Port)
}
mc.port = port
sc.port = port
return nil
}
func (mc *SerialClient) Run() error {
func (sc *SerialClient) Run() error {
// TODO(jwetzell): shutdown with router.Context properly
go func() {
<-mc.ctx.Done()
slog.Debug("router context done in module", "id", mc.config.Id)
if mc.port != nil {
mc.port.Close()
<-sc.ctx.Done()
sc.logger.Debug("router context done in module")
if sc.port != nil {
sc.port.Close()
}
}()
for {
err := mc.SetupPort()
err := sc.SetupPort()
if err != nil {
if mc.ctx.Err() != nil {
slog.Debug("router context done in module", "id", mc.config.Id)
if sc.ctx.Err() != nil {
sc.logger.Debug("router context done in module")
return nil
}
slog.Error("misc.serial.client", "id", mc.config.Id, "error", err.Error())
sc.logger.Error("serial.client", "error", err.Error())
time.Sleep(time.Second * 2)
continue
}
buffer := make([]byte, 1024)
select {
case <-mc.ctx.Done():
slog.Debug("router context done in module", "id", mc.config.Id)
case <-sc.ctx.Done():
sc.logger.Debug("router context done in module")
return nil
default:
READ:
for {
select {
case <-mc.ctx.Done():
slog.Debug("router context done in module", "id", mc.config.Id)
case <-sc.ctx.Done():
sc.logger.Debug("router context done in module")
return nil
default:
byteCount, err := mc.port.Read(buffer)
byteCount, err := sc.port.Read(buffer)
if err != nil {
mc.Framer.Clear()
sc.Framer.Clear()
break READ
}
if mc.Framer != nil {
if sc.Framer != nil {
if byteCount > 0 {
messages := mc.Framer.Decode(buffer[0:byteCount])
messages := sc.Framer.Decode(buffer[0:byteCount])
for _, message := range messages {
if mc.router != nil {
mc.router.HandleInput(mc.config.Id, message)
if sc.router != nil {
sc.router.HandleInput(sc.Id(), message)
} else {
slog.Error("misc.serial.client has no router", "id", mc.config.Id)
sc.logger.Error("serial.client has no router")
}
}
}
@@ -161,14 +162,14 @@ func (mc *SerialClient) Run() error {
}
}
func (mc *SerialClient) Output(payload any) error {
func (sc *SerialClient) Output(payload any) error {
payloadBytes, ok := payload.([]byte)
if !ok {
return fmt.Errorf("misc.serial.client can only ouptut bytes")
return errors.New("serial.client can only ouptut bytes")
}
_, err := mc.port.Write(mc.Framer.Encode(payloadBytes))
_, err := sc.port.Write(sc.Framer.Encode(payloadBytes))
return err
}

View File

@@ -0,0 +1,187 @@
package module
import (
"context"
"errors"
"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
logger *slog.Logger
}
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, errors.New("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, errors.New("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, errors.New("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, errors.New("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, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
func (scs *SIPCallServer) Id() string {
return scs.config.Id
}
func (scs *SIPCallServer) Type() string {
return scs.config.Type
}
func (scs *SIPCallServer) Run() error {
diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
ua, _ := sipgo.NewUA(
sipgo.WithUserAgent(scs.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: scs.Transport,
BindHost: scs.IP,
BindPort: scs.Port,
},
))
go func() {
dg.Serve(scs.ctx, func(inDialog *diago.DialogServerSession) {
scs.HandleCall(inDialog)
})
}()
scs.dg = dg
<-scs.ctx.Done()
scs.logger.Debug("router context done in module")
return nil
}
func (scs *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) {
inDialog.Trying()
inDialog.Ringing()
inDialog.Answer()
scs.router.HandleInput(scs.Id(), SIPCallMessage{
To: inDialog.ToUser(),
})
<-inDialog.Context().Done()
}
func (scs *SIPCallServer) Output(payload any) error {
payloadMsg, ok := payload.(string)
if !ok {
return errors.New("sip.call.server output payload must be of type string")
}
if scs.dg == nil {
return errors.New("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: %s", err)
}
outDialog, err := scs.dg.NewDialog(uri, diago.NewDialogOptions{
Transport: scs.Transport,
})
if err != nil {
return fmt.Errorf("sip.call.server failed to create new dialog: %s", err)
}
err = outDialog.Invite(scs.ctx, diago.InviteClientOptions{})
if err != nil {
return fmt.Errorf("sip.call.server failed to send invite: %s", err)
}
err = outDialog.Ack(scs.ctx)
if err != nil {
return fmt.Errorf("sip.call.server failed to send ack: %s", err)
}
// TODO(jwetzell): make this configurable
// NOTE(jwetzell): wait 5 seconds before hanging up the call
time.Sleep(5 * time.Second)
err = outDialog.Hangup(scs.ctx)
if err != nil {
return fmt.Errorf("sip.call.server failed to hangup call: %s", err)
}
return nil
}

View File

@@ -0,0 +1,164 @@
package module
import (
"context"
"errors"
"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
logger *slog.Logger
}
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, errors.New("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, errors.New("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, errors.New("sip.dtmf.server transport must be a string")
}
transportString = specificTransportString
}
separator, ok := params["separator"]
if !ok {
return nil, errors.New("sip.dtmf.server requires a separator parameter")
}
separatorString, ok := separator.(string)
if !ok {
return nil, errors.New("sip.dtmf.server separator must be a string")
}
if len(separatorString) != 1 {
return nil, errors.New("sip.dtmf.server separator must be a single character")
}
if !strings.ContainsRune("0123456789*#ABCD", rune(separatorString[0])) {
return nil, errors.New("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, logger: slog.Default().With("component", "module", "id", config.Id)}, 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()
sds.logger.Debug("router context done in module")
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 errors.New("sip.dtmf.server output is not implemented")
}

View File

@@ -2,6 +2,7 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
@@ -19,6 +20,7 @@ type TCPClient struct {
ctx context.Context
router route.RouteIO
Addr *net.TCPAddr
logger *slog.Logger
}
func init() {
@@ -29,24 +31,24 @@ func init() {
host, ok := params["host"]
if !ok {
return nil, fmt.Errorf("net.tcp.client requires a host parameter")
return nil, errors.New("net.tcp.client requires a host parameter")
}
hostString, ok := host.(string)
if !ok {
return nil, fmt.Errorf("net.tcp.client host must be string")
return nil, errors.New("net.tcp.client host must be string")
}
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.tcp.client requires a port parameter")
return nil, errors.New("net.tcp.client requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("net.tcp.client port must be a number")
return nil, errors.New("net.tcp.client port must be a number")
}
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
@@ -62,7 +64,7 @@ func init() {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, fmt.Errorf("misc.serial.client framing method must be a string")
return nil, errors.New("misc.serial.client framing method must be a string")
}
framingMethod = framingMethodString
}
@@ -73,7 +75,7 @@ func init() {
return nil, err
}
return &TCPClient{framer: framer, Addr: addr, config: config, ctx: ctx, router: router}, nil
return &TCPClient{framer: framer, Addr: addr, config: config, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -91,7 +93,7 @@ func (tc *TCPClient) Run() error {
// TODO(jwetzell): shutdown with router.Context properly
go func() {
<-tc.ctx.Done()
slog.Debug("router context done in module", "id", tc.config.Id)
tc.logger.Debug("router context done in module")
if tc.conn != nil {
tc.conn.Close()
}
@@ -101,10 +103,10 @@ func (tc *TCPClient) Run() error {
err := tc.SetupConn()
if err != nil {
if tc.ctx.Err() != nil {
slog.Debug("router context done in module", "id", tc.config.Id)
tc.logger.Debug("router context done in module")
return nil
}
slog.Error("net.tcp.client", "id", tc.config.Id, "error", err.Error())
tc.logger.Error("net.tcp.client", "error", err.Error())
time.Sleep(time.Second * 2)
continue
}
@@ -112,14 +114,14 @@ func (tc *TCPClient) Run() error {
buffer := make([]byte, 1024)
select {
case <-tc.ctx.Done():
slog.Debug("router context done in module", "id", tc.config.Id)
tc.logger.Debug("router context done in module")
return nil
default:
READ:
for {
select {
case <-tc.ctx.Done():
slog.Debug("router context done in module", "id", tc.config.Id)
tc.logger.Debug("router context done in module")
return nil
default:
byteCount, err := tc.conn.Read(buffer)
@@ -134,9 +136,9 @@ func (tc *TCPClient) Run() error {
messages := tc.framer.Decode(buffer[0:byteCount])
for _, message := range messages {
if tc.router != nil {
tc.router.HandleInput(tc.config.Id, message)
tc.router.HandleInput(tc.Id(), message)
} else {
slog.Error("net.tcp.client has no router", "id", tc.config.Id)
tc.logger.Error("net.tcp.client has no router")
}
}
}
@@ -163,7 +165,7 @@ func (tc *TCPClient) Output(payload any) error {
}
payloadBytes, ok := payload.([]byte)
if !ok {
return fmt.Errorf("net.tcp.client is only able to output bytes")
return errors.New("net.tcp.client is only able to output bytes")
}
_, err := tc.conn.Write(tc.framer.Encode(payloadBytes))
return err

View File

@@ -26,6 +26,7 @@ type TCPServer struct {
wg sync.WaitGroup
connections []*net.TCPConn
connectionsMu sync.RWMutex
logger *slog.Logger
}
func init() {
@@ -35,13 +36,13 @@ func init() {
params := config.Params
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.tcp.server requires a port parameter")
return nil, errors.New("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")
return nil, errors.New("net.tcp.server port must be a number")
}
framingMethod := "RAW"
@@ -52,7 +53,7 @@ func init() {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, fmt.Errorf("misc.serial.client framing method must be a string")
return nil, errors.New("misc.serial.client framing method must be a string")
}
framingMethod = framingMethodString
}
@@ -71,7 +72,7 @@ func init() {
specificIpString, ok := ip.(string)
if !ok {
return nil, fmt.Errorf("net.tcp.server ip must be a string")
return nil, errors.New("net.tcp.server ip must be a string")
}
ipString = specificIpString
}
@@ -81,7 +82,7 @@ func init() {
return nil, err
}
return &TCPServer{Framer: framer, Addr: addr, config: config, quit: make(chan interface{}), ctx: ctx, router: router}, nil
return &TCPServer{Framer: framer, Addr: addr, config: config, quit: make(chan interface{}), ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -98,7 +99,7 @@ func (ts *TCPServer) handleClient(client *net.TCPConn) {
ts.connectionsMu.Lock()
ts.connections = append(ts.connections, client)
ts.connectionsMu.Unlock()
slog.Debug("net.tcp.server connection accepted", "id", ts.config.Id, "remoteAddr", client.RemoteAddr().String())
ts.logger.Debug("net.tcp.server connection accepted", "remoteAddr", client.RemoteAddr().String())
defer client.Close()
buffer := make([]byte, 1024)
@@ -125,7 +126,7 @@ ClientRead:
break
}
}
slog.Debug("net.tcp.server connection reset", "id", ts.config.Id, "remoteAddr", client.RemoteAddr().String())
ts.logger.Debug("net.tcp.server connection reset", "remoteAddr", client.RemoteAddr().String())
ts.connectionsMu.Unlock()
}
}
@@ -138,7 +139,7 @@ ClientRead:
break
}
}
slog.Debug("net.tcp.server stream ended", "id", ts.config.Id, "remoteAddr", client.RemoteAddr().String())
ts.logger.Debug("net.tcp.server stream ended", "remoteAddr", client.RemoteAddr().String())
ts.connectionsMu.Unlock()
}
return
@@ -148,9 +149,9 @@ ClientRead:
messages := ts.Framer.Decode(buffer[0:byteCount])
for _, message := range messages {
if ts.router != nil {
ts.router.HandleInput(ts.config.Id, message)
ts.router.HandleInput(ts.Id(), message)
} else {
slog.Error("net.tcp.server has no router", "id", ts.config.Id)
ts.logger.Error("net.tcp.server has no router")
}
}
}
@@ -170,7 +171,7 @@ func (ts *TCPServer) Run() error {
<-ts.ctx.Done()
close(ts.quit)
listener.Close()
slog.Debug("router context done in module", "id", ts.config.Id)
ts.logger.Debug("router context done in module")
}()
AcceptLoop:
@@ -181,7 +182,7 @@ AcceptLoop:
case <-ts.quit:
break AcceptLoop
default:
slog.Debug("net.tcp.server problem with listener", "error", err)
ts.logger.Debug("net.tcp.server problem with listener", "error", err)
}
} else {
ts.wg.Add(1)
@@ -200,7 +201,7 @@ 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")
return errors.New("net.tcp.server is only able to output bytes")
}
ts.connectionsMu.Lock()
errorString := ""
@@ -216,5 +217,5 @@ func (ts *TCPServer) Output(payload any) error {
if errorString == "" {
return nil
}
return fmt.Errorf("%s", errorString)
return fmt.Errorf("net.tcp.server error during output: %s", errorString)
}

View File

@@ -2,7 +2,7 @@ package module
import (
"context"
"fmt"
"errors"
"log/slog"
"time"
@@ -16,6 +16,7 @@ type Timer struct {
ctx context.Context
router route.RouteIO
timer *time.Timer
logger *slog.Logger
}
func init() {
@@ -26,16 +27,16 @@ func init() {
duration, ok := params["duration"]
if !ok {
return nil, fmt.Errorf("gen.timer requires a duration parameter")
return nil, errors.New("gen.timer requires a duration parameter")
}
durationNum, ok := duration.(float64)
if !ok {
return nil, fmt.Errorf("gen.timer duration must be a number")
return nil, errors.New("gen.timer duration must be a number")
}
return &Timer{Duration: uint32(durationNum), config: config, ctx: ctx, router: router}, nil
return &Timer{Duration: uint32(durationNum), config: config, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -55,11 +56,11 @@ func (t *Timer) Run() error {
select {
case <-t.ctx.Done():
t.timer.Stop()
slog.Debug("router context done in module", "id", t.config.Id)
t.logger.Debug("router context done in module")
return nil
case time := <-t.timer.C:
if t.router != nil {
t.router.HandleInput(t.config.Id, time)
t.router.HandleInput(t.Id(), time)
}
}
}

View File

@@ -2,6 +2,7 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
@@ -17,6 +18,7 @@ type UDPClient struct {
conn *net.UDPConn
ctx context.Context
router route.RouteIO
logger *slog.Logger
}
func init() {
@@ -27,24 +29,24 @@ func init() {
host, ok := params["host"]
if !ok {
return nil, fmt.Errorf("net.udp.client requires a host parameter")
return nil, errors.New("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")
return nil, errors.New("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")
return nil, errors.New("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 nil, errors.New("net.udp.client port must be a number")
}
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
@@ -52,7 +54,7 @@ func init() {
return nil, err
}
return &UDPClient{Addr: addr, config: config, ctx: ctx, router: router}, nil
return &UDPClient{Addr: addr, config: config, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -79,7 +81,7 @@ func (uc *UDPClient) Run() error {
}
<-uc.ctx.Done()
slog.Debug("router context done in module", "id", uc.config.Id)
uc.logger.Debug("router context done in module")
if uc.conn != nil {
uc.conn.Close()
}
@@ -90,7 +92,7 @@ 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")
return errors.New("net.udp.client is only able to output bytes")
}
if uc.conn != nil {
_, err := uc.conn.Write(payloadBytes)
@@ -99,7 +101,7 @@ func (uc *UDPClient) Output(payload any) error {
return err
}
} else {
return fmt.Errorf("net.udp.client client is not setup")
return errors.New("net.udp.client client is not setup")
}
return nil
}

View File

@@ -2,6 +2,7 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
@@ -17,6 +18,7 @@ type UDPMulticast struct {
ctx context.Context
router route.RouteIO
Addr *net.UDPAddr
logger *slog.Logger
}
func init() {
@@ -27,31 +29,31 @@ func init() {
ip, ok := params["ip"]
if !ok {
return nil, fmt.Errorf("net.udp.client requires am ip parameter")
return nil, errors.New("net.udp.multicast requires an ip parameter")
}
ipString, ok := ip.(string)
if !ok {
return nil, fmt.Errorf("net.udp.client ip must be a string")
return nil, errors.New("net.udp.multicast ip must be a string")
}
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.udp.client requires a port parameter")
return nil, errors.New("net.udp.multicast requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("net.udp.client port must be a number")
return nil, errors.New("net.udp.multicast 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
return &UDPMulticast{config: config, Addr: addr, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -79,7 +81,7 @@ func (um *UDPMulticast) Run() error {
select {
case <-um.ctx.Done():
// TODO(jwetzell): cleanup?
slog.Debug("router context done in module", "id", um.config.Id)
um.logger.Debug("router context done in module")
return nil
default:
um.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -97,9 +99,9 @@ func (um *UDPMulticast) Run() error {
message := buffer[:numBytes]
if um.router != nil {
um.router.HandleInput(um.config.Id, message)
um.router.HandleInput(um.Id(), message)
} else {
slog.Error("net.udp.multicast has no router", "id", um.config.Id)
um.logger.Error("net.udp.multicast has no router")
}
}
}
@@ -110,11 +112,11 @@ func (um *UDPMulticast) Output(payload any) error {
payloadBytes, ok := payload.([]byte)
if !ok {
return fmt.Errorf("net.udp.multicast can only output bytes")
return errors.New("net.udp.multicast can only output bytes")
}
if um.conn == nil {
return fmt.Errorf("net.udp.multicast connection is not setup")
return errors.New("net.udp.multicast connection is not setup")
}
_, err := um.conn.Write(payloadBytes)

View File

@@ -2,6 +2,7 @@ package module
import (
"context"
"errors"
"fmt"
"log"
"log/slog"
@@ -14,9 +15,11 @@ import (
type UDPServer struct {
Addr *net.UDPAddr
BufferSize int
config config.ModuleConfig
ctx context.Context
router route.RouteIO
logger *slog.Logger
}
func init() {
@@ -26,13 +29,13 @@ func init() {
params := config.Params
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.udp.server requires a port parameter")
return nil, errors.New("net.udp.server requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("net.udp.server port must be a number")
return nil, errors.New("net.udp.server port must be a number")
}
ipString := "0.0.0.0"
@@ -43,7 +46,7 @@ func init() {
specificIpString, ok := ip.(string)
if !ok {
return nil, fmt.Errorf("net.udp.server ip must be a string")
return nil, errors.New("net.udp.server ip must be a string")
}
ipString = specificIpString
}
@@ -53,7 +56,19 @@ func init() {
log.Fatalf("error resolving UDP address: %v", err)
}
return &UDPServer{Addr: addr, config: config, ctx: ctx, router: router}, nil
bufferSizeNum := 2048
bufferSize, ok := params["bufferSize"]
if ok {
bufferSizeFloat, ok := bufferSize.(float64)
if !ok {
return nil, errors.New("net.udp.server bufferSize must be a number")
}
bufferSizeNum = int(bufferSizeFloat)
}
return &UDPServer{Addr: addr, BufferSize: bufferSizeNum, config: config, ctx: ctx, router: router, logger: slog.Default().With("component", "module", "id", config.Id)}, nil
},
})
}
@@ -75,13 +90,12 @@ func (us *UDPServer) Run() error {
defer listener.Close()
// TODO(jwetzell): make buffer size configurable
buffer := make([]byte, 65535)
buffer := make([]byte, us.BufferSize)
for {
select {
case <-us.ctx.Done():
// TODO(jwetzell): cleanup?
slog.Debug("router context done in module", "id", us.config.Id)
us.logger.Debug("router context done in module")
return nil
default:
listener.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -96,9 +110,9 @@ func (us *UDPServer) Run() error {
}
message := buffer[:numBytes]
if us.router != nil {
us.router.HandleInput(us.config.Id, message)
us.router.HandleInput(us.Id(), message)
} else {
slog.Error("net.udp.server has no router", "id", us.config.Id)
us.logger.Error("net.udp.server has no router")
}
}
}
@@ -106,5 +120,5 @@ func (us *UDPServer) Run() error {
}
func (us *UDPServer) Output(payload any) error {
return fmt.Errorf("net.udp.server output is not implemented")
return errors.New("net.udp.server output is not implemented")
}

View File

@@ -10,10 +10,11 @@ import (
type DebugLog struct {
config config.ProcessorConfig
logger *slog.Logger
}
func (dl *DebugLog) Process(ctx context.Context, payload any) (any, error) {
slog.Debug("debug.log", "payload", payload, "payloadType", fmt.Sprintf("%T", payload))
dl.logger.Debug("debug.log", "payload", payload, "payloadType", fmt.Sprintf("%T", payload))
return payload, nil
}
@@ -25,7 +26,7 @@ func init() {
RegisterProcessor(ProcessorRegistration{
Type: "debug.log",
New: func(config config.ProcessorConfig) (Processor, error) {
return &DebugLog{config: config}, nil
return &DebugLog{config: config, logger: slog.Default().With("component", "processor")}, nil
},
})
}

View File

@@ -2,13 +2,14 @@ package processor
import (
"context"
"fmt"
"errors"
"strconv"
"github.com/jwetzell/showbridge-go/internal/config"
)
type FloatParse struct {
BitSize int
config config.ProcessorConfig
}
@@ -16,11 +17,10 @@ func (fp *FloatParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string)
if !ok {
return nil, fmt.Errorf("float.parse processor only accepts a string")
return nil, errors.New("float.parse processor only accepts a string")
}
// TODO(jwetzell): make bitSize configurable
payloadFloat, err := strconv.ParseFloat(payloadString, 64)
payloadFloat, err := strconv.ParseFloat(payloadString, fp.BitSize)
if err != nil {
return nil, err
}
@@ -35,7 +35,19 @@ func init() {
RegisterProcessor(ProcessorRegistration{
Type: "float.parse",
New: func(config config.ProcessorConfig) (Processor, error) {
return &FloatParse{config: config}, nil
params := config.Params
bitSizeNum := 64
bitSize, ok := params["bitSize"]
if ok {
bitSizeFloat, ok := bitSize.(float64)
if !ok {
return nil, errors.New("float.parse bitSize must be a number")
}
bitSizeNum = int(bitSizeFloat)
}
return &FloatParse{config: config, BitSize: bitSizeNum}, nil
},
})
}

View File

@@ -3,7 +3,7 @@ package processor
import (
"bytes"
"context"
"fmt"
"errors"
"strconv"
"text/template"
@@ -190,13 +190,13 @@ func init() {
id, ok := params["id"]
if !ok {
return nil, fmt.Errorf("freed.create requires an id parameter")
return nil, errors.New("freed.create requires an id parameter")
}
idString, ok := id.(string)
if !ok {
return nil, fmt.Errorf("freed.create id must be a string")
return nil, errors.New("freed.create id must be a string")
}
idTemplate, err := template.New("id").Parse(idString)
@@ -208,13 +208,13 @@ func init() {
pan, ok := params["pan"]
if !ok {
return nil, fmt.Errorf("freed.create requires an pan parameter")
return nil, errors.New("freed.create requires a pan parameter")
}
panString, ok := pan.(string)
if !ok {
return nil, fmt.Errorf("freed.create pan must be a string")
return nil, errors.New("freed.create pan must be a string")
}
panTemplate, err := template.New("pan").Parse(panString)
@@ -222,13 +222,13 @@ func init() {
tilt, ok := params["tilt"]
if !ok {
return nil, fmt.Errorf("freed.create requires an tilt parameter")
return nil, errors.New("freed.create requires a tilt parameter")
}
tiltString, ok := tilt.(string)
if !ok {
return nil, fmt.Errorf("freed.create tilt must be a string")
return nil, errors.New("freed.create tilt must be a string")
}
tiltTemplate, err := template.New("tilt").Parse(tiltString)
@@ -236,13 +236,13 @@ func init() {
roll, ok := params["roll"]
if !ok {
return nil, fmt.Errorf("freed.create requires an roll parameter")
return nil, errors.New("freed.create requires a roll parameter")
}
rollString, ok := roll.(string)
if !ok {
return nil, fmt.Errorf("freed.create roll must be a string")
return nil, errors.New("freed.create roll must be a string")
}
rollTemplate, err := template.New("roll").Parse(rollString)
@@ -254,13 +254,13 @@ func init() {
posX, ok := params["posX"]
if !ok {
return nil, fmt.Errorf("freed.create requires a posX parameter")
return nil, errors.New("freed.create requires a posX parameter")
}
posXString, ok := posX.(string)
if !ok {
return nil, fmt.Errorf("freed.create posX must be a string")
return nil, errors.New("freed.create posX must be a string")
}
posXTemplate, err := template.New("posX").Parse(posXString)
@@ -272,13 +272,13 @@ func init() {
posY, ok := params["posY"]
if !ok {
return nil, fmt.Errorf("freed.create requires a posY parameter")
return nil, errors.New("freed.create requires a posY parameter")
}
posYString, ok := posY.(string)
if !ok {
return nil, fmt.Errorf("freed.create posY must be a string")
return nil, errors.New("freed.create posY must be a string")
}
posYTemplate, err := template.New("posY").Parse(posYString)
@@ -290,13 +290,13 @@ func init() {
posZ, ok := params["posZ"]
if !ok {
return nil, fmt.Errorf("freed.create requires a posZ parameter")
return nil, errors.New("freed.create requires a posZ parameter")
}
posZString, ok := posZ.(string)
if !ok {
return nil, fmt.Errorf("freed.create posZ must be a string")
return nil, errors.New("freed.create posZ must be a string")
}
posZTemplate, err := template.New("posZ").Parse(posZString)
@@ -308,13 +308,13 @@ func init() {
zoom, ok := params["zoom"]
if !ok {
return nil, fmt.Errorf("freed.create requires an zoom parameter")
return nil, errors.New("freed.create requires a zoom parameter")
}
zoomString, ok := zoom.(string)
if !ok {
return nil, fmt.Errorf("freed.create zoom must be a string")
return nil, errors.New("freed.create zoom must be a string")
}
zoomTemplate, err := template.New("zoom").Parse(zoomString)
@@ -322,13 +322,13 @@ func init() {
focus, ok := params["focus"]
if !ok {
return nil, fmt.Errorf("freed.create requires an focus parameter")
return nil, errors.New("freed.create requires a focus parameter")
}
focusString, ok := focus.(string)
if !ok {
return nil, fmt.Errorf("freed.create focus must be a string")
return nil, errors.New("freed.create focus must be a string")
}
focusTemplate, err := template.New("focus").Parse(focusString)

View File

@@ -2,8 +2,7 @@ package processor
import (
"context"
"fmt"
"log/slog"
"errors"
freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -17,12 +16,12 @@ func (fdd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := payload.([]byte)
if !ok {
return nil, fmt.Errorf("freed.decode processor only accepts a []byte")
return nil, errors.New("freed.decode processor only accepts a []byte")
}
payloadMessage, err := freeD.Decode(payloadBytes)
if err != nil {
slog.Error("error decoding", "err", err)
return nil, err
}
return payloadMessage, nil
}

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -16,7 +16,7 @@ func (fde *FreeDEncode) Process(ctx context.Context, payload any) (any, error) {
payloadPosition, ok := payload.(freeD.FreeDPosition)
if !ok {
return nil, fmt.Errorf("freed.decode processor only accepts a FreeDEncode")
return nil, errors.New("freed.decode processor only accepts a FreeDEncode")
}
payloadBytes := freeD.Encode(payloadPosition)

View File

@@ -3,7 +3,7 @@ package processor
import (
"bytes"
"context"
"fmt"
"errors"
"net/http"
"text/template"
@@ -16,10 +16,10 @@ type HTTPRequestCreate struct {
URL *template.Template
}
func (hre *HTTPRequestCreate) Process(ctx context.Context, payload any) (any, error) {
func (hrc *HTTPRequestCreate) Process(ctx context.Context, payload any) (any, error) {
var urlBuffer bytes.Buffer
err := hre.URL.Execute(&urlBuffer, payload)
err := hrc.URL.Execute(&urlBuffer, payload)
if err != nil {
return nil, err
@@ -28,7 +28,7 @@ func (hre *HTTPRequestCreate) Process(ctx context.Context, payload any) (any, er
urlString := urlBuffer.String()
//TODO(jwetzell): support body
request, err := http.NewRequest(hre.Method, urlString, bytes.NewBuffer([]byte{}))
request, err := http.NewRequest(hrc.Method, urlString, bytes.NewBuffer([]byte{}))
if err != nil {
return nil, err
@@ -37,8 +37,8 @@ func (hre *HTTPRequestCreate) Process(ctx context.Context, payload any) (any, er
return request, nil
}
func (hre *HTTPRequestCreate) Type() string {
return hre.config.Type
func (hrc *HTTPRequestCreate) Type() string {
return hrc.config.Type
}
func init() {
@@ -50,25 +50,25 @@ func init() {
method, ok := params["method"]
if !ok {
return nil, fmt.Errorf("http.request.create requires an method parameter")
return nil, errors.New("http.request.create requires a method parameter")
}
methodString, ok := method.(string)
if !ok {
return nil, fmt.Errorf("http.request.create url must be a string")
return nil, errors.New("http.request.create url must be a string")
}
url, ok := params["url"]
if !ok {
return nil, fmt.Errorf("http.request.create requires an url parameter")
return nil, errors.New("http.request.create requires a url parameter")
}
urlString, ok := url.(string)
if !ok {
return nil, fmt.Errorf("http.request.create url must be a string")
return nil, errors.New("http.request.create url must be a string")
}
urlTemplate, err := template.New("url").Parse(urlString)

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
"io"
"net/http"
@@ -17,7 +17,7 @@ func (hre *HTTPRequestEncode) Process(ctx context.Context, payload any) (any, er
payloadRequest, ok := payload.(*http.Request)
if !ok {
return nil, fmt.Errorf("http.request.encode processor only accepts an http.Request")
return nil, errors.New("http.request.encode processor only accepts an http.Request")
}
bytes, err := io.ReadAll(payloadRequest.Body)

View File

@@ -2,6 +2,7 @@ package processor
import (
"context"
"errors"
"fmt"
"net/http"
"regexp"
@@ -20,7 +21,7 @@ func (hrf *HTTPRequestFilter) Process(ctx context.Context, payload any) (any, er
payloadRequest, ok := payload.(*http.Request)
if !ok {
return nil, fmt.Errorf("http.request.filter can only operate on http.Request payloads")
return nil, errors.New("http.request.filter can only operate on http.Request payloads")
}
if hrf.Method != "" {
@@ -48,13 +49,13 @@ func init() {
path, ok := params["path"]
if !ok {
return nil, fmt.Errorf("http.request.filter requires an path parameter")
return nil, errors.New("http.request.filter requires a path parameter")
}
pathString, ok := path.(string)
if !ok {
return nil, fmt.Errorf("http.request.filter path must be a string")
return nil, errors.New("http.request.filter path must be a string")
}
pathRegexp, err := regexp.Compile(fmt.Sprintf("^%s$", pathString))
@@ -69,7 +70,7 @@ func init() {
methodString, ok := method.(string)
if !ok {
return nil, fmt.Errorf("http.request.filter method must be a string")
return nil, errors.New("http.request.filter method must be a string")
}
return &HTTPRequestFilter{config: config, Path: pathRegexp, Method: methodString}, nil
}

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
"io"
"net/http"
@@ -17,7 +17,7 @@ func (hre *HTTPResponseEncode) Process(ctx context.Context, payload any) (any, e
payloadResponse, ok := payload.(*http.Response)
if !ok {
return nil, fmt.Errorf("http.response.encode processor only accepts an http.Response")
return nil, errors.New("http.response.encode processor only accepts an http.Response")
}
defer payloadResponse.Body.Close()

View File

@@ -2,13 +2,15 @@ package processor
import (
"context"
"fmt"
"errors"
"strconv"
"github.com/jwetzell/showbridge-go/internal/config"
)
type IntParse struct {
Base int
BitSize int
config config.ProcessorConfig
}
@@ -16,11 +18,10 @@ func (ip *IntParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string)
if !ok {
return nil, fmt.Errorf("int.parse processor only accepts a string")
return nil, errors.New("int.parse processor only accepts a string")
}
// TODO(jwetzell): make base and bitSize configurable
payloadInt, err := strconv.ParseInt(payloadString, 10, 64)
payloadInt, err := strconv.ParseInt(payloadString, ip.Base, ip.BitSize)
if err != nil {
return nil, err
}
@@ -35,7 +36,32 @@ func init() {
RegisterProcessor(ProcessorRegistration{
Type: "int.parse",
New: func(config config.ProcessorConfig) (Processor, error) {
return &IntParse{config: config}, nil
params := config.Params
baseNum := 10
base, ok := params["base"]
if ok {
baseFloat, ok := base.(float64)
if !ok {
return nil, errors.New("int.parse base must be a number")
}
baseNum = int(baseFloat)
}
bitSizeNum := 64
bitSize, ok := params["bitSize"]
if ok {
bitSizeFloat, ok := bitSize.(float64)
if !ok {
return nil, errors.New("int.parse bitSize must be a number")
}
bitSizeNum = int(bitSizeFloat)
}
return &IntParse{config: config, Base: baseNum, BitSize: bitSizeNum}, nil
},
})
}

View File

@@ -0,0 +1,376 @@
//go:build cgo
package processor
import (
"bytes"
"context"
"errors"
"fmt"
"strconv"
"text/template"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
// TODO(jwetzell): support using numbers in config file treated as hardcoded values
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, errors.New("midi.message.create NoteOn requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, errors.New("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, errors.New("midi.message.create NoteOn requires a note parameter")
}
noteString, ok := note.(string)
if !ok {
return nil, errors.New("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, errors.New("midi.message.create NoteOn requires a velocity parameter")
}
velocityString, ok := velocity.(string)
if !ok {
return nil, errors.New("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, errors.New("midi.message.create NoteOn requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, errors.New("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, errors.New("midi.message.create NoteOn requires a note parameter")
}
noteString, ok := note.(string)
if !ok {
return nil, errors.New("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, errors.New("midi.message.create ControlChange requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, errors.New("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, errors.New("midi.message.create ControlChange requires a controller parameter")
}
controllerString, ok := controller.(string)
if !ok {
return nil, errors.New("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, errors.New("midi.message.create ControlChange requires a value parameter")
}
valueString, ok := value.(string)
if !ok {
return nil, errors.New("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, errors.New("midi.message.create ProgramChange requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, errors.New("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, errors.New("midi.message.create ProgramChange requires a program parameter")
}
programString, ok := program.(string)
if !ok {
return nil, errors.New("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, errors.New("midi.message.create requires a type parameter")
}
msgTypeString, ok := msgType.(string)
if !ok {
return nil, errors.New("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

@@ -4,7 +4,7 @@ package processor
import (
"context"
"fmt"
"errors"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
@@ -18,7 +18,7 @@ func (mmd *MIDIMessageDecode) Process(ctx context.Context, payload any) (any, er
payloadBytes, ok := payload.([]byte)
if !ok {
return nil, fmt.Errorf("midi.message.decode processor only accepts a []byte")
return nil, errors.New("midi.message.decode processor only accepts a []byte")
}
payloadMessage := midi.Message(payloadBytes)

View File

@@ -4,7 +4,7 @@ package processor
import (
"context"
"fmt"
"errors"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
@@ -18,7 +18,7 @@ func (mme *MIDIMessageEncode) Process(ctx context.Context, payload any) (any, er
payloadMessage, ok := payload.(midi.Message)
if !ok {
return nil, fmt.Errorf("midi.message.encode processor only accepts an midi.Message")
return nil, errors.New("midi.message.encode processor only accepts an midi.Message")
}
return payloadMessage.Bytes(), nil

View File

@@ -0,0 +1,55 @@
//go:build cgo
package processor
import (
"context"
"errors"
"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, errors.New("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, errors.New("midi.message.filter requires a type parameter")
}
midiTypeString, ok := midiType.(string)
if !ok {
return nil, errors.New("midi.message.filter type must be a string")
}
return &MIDIMessageFilter{config: config, MIDIType: midiTypeString}, nil
},
})
}

View File

@@ -0,0 +1,91 @@
//go:build cgo
package processor
import (
"context"
"errors"
"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, errors.New("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

@@ -2,16 +2,16 @@ package processor
import (
"context"
"fmt"
"errors"
"github.com/jwetzell/showbridge-go/internal/config"
)
type MQTTMessage struct {
Topic string
QoS byte
Payload any
Retained bool
topic string
qos byte
payload []byte
retained bool
}
type MQTTMessageCreate struct {
@@ -19,16 +19,44 @@ type MQTTMessageCreate struct {
Topic string
QoS byte
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) {
message := MQTTMessage{
Topic: mmc.Topic,
QoS: mmc.QoS,
Retained: mmc.Retained,
Payload: mmc.Payload,
topic: mmc.Topic,
qos: mmc.QoS,
retained: mmc.Retained,
payload: mmc.Payload,
}
return message, nil
@@ -46,47 +74,59 @@ func init() {
topic, ok := params["topic"]
if !ok {
return nil, fmt.Errorf("mqtt.message.create requires an topic parameter")
return nil, errors.New("mqtt.message.create requires a topic parameter")
}
topicString, ok := topic.(string)
if !ok {
return nil, fmt.Errorf("mqtt.message.create topic must be a string")
return nil, errors.New("mqtt.message.create topic must be a string")
}
qos, ok := params["qos"]
if !ok {
return nil, fmt.Errorf("mqtt.message.create requires an qos parameter")
return nil, errors.New("mqtt.message.create requires a qos parameter")
}
qosByte, ok := qos.(float64)
if !ok {
return nil, fmt.Errorf("mqtt.message.create qos must be a number")
return nil, errors.New("mqtt.message.create qos must be a number")
}
retained, ok := params["retained"]
if !ok {
return nil, fmt.Errorf("mqtt.message.create requires an retained parameter")
return nil, errors.New("mqtt.message.create requires a retained parameter")
}
retainedBool, ok := retained.(bool)
if !ok {
return nil, fmt.Errorf("mqtt.message.create retained must be a boolean")
return nil, errors.New("mqtt.message.create retained must be a boolean")
}
//TODO(jwetzell): convert payload into []byte or string for sending
payload, ok := params["payload"]
if !ok {
return nil, fmt.Errorf("mqtt.message.create requires an payload parameter")
return nil, errors.New("mqtt.message.create requires a 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, errors.New("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

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -16,7 +16,7 @@ func (mme *MQTTMessageEncode) Process(ctx context.Context, payload any) (any, er
payloadMessage, ok := payload.(mqtt.Message)
if !ok {
return nil, fmt.Errorf("mqtt.message.encode processor only accepts an mqtt.Message")
return nil, errors.New("mqtt.message.encode processor only accepts an mqtt.Message")
}
return payloadMessage.Payload(), nil

View File

@@ -3,7 +3,7 @@ package processor
import (
"bytes"
"context"
"fmt"
"errors"
"text/template"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -52,25 +52,25 @@ func init() {
subject, ok := params["subject"]
if !ok {
return nil, fmt.Errorf("nats.message.create requires a subject parameter")
return nil, errors.New("nats.message.create requires a subject parameter")
}
subjectString, ok := subject.(string)
if !ok {
return nil, fmt.Errorf("nats.message.create subject must be a string")
return nil, errors.New("nats.message.create subject must be a string")
}
payload, ok := params["payload"]
if !ok {
return nil, fmt.Errorf("osc.message.create requires a payload parameter")
return nil, errors.New("nats.message.create requires a payload parameter")
}
payloadString, ok := payload.(string)
if !ok {
return nil, fmt.Errorf("osc.message.create payload must be a string")
return nil, errors.New("nats.message.create payload must be a string")
}
payloadTemplate, err := template.New("payload").Parse(payloadString)

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/nats-io/nats.go"
@@ -16,7 +16,7 @@ func (nme *NATSMessageEncode) Process(ctx context.Context, payload any) (any, er
payloadMessage, ok := payload.(*nats.Msg)
if !ok {
return nil, fmt.Errorf("nats.message.encode processor only accepts an nats.Msg")
return nil, errors.New("nats.message.encode processor only accepts an nats.Msg")
}
return payloadMessage.Data, nil

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"strconv"
"text/template"
@@ -31,11 +32,11 @@ func (o *OSCMessageCreate) Process(ctx context.Context, payload any) (any, error
addressString := addressBuffer.String()
if len(addressString) == 0 {
return nil, fmt.Errorf("osc.message.create address must not be empty")
return nil, errors.New("osc.message.create address must not be empty")
}
if addressString[0] != '/' {
return nil, fmt.Errorf("osc.message.create address must start with '/'")
return nil, errors.New("osc.message.create address must start with '/'")
}
payloadMessage := osc.OSCMessage{
@@ -82,13 +83,13 @@ func init() {
address, ok := params["address"]
if !ok {
return nil, fmt.Errorf("osc.message.create requires an address parameter")
return nil, errors.New("osc.message.create requires an address parameter")
}
addressString, ok := address.(string)
if !ok {
return nil, fmt.Errorf("osc.message.create address must be a string")
return nil, errors.New("osc.message.create address must be a string")
}
addressTemplate, err := template.New("address").Parse(addressString)
@@ -109,17 +110,17 @@ func init() {
types, ok := params["types"]
if !ok {
return nil, fmt.Errorf("osc.message.create requires a types parameter with args")
return nil, errors.New("osc.message.create requires a types parameter with args")
}
typesString, ok := types.(string)
if !ok {
return nil, fmt.Errorf("osc.message.create types must be a string")
return nil, errors.New("osc.message.create types must be a string")
}
if len(rawArgs) != len(typesString) {
return nil, fmt.Errorf("osc.message.create args and types must be the same length")
return nil, errors.New("osc.message.create args and types must be the same length")
}
argTemplates := []*template.Template{}
@@ -128,7 +129,7 @@ func init() {
argString, ok := rawArg.(string)
if !ok {
return nil, fmt.Errorf("osc.message.create arg must be a string")
return nil, errors.New("osc.message.create arg must be a string")
}
argTemplate, err := template.New("arg").Parse(argString)

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -16,15 +16,15 @@ func (o *OSCMessageDecode) Process(ctx context.Context, payload any) (any, error
payloadBytes, ok := payload.([]byte)
if !ok {
return nil, fmt.Errorf("osc.message.decode processor only accepts a []byte payload")
return nil, errors.New("osc.message.decode processor only accepts a []byte payload")
}
if len(payloadBytes) == 0 {
return nil, fmt.Errorf("osc.message.decode processor can't work on empty []byte")
return nil, errors.New("osc.message.decode processor can't work on empty []byte")
}
if payloadBytes[0] != '/' {
return nil, fmt.Errorf("osc.message.decode processor needs an OSC looking []byte")
return nil, errors.New("osc.message.decode processor needs an OSC looking []byte")
}
message, err := osc.MessageFromBytes(payloadBytes)

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -16,7 +16,7 @@ func (o *OSCMessageEncode) Process(ctx context.Context, payload any) (any, error
payloadMessage, ok := payload.(osc.OSCMessage)
if !ok {
return nil, fmt.Errorf("osc.message.encode processor only accepts an OSCMessage")
return nil, errors.New("osc.message.encode processor only accepts an OSCMessage")
}
bytes := payloadMessage.ToBytes()

View File

@@ -2,6 +2,7 @@ package processor
import (
"context"
"errors"
"fmt"
"regexp"
"strings"
@@ -20,7 +21,7 @@ func (o *OSCMessageFilter) Process(ctx context.Context, payload any) (any, error
payloadMessage, ok := payload.(osc.OSCMessage)
if !ok {
return nil, fmt.Errorf("osc.message.filter can only operate on OSCMessage payloads")
return nil, errors.New("osc.message.filter can only operate on OSCMessage payloads")
}
if !o.Address.MatchString(payloadMessage.Address) {
@@ -42,13 +43,13 @@ func init() {
address, ok := params["address"]
if !ok {
return nil, fmt.Errorf("osc.message.filter requires an address parameter")
return nil, errors.New("osc.message.filter requires an address parameter")
}
addressString, ok := address.(string)
if !ok {
return nil, fmt.Errorf("osc.message.filter address must be a string")
return nil, errors.New("osc.message.filter address must be a string")
}
addressPattern := strings.ReplaceAll(addressString, "?", ".")

View File

@@ -3,7 +3,7 @@ package processor
import (
"bytes"
"context"
"fmt"
"errors"
"text/template"
osc "github.com/jwetzell/osc-go"
@@ -19,11 +19,10 @@ func (o *OSCMessageTransform) Process(ctx context.Context, payload any) (any, er
payloadMessage, ok := payload.(osc.OSCMessage)
if !ok {
return nil, fmt.Errorf("osc.message.transform processor only accepts an OSCMessage")
return nil, errors.New("osc.message.transform processor only accepts an OSCMessage")
}
var addressBuffer bytes.Buffer
//TODO(jwetzell): actually inject data into template
err := o.Address.Execute(&addressBuffer, payloadMessage)
if err != nil {
@@ -33,11 +32,11 @@ func (o *OSCMessageTransform) Process(ctx context.Context, payload any) (any, er
addressString := addressBuffer.String()
if len(addressString) == 0 {
return nil, fmt.Errorf("osc.message.transform address must not be empty")
return nil, errors.New("osc.message.transform address must not be empty")
}
if addressString[0] != '/' {
return nil, fmt.Errorf("osc.message.transform address must start with '/'")
return nil, errors.New("osc.message.transform address must start with '/'")
}
payloadMessage.Address = addressString
@@ -57,13 +56,13 @@ func init() {
address, ok := params["address"]
if !ok {
return nil, fmt.Errorf("osc.message.transform requires an address parameter")
return nil, errors.New("osc.message.transform requires an address parameter")
}
addressString, ok := address.(string)
if !ok {
return nil, fmt.Errorf("osc.message.transform address must be a string")
return nil, errors.New("osc.message.transform address must be a string")
}
addressTemplate, err := template.New("address").Parse(addressString)

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
@@ -38,13 +38,13 @@ func init() {
expression, ok := params["expression"]
if !ok {
return nil, fmt.Errorf("script.expr requires an expression parameter")
return nil, errors.New("script.expr requires an expression parameter")
}
expressionString, ok := expression.(string)
if !ok {
return nil, fmt.Errorf("script.expr expression must be a string")
return nil, errors.New("script.expr expression must be a string")
}
program, err := expr.Compile(expressionString)

View File

@@ -0,0 +1,58 @@
package processor_test
import (
"testing"
"github.com/expr-lang/expr"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestGoodScriptExpr(t *testing.T) {
tests := []struct {
program string
name string
payload map[string]any
expected any
}{
{
program: "foo + bar",
name: "number",
payload: map[string]any{
"foo": 1,
"bar": 1,
},
expected: 2,
},
{
program: "foo + bar",
name: "string",
payload: map[string]any{
"foo": "1",
"bar": "1",
},
expected: "11",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
program, err := expr.Compile(test.program)
if err != nil {
t.Errorf("script.expr failed to compile program: %s", err)
}
exprProcessor := &processor.ScriptExpr{Program: program}
got, err := exprProcessor.Process(t.Context(), test.payload)
if err != nil {
t.Errorf("script.expr failed: %s", err)
}
//TODO(jwetzell): work out better way to compare the any/any
if got != test.expected {
t.Errorf("script.expr got %+v (%T), expected %+v (%T)", got, got, test.expected, test.expected)
}
})
}
}

View File

@@ -3,7 +3,7 @@ package processor
import (
"context"
"encoding/json"
"fmt"
"errors"
"github.com/jwetzell/showbridge-go/internal/config"
"modernc.org/quickjs"
@@ -74,13 +74,13 @@ func init() {
program, ok := params["program"]
if !ok {
return nil, fmt.Errorf("script.js requires a program parameter")
return nil, errors.New("script.js requires a program parameter")
}
programString, ok := program.(string)
if !ok {
return nil, fmt.Errorf("script.js program must be a string")
return nil, errors.New("script.js program must be a string")
}
return &ScriptJS{config: config, Program: programString}, nil

View File

@@ -0,0 +1,48 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestGoodScriptJS(t *testing.T) {
tests := []struct {
processor processor.Processor
name string
payload any
expected any
}{
{
processor: &processor.ScriptJS{Program: `
payload = payload + 1
`},
name: "number",
payload: 1,
expected: 2,
},
{
processor: &processor.ScriptJS{Program: `
payload = payload + "1"
`},
name: "string",
payload: "1",
expected: "11",
},
}
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("script.js failed: %s", err)
}
//TODO(jwetzell): work out better way to compare the any/any
if got != test.expected {
t.Errorf("script.js got %+v, expected %+v", got, test.expected)
}
})
}
}

View File

@@ -3,7 +3,7 @@ package processor
import (
"bytes"
"context"
"fmt"
"errors"
"text/template"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -39,13 +39,13 @@ func init() {
tmpl, ok := params["template"]
if !ok {
return nil, fmt.Errorf("string.create requires a template parameter")
return nil, errors.New("string.create requires a template parameter")
}
templateString, ok := tmpl.(string)
if !ok {
return nil, fmt.Errorf("string.create template must be a string")
return nil, errors.New("string.create template must be a string")
}
templateTemplate, err := template.New("template").Parse(templateString)

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -15,7 +15,7 @@ func (sd *StringDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := payload.([]byte)
if !ok {
return nil, fmt.Errorf("string.decode processor only accepts a []byte")
return nil, errors.New("string.decode processor only accepts a []byte")
}
payloadMessage := string(payloadBytes)

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -15,7 +15,7 @@ func (se *StringEncode) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string)
if !ok {
return nil, fmt.Errorf("string.encode processor only accepts a string")
return nil, errors.New("string.encode processor only accepts a string")
}
payloadBytes := []byte(payloadString)

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
"regexp"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -17,7 +17,7 @@ func (se *StringFilter) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string)
if !ok {
return nil, fmt.Errorf("string.filter processor only accepts a string")
return nil, errors.New("string.filter processor only accepts a string")
}
if !se.Pattern.MatchString(payloadString) {
@@ -40,16 +40,16 @@ func init() {
pattern, ok := params["pattern"]
if !ok {
return nil, fmt.Errorf("http.request.filter requires an pattern parameter")
return nil, errors.New("string.filter requires a pattern parameter")
}
patternString, ok := pattern.(string)
if !ok {
return nil, fmt.Errorf("http.request.filter pattern must be a string")
return nil, errors.New("string.filter pattern must be a string")
}
patternRegexp, err := regexp.Compile(fmt.Sprintf("^%s$", patternString))
patternRegexp, err := regexp.Compile(patternString)
if err != nil {
return nil, err

View File

@@ -2,7 +2,7 @@ package processor
import (
"context"
"fmt"
"errors"
"strings"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -17,7 +17,7 @@ func (se *StringSplit) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string)
if !ok {
return nil, fmt.Errorf("string.split only accepts a string")
return nil, errors.New("string.split only accepts a string")
}
payloadParts := strings.Split(payloadString, se.Separator)
@@ -38,13 +38,13 @@ func init() {
separator, ok := params["separator"]
if !ok {
return nil, fmt.Errorf("string.split requires a separator")
return nil, errors.New("string.split requires a separator")
}
separatorString, ok := separator.(string)
if !ok {
return nil, fmt.Errorf("string.split separator must be a string")
return nil, errors.New("string.split separator must be a string")
}
return &StringSplit{config: config, Separator: separatorString}, nil

View File

@@ -40,7 +40,7 @@ func TestGoodStringSplit(t *testing.T) {
}
}
func TestBasStringSplit(t *testing.T) {
func TestBadStringSplit(t *testing.T) {
tests := []struct {
processor processor.Processor
name string

View File

@@ -2,13 +2,15 @@ package processor
import (
"context"
"fmt"
"errors"
"strconv"
"github.com/jwetzell/showbridge-go/internal/config"
)
type UintParse struct {
Base int
BitSize int
config config.ProcessorConfig
}
@@ -16,11 +18,10 @@ func (up *UintParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string)
if !ok {
return nil, fmt.Errorf("uint.parse processor only accepts a string")
return nil, errors.New("uint.parse processor only accepts a string")
}
// TODO(jwetzell): make base and bitSize configurable
payloadUint, err := strconv.ParseUint(payloadString, 10, 64)
payloadUint, err := strconv.ParseUint(payloadString, up.Base, up.BitSize)
if err != nil {
return nil, err
}
@@ -35,7 +36,31 @@ func init() {
RegisterProcessor(ProcessorRegistration{
Type: "uint.parse",
New: func(config config.ProcessorConfig) (Processor, error) {
return &UintParse{config: config}, nil
params := config.Params
baseNum := 10
base, ok := params["base"]
if ok {
baseFloat, ok := base.(float64)
if !ok {
return nil, errors.New("uint.parse base must be a number")
}
baseNum = int(baseFloat)
}
bitSizeNum := 64
bitSize, ok := params["bitSize"]
if ok {
bitSizeFloat, ok := bitSize.(float64)
if !ok {
return nil, errors.New("uint.parse bitSize must be a number")
}
bitSizeNum = int(bitSizeFloat)
}
return &UintParse{config: config, Base: baseNum, BitSize: bitSizeNum}, nil
},
})
}

View File

@@ -2,9 +2,9 @@ package showbridge
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"sync"
"github.com/jwetzell/showbridge-go/internal/config"
@@ -18,16 +18,10 @@ type Router struct {
ModuleInstances []module.Module
RouteInstances []route.Route
moduleWait sync.WaitGroup
logger *slog.Logger
}
func NewRouter(ctx context.Context, config config.Config) (*Router, []module.ModuleError, []route.RouteError) {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
slog.Debug("creating router")
routerContext, cancel := context.WithCancel(ctx)
router := Router{
@@ -35,8 +29,11 @@ func NewRouter(ctx context.Context, config config.Config) (*Router, []module.Mod
contextCancel: cancel,
ModuleInstances: []module.Module{},
RouteInstances: []route.Route{},
logger: slog.Default().With("component", "router"),
}
router.logger.Debug("creating router")
var moduleErrors []module.ModuleError
for moduleIndex, moduleDecl := range config.Modules {
@@ -49,7 +46,7 @@ func NewRouter(ctx context.Context, config config.Config) (*Router, []module.Mod
moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex,
Config: moduleDecl,
Error: fmt.Errorf("module type not defined"),
Error: errors.New("module type not defined"),
})
continue
}
@@ -64,7 +61,7 @@ func NewRouter(ctx context.Context, config config.Config) (*Router, []module.Mod
moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex,
Config: moduleDecl,
Error: fmt.Errorf("duplicate module id"),
Error: errors.New("duplicate module id"),
})
break
}
@@ -110,20 +107,20 @@ func NewRouter(ctx context.Context, config config.Config) (*Router, []module.Mod
}
func (r *Router) Run() {
slog.Info("running router")
r.logger.Info("running router")
for _, moduleInstance := range r.ModuleInstances {
r.moduleWait.Add(1)
go func() {
err := moduleInstance.Run()
if err != nil {
slog.Error("error encountered running module", "id", moduleInstance.Id(), "error", err)
r.logger.Error("error encountered running module", "error", err)
}
r.moduleWait.Done()
}()
}
<-r.Context.Done()
r.moduleWait.Wait()
slog.Info("router done")
r.logger.Info("router done")
}
func (r *Router) Stop() {
@@ -143,7 +140,7 @@ func (r *Router) HandleInput(sourceId string, payload any) []route.RouteIOError
Index: routeIndex,
Error: err,
})
slog.Error("router unable to route input", "route", routeIndex, "source", sourceId, "error", err)
r.logger.Error("router unable to route input", "route", routeIndex, "source", sourceId, "error", err)
}
}
}