421 Commits

Author SHA1 Message Date
Joel Wetzell
b6c1c5c600 fix schema for router.input 2026-03-14 09:05:22 -05:00
Joel Wetzell
b6ec615e40 comment 2026-03-13 21:37:40 -05:00
Joel Wetzell
216775c9c6 switch to read lock in getter method 2026-03-13 21:32:59 -05:00
Joel Wetzell
1bfee2186b add payload type to span attributes 2026-03-13 21:18:47 -05:00
Joel Wetzell
fa612231fb fix otel import path 2026-03-13 21:18:32 -05:00
Joel Wetzell
c1161c5aa9 add processor to push input into router (#104) 2026-03-13 21:17:54 -05:00
Joel Wetzell
b1076ce1e4 do some manual casting for script.js payload 2026-03-12 21:38:08 -05:00
Joel Wetzell
7f5204aca0 set minimum port above 1024 for server modules 2026-03-12 17:51:46 -05:00
Joel Wetzell
f3d87e92f8 Merge pull request #103 from jwetzell/feat/http-api
add an HTTP/WS API for interacting with webui
2026-03-12 17:35:53 -05:00
Joel Wetzell
e50e269697 pull from config changes channel to write out to config file 2026-03-12 17:34:12 -05:00
Joel Wetzell
ca6aebadef add channel for router to communicate config changes out 2026-03-12 17:33:41 -05:00
Joel Wetzell
818ddc18f4 go mod tidy 2026-03-12 17:13:12 -05:00
Joel Wetzell
3e7b245536 add config reloading via API endpoint and more WS work 2026-03-12 17:04:21 -05:00
Joel Wetzell
e996d84234 udpate module and route config error objects for JSON 2026-03-12 17:03:03 -05:00
Joel Wetzell
04c8ebbe06 add api config to example config 2026-03-11 21:07:30 -05:00
Joel Wetzell
0f57e123ce start work on http/ws api 2026-03-11 20:58:53 -05:00
Joel Wetzell
82ba1d5d10 log out errors running modules 2026-03-11 12:49:08 -05:00
Joel Wetzell
20c53e4996 Merge pull request #101 from jwetzell/dependabot/go_modules/github.com/nats-io/nats-server/v2-2.12.5
Bump github.com/nats-io/nats-server/v2 from 2.12.4 to 2.12.5
2026-03-11 10:03:20 -05:00
Joel Wetzell
8132faaba6 reuse quickjs objects in script.js processor 2026-03-10 18:14:50 -05:00
Joel Wetzell
5a0f21bd64 move any helper methods to common and reuse for params getter 2026-03-10 18:14:30 -05:00
dependabot[bot]
fd0f78733c Bump github.com/nats-io/nats-server/v2 from 2.12.4 to 2.12.5
Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.12.4 to 2.12.5.
- [Release notes](https://github.com/nats-io/nats-server/releases)
- [Changelog](https://github.com/nats-io/nats-server/blob/main/RELEASES.md)
- [Commits](https://github.com/nats-io/nats-server/compare/v2.12.4...v2.12.5)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats-server/v2
  dependency-version: 2.12.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 17:26:30 +00:00
Joel Wetzell
65476d5ecc rename files in common package 2026-03-09 14:13:17 -05:00
Joel Wetzell
536bc2af4e Merge pull request #96 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp-1.42.0
Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.41.0 to 1.42.0
2026-03-08 17:35:41 -05:00
dependabot[bot]
b89d907759 Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go) from 1.41.0 to 1.42.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.42.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
  dependency-version: 1.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 22:34:42 +00:00
Joel Wetzell
a3c48de2ad Merge pull request #97 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel/sdk-1.42.0
Bump go.opentelemetry.io/otel/sdk from 1.41.0 to 1.42.0
2026-03-08 17:33:34 -05:00
dependabot[bot]
8ca9d9940f Bump go.opentelemetry.io/otel/sdk from 1.41.0 to 1.42.0
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.41.0 to 1.42.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.42.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 22:27:05 +00:00
Joel Wetzell
53d0648774 Merge pull request #98 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel-1.42.0
Bump go.opentelemetry.io/otel from 1.41.0 to 1.42.0
2026-03-08 17:26:02 -05:00
Joel Wetzell
cf2881e5c3 Merge pull request #94 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel/trace-1.42.0
Bump go.opentelemetry.io/otel/trace from 1.41.0 to 1.42.0
2026-03-08 17:25:46 -05:00
Joel Wetzell
f437438dca Merge pull request #93 from jwetzell/dependabot/github_actions/docker/setup-qemu-action-4.0.0
Bump docker/setup-qemu-action from 3.7.0 to 4.0.0
2026-03-08 17:25:37 -05:00
Joel Wetzell
6b3c470534 Merge pull request #92 from jwetzell/dependabot/github_actions/docker/setup-buildx-action-4.0.0
Bump docker/setup-buildx-action from 3.12.0 to 4.0.0
2026-03-08 17:25:29 -05:00
Joel Wetzell
3bdff5309f Merge pull request #91 from jwetzell/dependabot/github_actions/docker/login-action-4.0.0
Bump docker/login-action from 3.7.0 to 4.0.0
2026-03-08 17:25:21 -05:00
Joel Wetzell
3792d40bb1 Merge pull request #90 from jwetzell/dependabot/github_actions/docker/metadata-action-6.0.0
Bump docker/metadata-action from 5.10.0 to 6.0.0
2026-03-08 17:25:10 -05:00
Joel Wetzell
0cca2e3be9 Merge pull request #95 from jwetzell/dependabot/github_actions/docker/build-push-action-7.0.0
Bump docker/build-push-action from 6.19.2 to 7.0.0
2026-03-08 17:24:41 -05:00
Joel Wetzell
5447781232 Merge pull request #99 from jwetzell/dependabot/go_modules/github.com/emiago/sipgo-1.2.1
Bump github.com/emiago/sipgo from 1.2.0 to 1.2.1
2026-03-08 17:24:25 -05:00
Joel Wetzell
c760b220c0 Merge pull request #100 from jwetzell/feat/filter-regex
rename string.filter to filter.regex
2026-03-08 17:22:17 -05:00
Joel Wetzell
cb71b5c5b8 rename string.filter to filter.regex 2026-03-08 17:19:49 -05:00
dependabot[bot]
84fd7c1028 Bump github.com/emiago/sipgo from 1.2.0 to 1.2.1
Bumps [github.com/emiago/sipgo](https://github.com/emiago/sipgo) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/emiago/sipgo/releases)
- [Commits](https://github.com/emiago/sipgo/compare/v1.2.0...v1.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:56 +00:00
dependabot[bot]
7bdf6e01b3 Bump go.opentelemetry.io/otel from 1.41.0 to 1.42.0
Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.41.0 to 1.42.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.42.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:51 +00:00
dependabot[bot]
1b607f64cd Bump go.opentelemetry.io/otel/trace from 1.41.0 to 1.42.0
Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.41.0 to 1.42.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.42.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/trace
  dependency-version: 1.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:36 +00:00
dependabot[bot]
28e9141cbc Bump docker/build-push-action from 6.19.2 to 7.0.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](10e90e3645...d08e5c354a)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:36 +00:00
dependabot[bot]
3b38110ffb Bump docker/setup-qemu-action from 3.7.0 to 4.0.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](c7c5346462...ce360397dd)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:32 +00:00
dependabot[bot]
25e8563cd1 Bump docker/setup-buildx-action from 3.12.0 to 4.0.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.12.0 to 4.0.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](8d2750c68a...4d04d5d948)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:29 +00:00
dependabot[bot]
80969ac1ff Bump docker/login-action from 3.7.0 to 4.0.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](c94ce9fb46...b45d80f862)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:24 +00:00
dependabot[bot]
beb3664e3b Bump docker/metadata-action from 5.10.0 to 6.0.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.10.0 to 6.0.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](c299e40c65...030e881283)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:21 +00:00
Joel Wetzell
6e53cbe8fd format README 2026-03-08 15:32:36 -05:00
Joel Wetzell
4ebaa48f93 yaml formatting 2026-03-08 15:32:27 -05:00
Joel Wetzell
79d535c283 json formatting 2026-03-08 15:31:04 -05:00
Joel Wetzell
85349f8540 Merge pull request #89 from jwetzell/feat/int-scale
add processor to scale int values
2026-03-08 15:29:27 -05:00
Joel Wetzell
e49900a3a7 add processor to scale int values 2026-03-08 15:28:05 -05:00
Joel Wetzell
1f706becdc fix go version in dockerfile 2026-03-08 14:20:28 -05:00
Joel Wetzell
13a494239e Merge pull request #88 from jwetzell/remove-specific-filter-processors
remove specific filter processors
2026-03-08 14:13:11 -05:00
Joel Wetzell
153de944a2 remove filter processors 2026-03-08 14:06:28 -05:00
Joel Wetzell
42c75074fe update Go version 2026-03-08 13:38:52 -05:00
Joel Wetzell
361b07ec00 fix error message 2026-03-08 13:36:06 -05:00
Joel Wetzell
8c4f0591e1 Merge pull request #87 from jwetzell/feat/script-processor-env-data
consistent script processor environments
2026-03-08 13:34:01 -05:00
Joel Wetzell
09ddc40f1f consistent script processor environments 2026-03-08 13:32:34 -05:00
Joel Wetzell
6382cf6944 Merge pull request #86 from jwetzell/feat/struct-get-pointer
allow struct based processors to also operate on pointers to structs
2026-03-08 13:16:08 -05:00
Joel Wetzell
0732113a02 allow struct based processors to also operate on pointers to structs 2026-03-08 13:14:50 -05:00
Joel Wetzell
e5db9a48a9 Merge pull request #85 from jwetzell/feat/json-decode-bytes
allow json.decode to take byte slice and string
2026-03-08 13:11:17 -05:00
Joel Wetzell
9502201261 allow json.decode to take byte slice and string 2026-03-08 13:09:22 -05:00
Joel Wetzell
9859aa8fd4 Merge pull request #84 from jwetzell/feat/http-client-processor
make http client into a processor instead of module
2026-03-08 13:08:26 -05:00
Joel Wetzell
b7b05cbb77 make http client into a processor instead of module 2026-03-08 13:03:54 -05:00
Joel Wetzell
7b1fe47039 align variable names 2026-03-08 09:36:59 -05:00
Joel Wetzell
0848d8ab6e add descriptions to time module params 2026-03-08 09:36:31 -05:00
Joel Wetzell
c1bb2ff4cb update schema descriptions 2026-03-08 09:22:57 -05:00
Joel Wetzell
50898b9130 add missing title for params property 2026-03-07 11:49:29 -06:00
Joel Wetzell
05e68e90af add title and descriptions for params schemas 2026-03-06 18:59:27 -06:00
Joel Wetzell
cd4c3f59f2 fix missing properties on parse processors 2026-03-06 18:37:04 -06:00
Joel Wetzell
50399fa811 update titles on module schemas 2026-03-06 18:36:49 -06:00
Joel Wetzell
2dd44a11fd Merge pull request #83 from jwetzell/feat/router-output-as-processor
switch router output to be a processor instead of specific output per route
2026-03-04 21:34:10 -06:00
Joel Wetzell
b7a8b04a72 switch router output to be a processor instead of specific output per route 2026-03-04 21:21:11 -06:00
Joel Wetzell
078e6ec68c more detailed error message 2026-03-04 19:42:48 -06:00
Joel Wetzell
b58187d6a9 Merge pull request #82 from jwetzell/feat/sender-template-data
add sender to template data for relevant modules
2026-03-04 19:39:54 -06:00
Joel Wetzell
38b8e44f04 add sender to template data for relevant modules 2026-03-04 19:38:30 -06:00
Joel Wetzell
dff33f722d Merge pull request #81 from jwetzell/feat/richer-templating-data
use a struct to pass multiple pieces of data into templating context
2026-03-04 12:40:37 -06:00
Joel Wetzell
572a54d3b2 use a struct to pass multiple pieces of data into templating context 2026-03-04 12:38:51 -06:00
Joel Wetzell
6a21cc2639 make method to get data out of a module 2026-03-04 12:35:15 -06:00
Joel Wetzell
0d4ec24a9b move context keys to common package 2026-03-04 12:33:35 -06:00
Joel Wetzell
10c6e84682 fix template usage in freed.create 2026-03-04 09:14:09 -06:00
Joel Wetzell
7ac0ec7582 remove uint parse and random 2026-03-03 21:42:08 -06:00
Joel Wetzell
c25a36f09c Merge pull request #80 from jwetzell/feat/random-float
add processor to generator random float
2026-03-03 21:26:28 -06:00
Joel Wetzell
8d2c022baf fix variable name 2026-03-03 21:21:47 -06:00
Joel Wetzell
76318b4e10 add processor to generator random float 2026-03-03 21:20:36 -06:00
Joel Wetzell
633eaba0ec fix osc.message.create tests 2026-03-03 07:00:05 -06:00
Joel Wetzell
00b6f3660d fix osc.message.create 2026-03-03 06:58:46 -06:00
Joel Wetzell
a275cd2b78 upgrade osc library and switch to pointers 2026-03-02 21:17:49 -06:00
Joel Wetzell
5cb2f845a1 add more error case tests to osc.message.decode 2026-03-02 21:04:18 -06:00
Joel Wetzell
8f769f0a4a add OSC encode/decode test with arg 2026-03-02 20:59:29 -06:00
Joel Wetzell
fdd4b341cb fix osc message decode test 2026-03-02 20:56:16 -06:00
Joel Wetzell
9a9bf10078 add tests for osc message encode/decode 2026-03-02 20:54:03 -06:00
Joel Wetzell
ea46885f70 rework GetInt and GetIntSlice and more testing for params config 2026-03-02 20:53:53 -06:00
Joel Wetzell
fa8f0b7cfc add test for getting array and void from struct 2026-03-02 20:53:20 -06:00
Joel Wetzell
d316147411 align test err checking and logs 2026-03-02 20:53:00 -06:00
Joel Wetzell
30564523d7 Merge pull request #78 from jwetzell/dependabot/go_modules/github.com/urfave/cli/v3-3.7.0
Bump github.com/urfave/cli/v3 from 3.6.2 to 3.7.0
2026-03-02 14:07:05 -06:00
dependabot[bot]
f7231e54c2 Bump github.com/urfave/cli/v3 from 3.6.2 to 3.7.0
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.6.2 to 3.7.0.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.6.2...v3.7.0)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 20:05:27 +00:00
Joel Wetzell
14de829e7a Merge pull request #75 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp-1.41.0
Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.40.0 to 1.41.0
2026-03-02 14:04:27 -06:00
Joel Wetzell
33a54eb24c Merge pull request #76 from jwetzell/dependabot/go_modules/gitlab.com/gomidi/midi/v2-2.3.23
Bump gitlab.com/gomidi/midi/v2 from 2.3.22 to 2.3.23
2026-03-02 14:04:12 -06:00
Joel Wetzell
3c6c50c914 add tests for midi and mqtt message create processors 2026-03-02 14:01:47 -06:00
Joel Wetzell
da516f5102 fix test wording 2026-03-02 14:01:32 -06:00
Joel Wetzell
1f88819428 use params methods iin mqtt.message.create 2026-03-02 14:01:20 -06:00
Joel Wetzell
df4f0f745d fix GetByteSlice for params 2026-03-02 14:00:47 -06:00
dependabot[bot]
d86bff30cd Bump gitlab.com/gomidi/midi/v2 from 2.3.22 to 2.3.23
Bumps [gitlab.com/gomidi/midi/v2](https://gitlab.com/gomidi/midi) from 2.3.22 to 2.3.23.
- [Commits](https://gitlab.com/gomidi/midi/compare/v2.3.22...v2.3.23)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 19:21:14 +00:00
dependabot[bot]
22d8bbf88d Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go) from 1.40.0 to 1.41.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.40.0...v1.41.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
  dependency-version: 1.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 19:21:05 +00:00
Joel Wetzell
925873a124 add tests for midi.message.decode 2026-03-02 12:55:33 -06:00
Joel Wetzell
08308c1891 add count flag to hopefully avoid cache issues 2026-03-02 12:47:01 -06:00
Joel Wetzell
f68312ee84 standardize layout of bad tests 2026-03-02 12:34:46 -06:00
Joel Wetzell
cd1b5e1437 add tests for artnet packet encode/decode processors 2026-03-02 12:34:26 -06:00
Joel Wetzell
34af7d7aec cleanup unnecessary use of floats 2026-03-02 12:34:06 -06:00
Joel Wetzell
df14024012 add convenience method for casting payloads 2026-03-02 12:06:42 -06:00
Joel Wetzell
af39c16f30 add more convenience functions for getting slices from params and add tests 2026-03-02 12:03:35 -06:00
Joel Wetzell
7d2a3225a7 make more error const for params config 2026-03-02 12:03:05 -06:00
Joel Wetzell
b5ffee022a use GetStringSlice in osc.message.create 2026-03-02 12:02:27 -06:00
Joel Wetzell
cd264d9ed4 add function to get string slice from params 2026-03-02 09:07:31 -06:00
Joel Wetzell
9d14e5929f wrap all payload types for expr env 2026-03-01 23:49:53 -06:00
Joel Wetzell
8fe1463198 Merge pull request #74 from jwetzell/feat/expr-payload-wrapping
wrap literal payloads into a struct for expr processors
2026-03-01 23:36:28 -06:00
Joel Wetzell
28a5d1ea90 wrap literal payloads into a struct for expr processors 2026-03-01 23:35:08 -06:00
Joel Wetzell
e9d5931e9d Merge pull request #73 from jwetzell/feat/expr-filter
add processor to filter using expr expression
2026-03-01 23:15:02 -06:00
Joel Wetzell
d90f103d00 add processor to filter using expr expression 2026-03-01 23:04:04 -06:00
Joel Wetzell
05b0de1dfd remote http request/response encoding 2026-03-01 23:02:56 -06:00
Joel Wetzell
0d8dbab743 add title for all processor schemas 2026-03-01 22:12:45 -06:00
Joel Wetzell
fb599f1057 Merge pull request #72 from jwetzell/feat/struct-processors
add processors to pull data out of struct
2026-03-01 21:57:08 -06:00
Joel Wetzell
a2efed0ee2 add processor to pull method out of struct 2026-03-01 21:47:37 -06:00
Joel Wetzell
533fe150c8 add processor to pull field out of struct 2026-03-01 21:47:31 -06:00
Joel Wetzell
b653179e6d add functions to test struct 2026-03-01 21:44:41 -06:00
Joel Wetzell
695cabf15e move test struct to common place 2026-03-01 21:36:12 -06:00
Joel Wetzell
ccac116f8d fix error message 2026-03-01 15:00:09 -06:00
Joel Wetzell
c298f63ffc add convenience method to pull params from config 2026-03-01 14:57:19 -06:00
Joel Wetzell
183182e6cd use control instead of controller in midi.message.create 2026-03-01 14:21:36 -06:00
Joel Wetzell
01f172dbee Merge pull request #71 from jwetzell/feat/nats-server-module
add nats.server module
2026-03-01 13:18:28 -06:00
Joel Wetzell
20fd4170ed handle param values that aren't from loading in JSON 2026-03-01 13:16:13 -06:00
Joel Wetzell
bcf9299505 remove nats message encode 2026-03-01 13:03:26 -06:00
Joel Wetzell
4f7820af5e add basic nats server module 2026-03-01 13:02:55 -06:00
Joel Wetzell
87fcc63068 Merge pull request #70 from jwetzell/dependabot/go_modules/github.com/nats-io/nats.go-1.49.0
Bump github.com/nats-io/nats.go from 1.48.0 to 1.49.0
2026-02-27 08:31:56 -06:00
dependabot[bot]
1c49776662 Bump github.com/nats-io/nats.go from 1.48.0 to 1.49.0
Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.48.0 to 1.49.0.
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.48.0...v1.49.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 14:21:09 +00:00
Joel Wetzell
1a7207ae4c Merge pull request #66 from jwetzell/dependabot/github_actions/docker/build-push-action-6.19.2
Bump docker/build-push-action from 6.18.0 to 6.19.2
2026-02-27 08:20:11 -06:00
Joel Wetzell
f86a34cc92 Merge pull request #67 from jwetzell/dependabot/go_modules/github.com/expr-lang/expr-1.17.8
Bump github.com/expr-lang/expr from 1.17.7 to 1.17.8
2026-02-27 08:19:46 -06:00
Joel Wetzell
d9d64bd248 Merge pull request #68 from jwetzell/dependabot/go_modules/gitlab.com/gomidi/midi/v2-2.3.22
Bump gitlab.com/gomidi/midi/v2 from 2.3.20 to 2.3.22
2026-02-27 08:19:24 -06:00
Joel Wetzell
e828dc1b0b Merge pull request #69 from jwetzell/dependabot/github_actions/goreleaser/goreleaser-action-7
Bump goreleaser/goreleaser-action from 6 to 7
2026-02-27 08:19:08 -06:00
dependabot[bot]
cc4d50bc17 Bump goreleaser/goreleaser-action from 6 to 7
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 6 to 7.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 19:22:05 +00:00
Joel Wetzell
abd37439c5 rework Route struct 2026-02-16 19:13:04 -06:00
Joel Wetzell
8c3f93b601 update tracer name for processor 2026-02-16 18:57:44 -06:00
Joel Wetzell
2f7f6967e1 update tracer name for module 2026-02-16 18:25:30 -06:00
Joel Wetzell
09b030efa6 rework otel tracer providing 2026-02-16 17:11:43 -06:00
dependabot[bot]
bb49dba22e Bump gitlab.com/gomidi/midi/v2 from 2.3.20 to 2.3.22
Bumps [gitlab.com/gomidi/midi/v2](https://gitlab.com/gomidi/midi) from 2.3.20 to 2.3.22.
- [Commits](https://gitlab.com/gomidi/midi/compare/v2.3.20...v2.3.22)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 18:21:08 +00:00
dependabot[bot]
4ed6866731 Bump github.com/expr-lang/expr from 1.17.7 to 1.17.8
Bumps [github.com/expr-lang/expr](https://github.com/expr-lang/expr) from 1.17.7 to 1.17.8.
- [Release notes](https://github.com/expr-lang/expr/releases)
- [Commits](https://github.com/expr-lang/expr/compare/v1.17.7...v1.17.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 18:21:00 +00:00
dependabot[bot]
2d392873ca Bump docker/build-push-action from 6.18.0 to 6.19.2
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.18.0 to 6.19.2.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](263435318d...10e90e3645)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.19.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 18:20:37 +00:00
Joel Wetzell
11e25ab8f7 require id properties to be at least a one character 2026-02-13 21:46:46 -06:00
Joel Wetzell
87e6b09156 fix random processors schemas 2026-02-13 21:45:19 -06:00
Joel Wetzell
8cb2a0e9f0 add more tests for osc.message.create 2026-02-10 21:23:11 -06:00
Joel Wetzell
263943f4d6 add tests for osc.message.create and filter 2026-02-10 21:16:02 -06:00
Joel Wetzell
f9865765c6 fix test names for script.wasm 2026-02-10 21:15:52 -06:00
Joel Wetzell
e6aaffbb95 add error tests for script.wasm 2026-02-10 20:49:40 -06:00
Joel Wetzell
90a14b4d1b add more tests for router 2026-02-10 20:42:52 -06:00
Joel Wetzell
8bdd338a27 Merge pull request #59 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp-1.40.0
Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.28.0 to 1.40.0
2026-02-09 23:23:18 -06:00
Joel Wetzell
b20c16be4f Merge pull request #62 from jwetzell/dependabot/go_modules/github.com/emiago/diago-0.27.0
Bump github.com/emiago/diago from 0.26.2 to 0.27.0
2026-02-09 23:22:55 -06:00
Joel Wetzell
de060057e6 Merge pull request #61 from jwetzell/dependabot/go_modules/gitlab.com/gomidi/midi/v2-2.3.20
Bump gitlab.com/gomidi/midi/v2 from 2.3.18 to 2.3.20
2026-02-09 23:22:44 -06:00
Joel Wetzell
4526e805af add basic tests for mqtt.message.encode 2026-02-09 23:18:19 -06:00
Joel Wetzell
1d8baa8c17 add function to create new MQTTMessage 2026-02-09 23:17:53 -06:00
Joel Wetzell
060c12512c generate coverage file manually to filter out cmd package 2026-02-09 23:09:57 -06:00
Joel Wetzell
444093d375 add tests for error json decode/encode error scenarios 2026-02-09 22:28:55 -06:00
Joel Wetzell
a217b729cd add tests for error script.wasm error scenarios 2026-02-09 22:28:34 -06:00
Joel Wetzell
ad76480008 add good/bad tests for time.sleep 2026-02-09 22:16:54 -06:00
Joel Wetzell
a408e281a3 standardize processor instance creation in tests 2026-02-09 22:09:48 -06:00
Joel Wetzell
f028634401 cleanup grammar on error messages 2026-02-09 21:29:12 -06:00
Joel Wetzell
961316d28b add tests for invalid addr to net servers 2026-02-09 21:23:09 -06:00
Joel Wetzell
e22a941e4a fix non returning error in udp server 2026-02-09 21:19:29 -06:00
Joel Wetzell
b0cffe819d add more bad test for net servers 2026-02-09 21:14:02 -06:00
Joel Wetzell
0cdd1f6e6a add bad creation tests for sip servers 2026-02-09 21:06:02 -06:00
Joel Wetzell
a9245ab88c fix error message 2026-02-09 21:02:44 -06:00
Joel Wetzell
d69d3a370d Merge pull request #64 from jwetzell/feat/sip-dtmf-server-useragent
add optional userAgent param to sip.dtmf.server
2026-02-09 20:58:48 -06:00
Joel Wetzell
019624f152 add optional userAgent param to sip.dtmf.server 2026-02-09 20:51:03 -06:00
Joel Wetzell
90a3119fb8 fix variable name 2026-02-09 20:48:07 -06:00
Joel Wetzell
54c14cbbae make nil payload a debug log 2026-02-09 19:57:17 -06:00
Joel Wetzell
969ac6e04b add test for module registraion 2026-02-09 19:45:04 -06:00
Joel Wetzell
bfa63499c3 add basic tests for bad module creation 2026-02-09 19:44:54 -06:00
Joel Wetzell
6b178d1ae4 cleanup error messages 2026-02-09 19:15:34 -06:00
Joel Wetzell
988437fccf add test for loading from registry for time modules 2026-02-09 18:19:17 -06:00
Joel Wetzell
dc19d18b81 add tests to ensure module id returns correctly 2026-02-09 18:17:42 -06:00
Joel Wetzell
86f1082159 add test for loading from registry for all modules 2026-02-09 18:11:08 -06:00
Joel Wetzell
a46054c427 fix net.udp.server type return value 2026-02-09 18:10:28 -06:00
Joel Wetzell
9dc4706fd8 add test for loading from registry for rest of processors 2026-02-09 17:42:16 -06:00
Joel Wetzell
f91cb9dbbf add basic tests for script.wasm 2026-02-09 15:07:18 -06:00
Joel Wetzell
10b5cbae6c add coverage badge 2026-02-09 15:06:04 -06:00
Joel Wetzell
a9f9f1ba2c Merge pull request #63 from jwetzell/feat/script-wasm-wasi
add option to enable WASI for script.wasm processor
2026-02-09 14:55:18 -06:00
Joel Wetzell
8e27dc81a2 add option to enable WASI for script.wasm processor 2026-02-09 14:52:41 -06:00
dependabot[bot]
42bd5c46b5 Bump github.com/emiago/diago from 0.26.2 to 0.27.0
Bumps [github.com/emiago/diago](https://github.com/emiago/diago) from 0.26.2 to 0.27.0.
- [Release notes](https://github.com/emiago/diago/releases)
- [Commits](https://github.com/emiago/diago/compare/v0.26.2...v0.27.0)

---
updated-dependencies:
- dependency-name: github.com/emiago/diago
  dependency-version: 0.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 20:50:55 +00:00
dependabot[bot]
515a8d14fd Bump gitlab.com/gomidi/midi/v2 from 2.3.18 to 2.3.20
Bumps [gitlab.com/gomidi/midi/v2](https://gitlab.com/gomidi/midi) from 2.3.18 to 2.3.20.
- [Commits](https://gitlab.com/gomidi/midi/compare/v2.3.18...v2.3.20)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 20:50:51 +00:00
dependabot[bot]
1f3a2b9076 Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go) from 1.28.0 to 1.40.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.28.0...v1.40.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
  dependency-version: 1.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 20:50:36 +00:00
Joel Wetzell
5dc897c4b2 allow contents write permission 2026-02-09 14:49:44 -06:00
Joel Wetzell
7015a9d7a2 move tests to sub folders 2026-02-09 14:38:13 -06:00
Joel Wetzell
3c6d98638e add coverage report step to test workflow 2026-02-09 14:33:20 -06:00
Joel Wetzell
8ec8ad91a6 Merge pull request #58 from jwetzell/fix/sip-server-postwait
fix postwait on sip server for dtmf response
2026-02-09 12:05:44 -06:00
Joel Wetzell
f919017ca6 fix postwait on sip server for dtmf response 2026-02-09 12:03:54 -06:00
Joel Wetzell
66f170047a remove arm images for now 2026-02-08 22:02:33 -06:00
Joel Wetzell
7ff20e0136 add more tests for string.split 2026-02-08 21:50:31 -06:00
Joel Wetzell
b53494fddc add more error tests for string.filter 2026-02-08 21:49:30 -06:00
Joel Wetzell
3688510ded fix test names 2026-02-08 21:49:12 -06:00
Joel Wetzell
d0bdc44c30 add test for string.filter 2026-02-08 21:40:53 -06:00
Joel Wetzell
1c49548a4c test cleanup 2026-02-08 21:38:44 -06:00
Joel Wetzell
4ea6c8f11f test cleanup 2026-02-08 21:31:08 -06:00
Joel Wetzell
0ef512eb28 add registry test for json.decode 2026-02-08 21:23:21 -06:00
Joel Wetzell
ebbce3261c add registry test for json.encode 2026-02-08 21:21:28 -06:00
Joel Wetzell
79fa925823 clean up json.encode test 2026-02-08 21:21:14 -06:00
Joel Wetzell
659c95102b Merge pull request #57 from jwetzell/feat/json-decode-processor
add json.decode processor
2026-02-08 21:15:55 -06:00
Joel Wetzell
e06c4195c7 add json.decode processor 2026-02-08 21:13:34 -06:00
Joel Wetzell
28e3c99ad8 add test loading from registry for time.sleep 2026-02-08 15:56:33 -06:00
Joel Wetzell
7312bfec20 add error tests for random processors 2026-02-08 15:56:14 -06:00
Joel Wetzell
4bbf69e644 add test for artnet.packet.filter 2026-02-08 15:55:48 -06:00
Joel Wetzell
1ba51446ad fix error messages in test 2026-02-08 15:55:32 -06:00
Joel Wetzell
2266d6a9b8 Merge pull request #56 from jwetzell/feat/json-schema
add json schema definitions for config
2026-02-08 15:04:25 -06:00
Joel Wetzell
f1f340f963 add schema for rest of processors 2026-02-08 13:34:40 -06:00
Joel Wetzell
4ca989280c switch interval and timer to new namespace 2026-02-08 09:45:22 -06:00
Joel Wetzell
ef83e84e84 add schema for time.sleep processor 2026-02-08 09:45:22 -06:00
Joel Wetzell
b8ad912855 add JSON schema for config file 2026-02-08 09:45:22 -06:00
Joel Wetzell
f766d0e2a4 Merge pull request #55 from jwetzell/feat/router-reload
reload router config on SIGHUP
2026-02-08 09:44:49 -06:00
Joel Wetzell
725e8ad670 add mutex for router operations 2026-02-07 17:07:18 -06:00
Joel Wetzell
7fa3b6d33d TODO 2026-02-07 16:29:49 -06:00
Joel Wetzell
5e957d7d03 expose running config from router 2026-02-07 16:28:06 -06:00
Joel Wetzell
bd89df3da2 create app struct 2026-02-07 14:15:24 -06:00
Joel Wetzell
914f6aa833 mess around with being able to reload router 2026-02-07 13:42:55 -06:00
Joel Wetzell
2104d9f1a9 change to start/stop not run/stop 2026-02-07 12:51:01 -06:00
Joel Wetzell
457ff83c23 add more convenience methods for start/stopping modules on router 2026-02-07 12:48:27 -06:00
Joel Wetzell
37c7a23063 add debug log at beginning of running 2026-02-07 12:40:30 -06:00
Joel Wetzell
9ba3a88e8c add removeModule function to router 2026-02-07 09:53:48 -06:00
Joel Wetzell
33ecc94097 add Stop function to module 2026-02-07 09:53:38 -06:00
Joel Wetzell
8f5091cf9b load router from context in mock module 2026-02-05 20:20:00 -06:00
Joel Wetzell
b095419b6e switch module instances to map on id 2026-02-05 20:02:03 -06:00
Joel Wetzell
1e872ba0b9 fix RouteIO interface 2026-02-05 19:51:09 -06:00
Joel Wetzell
b959b88527 split out module and route creation in router and router output only does one module 2026-02-05 19:31:31 -06:00
Joel Wetzell
0dfbf30fa7 Merge pull request #54 from jwetzell/feat/opentelemetry
add option to export open telemetry tracing
2026-02-04 19:22:15 -06:00
Joel Wetzell
abff674a75 fix router test 2026-02-04 19:17:57 -06:00
Joel Wetzell
a920f34926 rework route and processing spans 2026-02-04 19:01:18 -06:00
Joel Wetzell
4dac1faae0 add basic opentelemetry 2026-02-04 19:01:18 -06:00
Joel Wetzell
ab5b40ee72 remove osc.message.transform 2026-02-04 18:39:16 -06:00
Joel Wetzell
5a29139e63 Merge pull request #53 from jwetzell/chore/testing
add more testing
2026-02-04 18:38:18 -06:00
Joel Wetzell
1587f84981 add test for uint.random processor 2026-02-04 18:30:51 -06:00
Joel Wetzell
0cb13a30f9 add test for int.random processor 2026-02-04 18:30:41 -06:00
Joel Wetzell
8212d15a70 add test for module with no id 2026-02-04 18:30:25 -06:00
Joel Wetzell
eee356225f Merge pull request #52 from jwetzell/fix/debug-log-payload-string
more compatible printing of payload in debug.log
2026-02-03 22:52:20 -06:00
Joel Wetzell
5cd02f9e3a more compatible printing of payload in debug.log 2026-02-03 22:50:30 -06:00
Joel Wetzell
100edab869 Merge pull request #51 from jwetzell/fix/no-default-framing-method
don't default framing method
2026-02-03 22:28:47 -06:00
Joel Wetzell
fd4c2cb59b make framing parameter required in net.tcp.client 2026-02-03 22:27:58 -06:00
Joel Wetzell
b6ee3c68f1 don't default framing method 2026-02-03 22:26:16 -06:00
Joel Wetzell
7e0c2b2560 Merge pull request #48 from jwetzell/dependabot/github_actions/docker/login-action-3.7.0
Bump docker/login-action from 3.6.0 to 3.7.0
2026-02-03 11:35:37 -06:00
Joel Wetzell
c9a3eb80bc Merge pull request #50 from jwetzell/dependabot/go_modules/github.com/emiago/diago-0.26.2
Bump github.com/emiago/diago from 0.25.0 to 0.26.2
2026-02-03 11:35:13 -06:00
dependabot[bot]
2a5feec07a Bump github.com/emiago/diago from 0.25.0 to 0.26.2
Bumps [github.com/emiago/diago](https://github.com/emiago/diago) from 0.25.0 to 0.26.2.
- [Release notes](https://github.com/emiago/diago/releases)
- [Commits](https://github.com/emiago/diago/compare/v0.25.0...v0.26.2)

---
updated-dependencies:
- dependency-name: github.com/emiago/diago
  dependency-version: 0.26.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 23:54:12 +00:00
dependabot[bot]
ced4bc839e Bump docker/login-action from 3.6.0 to 3.7.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](5e57cd1181...c94ce9fb46)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 23:53:47 +00:00
Joel Wetzell
ce6a9ff4a8 update labeler 2026-02-02 13:14:11 -06:00
Joel Wetzell
bd3b8a19d5 Merge pull request #47 from jwetzell/fix/module-id-required
add check to prevent empty module Id
2026-02-02 13:00:36 -06:00
Joel Wetzell
cb738a9b8e combine route and router labels 2026-02-02 12:59:50 -06:00
Joel Wetzell
1b59dbc1e5 add router to labeler 2026-02-02 12:55:23 -06:00
Joel Wetzell
c75a2327d5 add check to prevent empty module Id 2026-02-02 12:51:56 -06:00
Joel Wetzell
a17be985e6 todo 2026-02-01 20:52:58 -06:00
Joel Wetzell
78737f57af Merge pull request #46 from jwetzell/chore/upgrade-artnet
update artnet dependency
2026-01-31 19:32:12 -06:00
Joel Wetzell
720c0cd52a update artnet dependency 2026-01-31 19:29:30 -06:00
Joel Wetzell
3e1d4f07f9 Merge pull request #43 from jwetzell/dependabot/go_modules/github.com/emiago/diago-0.25.0
Bump github.com/emiago/diago from 0.24.0 to 0.25.0
2026-01-20 11:47:43 -06:00
dependabot[bot]
452e308f8f Bump github.com/emiago/diago from 0.24.0 to 0.25.0
Bumps [github.com/emiago/diago](https://github.com/emiago/diago) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/emiago/diago/releases)
- [Commits](https://github.com/emiago/diago/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: github.com/emiago/diago
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 17:38:15 +00:00
Joel Wetzell
d16d833520 Merge pull request #44 from jwetzell/dependabot/go_modules/github.com/emiago/sipgo-1.1.1
Bump github.com/emiago/sipgo from 1.1.0 to 1.1.1
2026-01-20 11:37:02 -06:00
Joel Wetzell
f203b32261 Merge pull request #45 from jwetzell/dependabot/go_modules/github.com/urfave/cli/v3-3.6.2
Bump github.com/urfave/cli/v3 from 3.6.1 to 3.6.2
2026-01-20 11:36:51 -06:00
dependabot[bot]
361c4f4499 Bump github.com/urfave/cli/v3 from 3.6.1 to 3.6.2
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.6.1 to 3.6.2.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.6.1...v3.6.2)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 19:15:13 +00:00
dependabot[bot]
a76d12d799 Bump github.com/emiago/sipgo from 1.1.0 to 1.1.1
Bumps [github.com/emiago/sipgo](https://github.com/emiago/sipgo) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/emiago/sipgo/releases)
- [Commits](https://github.com/emiago/sipgo/compare/v1.1.0...v1.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 19:14:56 +00:00
Joel Wetzell
7d91e64ec4 add complete test coverage for string processors 2026-01-17 20:42:37 -06:00
Joel Wetzell
589c59c693 fix label for cli 2026-01-17 20:02:50 -06:00
Joel Wetzell
56c67c5445 Merge pull request #42 from jwetzell/feat/nats-message-create-subject-template
allow subject to be a template in nats.message.create
2026-01-17 19:31:53 -06:00
Joel Wetzell
13229215a6 allow subject to be a template in nats.message.create 2026-01-17 19:27:40 -06:00
Joel Wetzell
909b4d2337 add TODO 2026-01-17 19:24:21 -06:00
Joel Wetzell
3bcb9a6608 Merge pull request #41 from jwetzell/fix/dtmf-create-rune-check
return error if DTMF create results in invalid characters
2026-01-17 18:40:53 -06:00
Joel Wetzell
17dcffc09b return error if dtmf create results in invalid characters 2026-01-17 18:39:54 -06:00
Joel Wetzell
8318645eef Merge pull request #40 from jwetzell/chore/router-context-handling
move router context to run methods
2026-01-16 19:04:21 -06:00
Joel Wetzell
0c777a8874 move router context to run methods 2026-01-16 18:50:40 -06:00
Joel Wetzell
a8f9894ff0 Merge pull request #39 from jwetzell/feat/wasm-module
add WASM processor
2026-01-14 17:56:33 -06:00
Joel Wetzell
f0f74f50e5 allow specifying function name to call 2026-01-14 17:53:37 -06:00
Joel Wetzell
bb3a8b982b add script.wasm via extism plugins 2026-01-14 17:50:16 -06:00
Joel Wetzell
be9ce5a8aa update to versioned artnet library 2026-01-14 17:35:31 -06:00
Joel Wetzell
ac2cf078a4 Remove debug print statement from SIPCallServer 2026-01-14 09:38:57 -06:00
Joel Wetzell
0fc264d0e9 Merge pull request #38 from jwetzell/dependabot/go_modules/github.com/emiago/sipgo-1.0.2
Bump github.com/emiago/sipgo from 1.0.1 to 1.0.2
2026-01-14 09:02:28 -06:00
dependabot[bot]
3c99eef999 Bump github.com/emiago/sipgo from 1.0.1 to 1.0.2
Bumps [github.com/emiago/sipgo](https://github.com/emiago/sipgo) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/emiago/sipgo/releases)
- [Commits](https://github.com/emiago/sipgo/compare/v1.0.1...v1.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-14 15:01:01 +00:00
Joel Wetzell
f7c3a85649 Merge pull request #37 from jwetzell/dependabot/go_modules/github.com/emiago/diago-0.24.0
Bump github.com/emiago/diago from 0.23.1-0.20251211215055-e1d875617111 to 0.24.0
2026-01-14 08:59:30 -06:00
dependabot[bot]
76caccd9f8 Bump github.com/emiago/diago
Bumps [github.com/emiago/diago](https://github.com/emiago/diago) from 0.23.1-0.20251211215055-e1d875617111 to 0.24.0.
- [Release notes](https://github.com/emiago/diago/releases)
- [Commits](https://github.com/emiago/diago/commits/v0.24.0)

---
updated-dependencies:
- dependency-name: github.com/emiago/diago
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 21:31:51 +00:00
Joel Wetzell
e8c71562a6 update artnet-go 2025-12-30 11:58:48 -06:00
Joel Wetzell
42551127ba Merge pull request #36 from jwetzell/dependabot/go_modules/modernc.org/quickjs-0.17.1
Bump modernc.org/quickjs from 0.17.0 to 0.17.1
2025-12-29 16:11:25 -06:00
Joel Wetzell
a99fef9389 Merge pull request #35 from jwetzell/dependabot/github_actions/docker/setup-buildx-action-3.12.0
Bump docker/setup-buildx-action from 3.11.1 to 3.12.0
2025-12-29 16:11:16 -06:00
Joel Wetzell
e6099b6acd update cli usage in README 2025-12-29 16:09:24 -06:00
Joel Wetzell
b93d754c19 fix usage descriptions 2025-12-29 16:08:52 -06:00
Joel Wetzell
710dcf3a02 change logging flags 2025-12-29 16:07:56 -06:00
dependabot[bot]
ac83b339a6 Bump modernc.org/quickjs from 0.17.0 to 0.17.1
Bumps [modernc.org/quickjs](https://gitlab.com/cznic/quickjs) from 0.17.0 to 0.17.1.
- [Commits](https://gitlab.com/cznic/quickjs/compare/v0.17.0...v0.17.1)

---
updated-dependencies:
- dependency-name: modernc.org/quickjs
  dependency-version: 0.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 17:28:41 +00:00
dependabot[bot]
cb06e2dbb5 Bump docker/setup-buildx-action from 3.11.1 to 3.12.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.11.1 to 3.12.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](e468171a9d...8d2750c68a)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 3.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 17:28:19 +00:00
Joel Wetzell
3ef41d0026 Merge pull request #34 from jwetzell/feat/sip-response
support responding to sip dtmf/audio calls
2025-12-28 20:45:51 -06:00
Joel Wetzell
8dcb70bfee Merge pull request #33 from jwetzell/fix/route-concurrency
process routes concurrently
2025-12-28 20:44:03 -06:00
Joel Wetzell
59f00c1a32 process routes concurrently 2025-12-28 20:42:11 -06:00
Joel Wetzell
6e88d259b8 support for responding with dtmf or an audio file to both sip call types 2025-12-28 20:40:09 -06:00
Joel Wetzell
bb33974e1c start messing with controlling response to SIP calls 2025-12-28 19:29:38 -06:00
Joel Wetzell
bd2a68ff6e test a couple error router scenarios 2025-12-28 18:17:23 -06:00
Joel Wetzell
7e2d76ef3a add test for router creation and basic input output 2025-12-28 18:09:51 -06:00
Joel Wetzell
d1cec1e094 Merge pull request #32 from jwetzell/feat/json-encode
add processor to encode json bytes
2025-12-28 16:27:12 -06:00
Joel Wetzell
477d70fad0 remove last byte in json.encode because of new line 2025-12-28 16:26:11 -06:00
Joel Wetzell
70f4636522 add test for json.encode 2025-12-28 16:25:57 -06:00
Joel Wetzell
0248ca6973 add processor to encode json bytes 2025-12-28 16:17:56 -06:00
Joel Wetzell
4aa586427b change body field name 2025-12-28 13:39:01 -06:00
Joel Wetzell
6d3cf6692f fix route output with nil payload 2025-12-28 12:42:20 -06:00
Joel Wetzell
a263b10690 Merge pull request #31 from jwetzell/feat/http-server-output
support basic http server response control with body string template
2025-12-28 12:41:45 -06:00
Joel Wetzell
3ce2909b0f check that response writer has not been written too already 2025-12-28 12:32:22 -06:00
Joel Wetzell
b15e282d59 support basic http server response control with body string template for now 2025-12-28 12:25:25 -06:00
Joel Wetzell
f97f9b9fc9 propagate a ctx all the way from input to output of a route 2025-12-28 12:21:58 -06:00
Joel Wetzell
12de947f3d cleanup logging 2025-12-28 11:47:02 -06:00
Joel Wetzell
7335ba973a Merge pull request #30 from jwetzell/fix/multi-out-errors
fix error handling/short-circuiting in multi route matching
2025-12-28 11:40:34 -06:00
Joel Wetzell
a994286402 fix error handling/short-circuiting in multi route matching 2025-12-28 11:30:37 -06:00
Joel Wetzell
b639a5c786 Merge pull request #29 from jwetzell/feat/int-random
add processors to create random int/uint
2025-12-28 09:06:12 -06:00
Joel Wetzell
d170958d9b add processors to create random int/uint 2025-12-28 09:05:01 -06:00
Joel Wetzell
ed4c14e82b rework route to just process payload 2025-12-28 08:24:12 -06:00
Joel Wetzell
fcb1378784 fix route test 2025-12-27 23:02:37 -06:00
Joel Wetzell
97f4cbeace fix filename 2025-12-27 22:59:50 -06:00
Joel Wetzell
8ffc7d02a5 stuff values into context 2025-12-27 22:59:30 -06:00
Joel Wetzell
3458b52206 Merge pull request #28 from jwetzell/feat/artnet-module
add artnet module and processors
2025-12-26 11:59:59 -06:00
Joel Wetzell
e12b6e098e add artnet packet filter 2025-12-26 11:51:22 -06:00
Joel Wetzell
58cb7766fe change namespace 2025-12-26 11:51:22 -06:00
Joel Wetzell
407f1f3618 add basic artnet decode/encode 2025-12-26 11:51:22 -06:00
Joel Wetzell
3c0f555a6f Merge pull request #27 from jwetzell/chore/time-namespace
move timer and interval to time namespace
2025-12-26 11:18:44 -06:00
Joel Wetzell
6c6e50e0eb move timer and interval to time namespace 2025-12-26 11:17:29 -06:00
Joel Wetzell
0ed4d6669f don't use default mux 2025-12-26 11:01:30 -06:00
Joel Wetzell
cc37559bde use router.Stop 2025-12-26 10:43:04 -06:00
Joel Wetzell
18b307bca1 control flow of router running with better context handling and logging in cmd 2025-12-26 10:39:09 -06:00
Joel Wetzell
17106b5096 add more debug logging to router 2025-12-26 10:38:33 -06:00
Joel Wetzell
df9ffdda9e add debug log about waiting for modules to close 2025-12-26 10:24:31 -06:00
Joel Wetzell
479cf41ea0 use WaitGroup Go function 2025-12-26 10:24:15 -06:00
Joel Wetzell
af833cb212 cleanup debug.log 2025-12-26 10:23:39 -06:00
Joel Wetzell
a6a8e275ee remove unecessary logger 2025-12-26 10:23:33 -06:00
Joel Wetzell
6968539c59 cleanup logging in router 2025-12-26 10:19:29 -06:00
Joel Wetzell
e96913230b logging cleanup 2025-12-26 10:08:24 -06:00
Joel Wetzell
f1dff33704 centralize module logger creation 2025-12-26 09:51:55 -06:00
Joel Wetzell
ff426994e4 add tests for registration and error cases to script.expr 2025-12-24 20:53:46 -06:00
Joel Wetzell
a1275b3b69 add tests for error scenarios to script.js 2025-12-24 20:53:25 -06:00
Joel Wetzell
227d042feb add bitsize and registration tests for float.parse 2025-12-24 20:32:42 -06:00
Joel Wetzell
e53515267e tests for debug.log 2025-12-24 20:32:31 -06:00
Joel Wetzell
502586dec6 add base and bitsize to int.parse tests 2025-12-24 20:25:59 -06:00
Joel Wetzell
cad3714664 add base and bitsize to bad tests 2025-12-24 20:25:46 -06:00
Joel Wetzell
cf6519b594 more tests for script.js 2025-12-24 20:16:02 -06:00
Joel Wetzell
7dcac9470a tests for general processor stuff 2025-12-24 20:15:46 -06:00
Joel Wetzell
e5268f42f8 add more tests for int/uint parsers 2025-12-24 20:15:29 -06:00
Joel Wetzell
ccff105e37 test error cleanup 2025-12-24 19:45:24 -06:00
Joel Wetzell
6611821155 add tests for route 2025-12-24 19:16:52 -06:00
Joel Wetzell
7cde1c244a cleanup route interface 2025-12-24 19:16:46 -06:00
Joel Wetzell
70878e6df6 Merge pull request #26 from jwetzell/fix/slip-framer
fix incorrect slip frame encoding
2025-12-24 19:15:17 -06:00
Joel Wetzell
bd005da358 fix incorrect slip frame encoding 2025-12-24 19:13:33 -06:00
Joel Wetzell
25f2ec30c2 rework framer tests 2025-12-24 19:13:04 -06:00
Joel Wetzell
e679bf2b46 add test for string.create 2025-12-24 18:18:08 -06:00
Joel Wetzell
460d2f051d switch separator test to use GetFramer 2025-12-24 18:17:57 -06:00
Joel Wetzell
efdcbae5c4 no need for framer to return two values 2025-12-24 18:17:41 -06:00
Joel Wetzell
076a13f48a Merge pull request #25 from jwetzell/feat/sleep-processor
add a processor that sleeps
2025-12-24 16:33:41 -06:00
Joel Wetzell
971eea6e41 add a processor that sleeps 2025-12-24 16:32:44 -06:00
Joel Wetzell
1172159455 align variable names with struct name 2025-12-24 16:06:01 -06:00
Joel Wetzell
ecb415f321 add basic test for raw framer 2025-12-24 16:02:23 -06:00
Joel Wetzell
b552672011 update README 2025-12-24 15:18:32 -06:00
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
179 changed files with 15102 additions and 2492 deletions

5
.dockerignore Normal file
View File

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

View File

@@ -1,9 +1,9 @@
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: 'weekly'
interval: "weekly"
- package-ecosystem: gomod
directory: /
schedule:

16
.github/labeler.yml vendored
View File

@@ -1,23 +1,27 @@
config:
- changed-files:
- any-glob-to-any-file: 'internal/config/**'
- any-glob-to-any-file: "internal/config/**"
framer:
- changed-files:
- any-glob-to-any-file: 'internal/framer/**'
- any-glob-to-any-file: "internal/framer/**"
module:
- changed-files:
- any-glob-to-any-file: 'internal/module/**'
- any-glob-to-any-file: "internal/module/**"
processor:
- changed-files:
- any-glob-to-any-file: 'internal/processor/**'
- any-glob-to-any-file: "internal/processor/**"
router:
- changed-files:
- any-glob-to-any-file: "router*"
route:
- changed-files:
- any-glob-to-any-file: 'internal/route/**'
- any-glob-to-any-file: "internal/route/**"
cli:
- changed-files:
- any-glob-to-any-file: 'cmd/showbridge/**'
- any-glob-to-any-file: "cmd/showbridge/**"

7
.github/release.yml vendored
View File

@@ -17,12 +17,13 @@ changelog:
- title: Processor 🏭
labels:
- processor
- title: Route 🛣️
- title: Routing 🛣️
labels:
- router
- route
- title: CLI ⌨️
labels:
- cmd
- cli
- title: Other Changes
labels:
- '*'
- "*"

View File

@@ -3,7 +3,7 @@ name: showbridge release
on:
push:
tags:
- "*"
- "v*"
permissions:
contents: write
@@ -19,9 +19,9 @@ jobs:
- name: setup go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
go-version-file: "go.mod"
- name: release
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
workdir: cmd/showbridge
distribution: goreleaser
@@ -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@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to Docker Hub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup Docker metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: |
jwetzell/showbridge
- name: Build and push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.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

View File

@@ -5,19 +5,19 @@ on:
branches:
- main
paths:
- '**/*.go'
- 'go.mod'
- 'go.sum'
- "**/*.go"
- "go.mod"
- "go.sum"
push:
branches:
- main
paths:
- '**/*.go'
- 'go.mod'
- 'go.sum'
- "**/*.go"
- "go.mod"
- "go.sum"
permissions:
contents: read
contents: write
jobs:
test:
@@ -29,9 +29,21 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
go-version-file: "go.mod"
- name: mod tidy
run: go mod tidy
- name: run tests
run: go test ./...
run: go test ./... -count=1 -coverpkg "github.com/jwetzell/showbridge-go/..." -coverprofile coverage.out
- name: clean up coverage report
run: cat coverage.out | grep -v "github.com/jwetzell/showbridge-go/cmd" > coverage.nocmd.out
- name: Update coverage report
uses: ncruces/go-coverage-report@v0.3.2
with:
coverage-file: coverage.nocmd.out
report: true
chart: true
amend: true
if: github.event_name == 'push'
continue-on-error: true

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

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

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
ARG GO_VERSION=1.26.0
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

@@ -1,29 +1,44 @@
<div align="center">
# showbridge (go edition)
# showbridge (go edition)
[![Coverage](https://github.com/jwetzell/showbridge-go/wiki/coverage.svg)](https://raw.githack.com/wiki/jwetzell/showbridge-go/coverage.html)
Simple protocol router _/s_
</div>
<p align="center">
<a href="https://github.com/jwetzell/showbridge-go/releases">Releases</a> ·
<a href="https://docs.showbridge.io">Documentation</a>
</p>
### Supported Protocols
- HTTP
- client
- server
- UDP
- client
- server
- TCP
- client
- server
- [MQTT](https://mqtt.org/)
- client
- [NATS](https://nats.io/)
- client
- [PosiStageNet](https://posistage.net/)
- client
- MIDI
- client (not included in pre-built binaries yet)
- MIDI (not included in pre-built binaries yet)
- Serial (not included in pre-built binaries yet)
- [OSC](https://opensoundcontrol.stanford.edu/spec-1_0.html)
- [FreeD](https://ptzoptics.com/freed/)
- [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol)
### CLI Usage
```
NAME:
showbridge - Simple protocol router /s
USAGE:
showbridge [global options]
GLOBAL OPTIONS:
--config string path to config file (default: "./config.yaml")
--log-level string set log level (default: "info")
--log-format string log format to use (default: "text")
--help, -h show help
--version, -v print the version
```

146
api.go Normal file
View File

@@ -0,0 +1,146 @@
package showbridge
import (
"context"
"embed"
_ "embed"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
)
func (r *Router) startAPIServer(config config.ApiConfig) {
r.logger.Debug("starting API server", "port", config.Port)
mux := http.NewServeMux()
mux.HandleFunc("/ws", r.handleWebsocket)
mux.HandleFunc("/health", r.handleHealthHTTP)
mux.HandleFunc("/schema/{schema}", r.handleSchemaHTTP)
mux.HandleFunc("/api/v1/config", r.handleConfigHTTP)
r.apiServerMu.Lock()
defer r.apiServerMu.Unlock()
r.apiServer = &http.Server{
Addr: fmt.Sprintf(":%d", config.Port),
Handler: mux,
}
go func() {
r.apiServer.ListenAndServe()
r.apiServerShutdown()
}()
}
func (r *Router) stopAPIServer() {
r.logger.Debug("stopping API server")
r.apiServerMu.Lock()
defer r.apiServerMu.Unlock()
if r.apiServer != nil {
apiShutdownCtx, apiShutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
r.apiServerShutdown = apiShutdownCancel
r.apiServer.Shutdown(apiShutdownCtx)
<-apiShutdownCtx.Done()
r.apiServer = nil
}
}
func (r *Router) handleHealthHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusOK)
case http.MethodOptions:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusOK)
default:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (r *Router) handleConfigHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
configJSON, err := json.Marshal(r.runningConfig)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
w.Write(configJSON)
case http.MethodPut:
var newConfig config.Config
err := json.NewDecoder(req.Body).Decode(&newConfig)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
moduleErrors, routeErrors := r.UpdateConfig(newConfig)
if len(moduleErrors) > 0 || len(routeErrors) > 0 {
errorResponse := struct {
ModuleErrors []module.ModuleError `json:"moduleErrors,omitempty"`
RouteErrors []route.RouteError `json:"routeErrors,omitempty"`
}{
ModuleErrors: moduleErrors,
RouteErrors: routeErrors,
}
errorResponseJSON, err := json.Marshal(errorResponse)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
w.Write(errorResponseJSON)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusOK)
r.ConfigChange <- newConfig
case http.MethodOptions:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusOK)
default:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
//go:embed schema
var schema embed.FS
func (r *Router) handleSchemaHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
schemaName := req.PathValue("schema")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
configSchema, err := schema.ReadFile(fmt.Sprintf("schema/%s.schema.json", schemaName))
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Write(configSchema)
case http.MethodOptions:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusOK)
default:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusMethodNotAllowed)
}
}

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,50 +2,80 @@ package main
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"os/signal"
"slices"
"sync"
"syscall"
"github.com/jwetzell/showbridge-go"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/urfave/cli/v3"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"sigs.k8s.io/yaml"
)
var (
version = "dev"
sigHangup = make(chan os.Signal, 1)
)
func main() {
cmd := &cli.Command{
Name: "showbridge",
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.StringFlag{
Name: "log-level",
Value: "info",
Usage: "set log level",
Validator: func(level string) error {
levels := []string{"debug", "info", "warn", "error"}
if !slices.Contains(levels, level) {
return fmt.Errorf("unknown log level: %s", level)
}
return nil
},
},
&cli.StringFlag{
Name: "log-format",
Value: "text",
Usage: "log format to use",
Validator: func(format string) error {
formats := []string{"text", "json"}
if !slices.Contains(formats, format) {
return fmt.Errorf("unknown log format: %s", format)
}
return nil
},
},
&cli.BoolFlag{
Name: "trace",
Value: false,
Usage: "enable OpenTelemetry tracing",
},
},
Action: func(ctx context.Context, c *cli.Command) error {
configPath := c.String("config")
if configPath == "" {
return fmt.Errorf("config value cannot be empty")
}
config, err := readConfig(configPath)
if err != nil {
return err
}
router, moduleErrors, routeErrors := showbridge.NewRouter(ctx, config)
for _, moduleError := range moduleErrors {
slog.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)
}
router.Run()
return nil
},
Action: run,
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Interrupt)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
signal.Notify(sigHangup, syscall.SIGHUP)
defer cancel()
err := cmd.Run(ctx, os.Args)
@@ -55,6 +85,15 @@ func main() {
}
type showbridgeApp struct {
ctx context.Context
configPath string
logger *slog.Logger
router *showbridge.Router
routerRunner *sync.WaitGroup
routerMutex sync.Mutex
}
func readConfig(configPath string) (config.Config, error) {
cfg := config.Config{}
@@ -71,3 +110,175 @@ func readConfig(configPath string) (config.Config, error) {
return cfg, nil
}
func writeConfig(configPath string, newConfig config.Config) error {
configBytes, err := yaml.Marshal(newConfig)
if err != nil {
return err
}
err = os.WriteFile(configPath, configBytes, 0644)
if err != nil {
return err
}
return nil
}
func run(ctx context.Context, c *cli.Command) error {
configPath := c.String("config")
if configPath == "" {
return errors.New("config path cannot be empty")
}
logLevel := slog.LevelInfo
logLevelFromFlag := c.String("log-level")
switch logLevelFromFlag {
case "debug":
logLevel = slog.LevelDebug
case "info":
logLevel = slog.LevelInfo
case "warn":
logLevel = slog.LevelWarn
case "error":
logLevel = slog.LevelError
default:
logLevel = slog.LevelInfo
}
logHandlerOptions := &slog.HandlerOptions{
Level: logLevel,
}
logOutput := os.Stderr
var logHandler slog.Handler
logFormat := c.String("log-format")
switch logFormat {
case "json":
logHandler = slog.NewJSONHandler(logOutput, logHandlerOptions)
case "text":
logHandler = slog.NewTextHandler(logOutput, logHandlerOptions)
default:
logHandler = slog.NewTextHandler(logOutput, logHandlerOptions)
}
slog.SetDefault(slog.New(logHandler))
if c.Bool("trace") {
exporter, err := otlptracehttp.New(ctx)
if err != nil {
return fmt.Errorf("failed to create trace exporter: %w", err)
}
tracerProvider := newTracerProvider(exporter)
otel.SetTracerProvider(tracerProvider)
defer tracerProvider.Shutdown(ctx)
otel.SetTracerProvider(tracerProvider)
}
showbridgeApp := &showbridgeApp{
ctx: ctx,
configPath: configPath,
logger: slog.Default().With("component", "cmd"),
routerRunner: &sync.WaitGroup{},
}
config, err := readConfig(showbridgeApp.configPath)
if err != nil {
return err
}
router, moduleErrors, routeErrors := showbridge.NewRouter(config)
showbridgeApp.logConfigErrors(moduleErrors, routeErrors)
if moduleErrors != nil || routeErrors != nil {
return fmt.Errorf("errors initializing modules or routes")
}
if err != nil {
return fmt.Errorf("failed to initialize router: %w", err)
}
showbridgeApp.routerMutex.Lock()
showbridgeApp.router = router
showbridgeApp.routerRunner.Go(func() {
router.Start(context.Background())
})
showbridgeApp.routerMutex.Unlock()
go showbridgeApp.handleChannels()
<-showbridgeApp.ctx.Done()
showbridgeApp.logger.Debug("shutting down router")
showbridgeApp.router.Stop()
showbridgeApp.logger.Debug("waiting for router to exit")
showbridgeApp.routerRunner.Wait()
return nil
}
func (app *showbridgeApp) handleChannels() {
for {
select {
case <-sigHangup:
app.logger.Info("received SIGHUP, reloading configuration")
app.routerMutex.Lock()
config, err := readConfig(app.configPath)
if err != nil {
app.logger.Error("failed to read config file", "error", err)
app.routerMutex.Unlock()
continue
}
moduleErrors, routeErrors := app.router.UpdateConfig(config)
app.logConfigErrors(moduleErrors, routeErrors)
app.logger.Info("configuration reloaded successfully")
app.routerMutex.Unlock()
case config := <-app.router.ConfigChange:
app.logger.Info("router config changed updating config file")
err := writeConfig(app.configPath, config)
if err != nil {
app.logger.Error("failed to write config file", "error", err)
continue
}
app.logger.Info("config file updated successfully")
case <-app.ctx.Done():
return
}
}
}
func (app *showbridgeApp) logConfigErrors(moduleErrors []module.ModuleError, routeErrors []route.RouteError) {
for _, moduleError := range moduleErrors {
app.logger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error)
}
for _, routeError := range routeErrors {
app.logger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error)
}
}
func newTracerProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
r, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("showbridge"),
semconv.ServiceVersion(version),
),
)
if err != nil {
panic(err)
}
return sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(r),
)
}

View File

@@ -1,6 +1,8 @@
api:
port: 8080
modules:
- id: http
type: net.http.server
type: http.server
params:
port: 3000
- id: udp
@@ -13,6 +15,8 @@ routes:
processors:
- type: osc.message.create
params:
address: "{{.URL.Path}}"
address: "{{.Payload.URL.Path}}"
- type: osc.message.encode
output: udp
- type: router.output
params:
module: udp

64
events.go Normal file
View File

@@ -0,0 +1,64 @@
package showbridge
import (
"encoding/json"
"github.com/gorilla/websocket"
)
type Event struct {
Type string `json:"type"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func (e Event) toJSON() ([]byte, error) {
return json.Marshal(e)
}
func (r *Router) handleEvent(event Event, sender *websocket.Conn) {
switch event.Type {
case "ping":
r.unicastEvent(Event{Type: "pong"}, sender)
default:
r.logger.Warn("unknown event type", "eventType", event.Type)
}
}
func (r *Router) unicastEvent(event Event, conn *websocket.Conn) {
eventJSON, err := event.toJSON()
if err != nil {
r.logger.Error("failed to marshal event to JSON", "error", err)
return
}
err = conn.WriteMessage(websocket.TextMessage, eventJSON)
if err != nil {
r.logger.Error("failed to write message to websocket connection", "error", err)
}
}
func (r *Router) broadcastEvent(event Event, excluded ...*websocket.Conn) {
eventJSON, err := event.toJSON()
if err != nil {
r.logger.Error("failed to marshal event to JSON", "error", err)
return
}
r.wsConnsMu.Lock()
defer r.wsConnsMu.Unlock()
for _, conn := range r.wsConns {
exclude := false
for _, excludedConn := range excluded {
if conn == excludedConn {
exclude = true
break
}
}
if exclude {
continue
}
err := conn.WriteMessage(websocket.TextMessage, eventJSON)
if err != nil {
r.logger.Error("failed to write message to websocket connection", "error", err)
}
}
}

84
go.mod
View File

@@ -1,40 +1,88 @@
module github.com/jwetzell/showbridge-go
go 1.25.3
go 1.26.0
require (
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/expr-lang/expr v1.17.6
github.com/emiago/diago v0.27.0
github.com/emiago/sipgo v1.2.1
github.com/expr-lang/expr v1.17.8
github.com/extism/go-sdk v1.7.1
github.com/gorilla/websocket v1.5.3
github.com/jwetzell/artnet-go v0.2.1
github.com/jwetzell/free-d-go v0.1.0
github.com/jwetzell/osc-go v0.1.0
github.com/jwetzell/osc-go v0.2.0
github.com/jwetzell/psn-go v0.3.0
github.com/nats-io/nats.go v1.47.0
github.com/urfave/cli/v3 v3.6.1
gitlab.com/gomidi/midi/v2 v2.3.16
github.com/nats-io/nats-server/v2 v2.12.5
github.com/nats-io/nats.go v1.49.0
github.com/urfave/cli/v3 v3.7.0
gitlab.com/gomidi/midi/v2 v2.3.23
go.bug.st/serial v1.6.4
modernc.org/quickjs v0.17.0
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
go.opentelemetry.io/otel/sdk v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
modernc.org/quickjs v0.17.1
sigs.k8s.io/yaml v1.6.0
)
require (
github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/creack/goselect v0.1.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 // indirect
github.com/go-audio/riff v1.0.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // 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/go-tpm v0.9.8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
github.com/nats-io/jwt/v2 v2.8.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ncruces/go-strftime v1.0.0 // 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/pion/transport/v4 v4.0.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/zaf/g711 v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.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
modernc.org/libc v1.66.10 // indirect
modernc.org/libquickjs v0.12.2 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.79.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect
modernc.org/libc v1.67.1 // indirect
modernc.org/libquickjs v0.12.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

197
go.sum
View File

@@ -1,74 +1,189 @@
github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE=
github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
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.27.0 h1:5SQBbcLR9ooxhSMlTkU9QSuDv/2nhMO9lxuWJTd/7rE=
github.com/emiago/diago v0.27.0/go.mod h1:8hUxCDPJY2p32hh+4ed7vHW/3yTMmEa3BjNctUPeGD0=
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 h1:o4LxpUnZ1zxiQ+Qjc9kLwXcjz31NGAHmnZ7xoJto3VM=
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0/go.mod h1:ydcZ977eS1I6uOWodzMuw30BwvNAzT9su/xcNYSJqjA=
github.com/emiago/sipgo v1.2.1 h1:5JTwogbe3yQFA3sjBVueN2Q4WTU350tGeBwPYT8HMv0=
github.com/emiago/sipgo v1.2.1/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
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/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
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/artnet-go v0.2.1 h1:iYTKWcwYrF5kBkYfkw2UbWvoueeA23iKEn7fR27mWZE=
github.com/jwetzell/artnet-go v0.2.1/go.mod h1:gli97Z32a0kMkZ6taoTiK7/lqHcF/dhiGjGJdx/PxqA=
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=
github.com/jwetzell/osc-go v0.1.0/go.mod h1:xLz0jTwebSxtx1TkKN1YVdeRqvpFNweDhTut5TE393A=
github.com/jwetzell/osc-go v0.2.0 h1:4as+BYCeZhEddFczGveP5yZZxvY728Uavz+ZSLZfOII=
github.com/jwetzell/osc-go v0.2.0/go.mod h1:D3ZIXYB12bt4S35lKFUqgCFbF1Y+9Ld0sOhHA9mGZZM=
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/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
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/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk=
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.12.5 h1:EOHLbsLJgUHUwzkj9gBTOlubkX+dmSs0EYWMdBiHivU=
github.com/nats-io/nats-server/v2 v2.12.5/go.mod h1:JQDAKcwdXs0NRhvYO31dzsXkzCyDkOBS7SKU3Nozu14=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/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/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
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.23 h1:P8NxV4EzV9c+BjpwTeB+G/qa+Xdq/UTazS2fKxY0O0g=
gitlab.com/gomidi/midi/v2 v2.3.23/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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
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=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
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/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=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
@@ -81,18 +196,18 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/libquickjs v0.12.2 h1:XtF6iD+aoN/pEz1MAQjg1wMZT412pzmOlO11UGLW/wQ=
modernc.org/libquickjs v0.12.2/go.mod h1:n+vyuJ4mXh1pQYt1bJttvoCE+2t+MqCM8BFlcSDg/70=
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/libquickjs v0.12.3 h1:2IU9B6njBmce2PuYttJDkXeoLRV9WnvgP+eU5HAC8YI=
modernc.org/libquickjs v0.12.3/go.mod h1:iCsgVxnHTX3i0YPxxHBmJk0GLA5sVUHXWI/090UXgeE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/quickjs v0.17.0 h1:gYZRSCKVokyOiHhZEKwran/NoBaGjAaWk+xJq52SWfQ=
modernc.org/quickjs v0.17.0/go.mod h1:TE+wpAYX4V7Nvi/sHRUrd9l5L176ZIiS0as3MMObvC0=
modernc.org/quickjs v0.17.1 h1:CbYnbTf7ksZk9YZ1rRM2Ab1Zfi+X6s50kXiOhpd2NIg=
modernc.org/quickjs v0.17.1/go.mod h1:hATT7DIJc33I5Q/Fjffhm0tpUHNSqdKHma/ossibTA0=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=

179
internal/common/common.go Normal file
View File

@@ -0,0 +1,179 @@
package common
import (
"math"
"reflect"
)
func GetAnyAs[T any](value any) (T, bool) {
typed, ok := value.(T)
return typed, ok
}
func GetAnyAsInt(value any) (int, bool) {
intValue, ok := value.(int)
if ok {
return intValue, true
}
uintValue, ok := value.(uint)
if ok {
return int(uintValue), true
}
byteValue, ok := value.(byte)
if ok {
return int(byteValue), true
}
floatValue, ok := value.(float64)
if ok {
if floatValue != math.Floor(floatValue) {
return 0, false
}
return int(floatValue), true
}
return 0, false
}
func GetAnyAsByteSlice(value any) ([]byte, bool) {
v := reflect.ValueOf(value)
if v.Kind() != reflect.Slice {
return nil, false
}
result := make([]byte, v.Len())
for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface()
byteValue, ok := elem.(byte)
if ok {
result[i] = byteValue
continue
}
uintValue, ok := elem.(uint)
if ok {
if uintValue > 255 {
return nil, false
}
result[i] = byte(uintValue)
continue
}
intValue, ok := elem.(int)
if ok {
if intValue < 0 || intValue > 255 {
return nil, false
}
result[i] = byte(intValue)
continue
}
floatValue, ok := elem.(float64)
if ok {
if floatValue != math.Floor(floatValue) {
return nil, false
}
if floatValue < 0 || floatValue > 255 {
return nil, false
}
result[i] = byte(floatValue)
continue
}
return nil, false
}
return result, true
}
func GetAnyAsIntSlice(value any) ([]int, bool) {
v := reflect.ValueOf(value)
if v.Kind() != reflect.Slice {
return nil, false
}
result := make([]int, v.Len())
for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface()
byteValue, ok := elem.(byte)
if ok {
result[i] = int(byteValue)
continue
}
uintValue, ok := elem.(uint)
if ok {
result[i] = int(uintValue)
continue
}
intValue, ok := elem.(int)
if ok {
result[i] = int(intValue)
continue
}
floatValue, ok := elem.(float64)
if ok {
if floatValue != math.Floor(floatValue) {
return nil, false
}
result[i] = int(floatValue)
continue
}
return nil, false
}
return result, true
}
func GetAnyAsFloat32(value any) (float32, bool) {
float32Value, ok := value.(float32)
if ok {
return float32Value, true
}
float64Value, ok := value.(float64)
if ok {
return float32(float64Value), true
}
intValue, ok := value.(int)
if ok {
return float32(intValue), true
}
uintValue, ok := value.(uint)
if ok {
return float32(uintValue), true
}
byteValue, ok := value.(byte)
if ok {
return float32(byteValue), true
}
return 0, false
}
func GetAnyAsFloat64(value any) (float64, bool) {
float64Value, ok := value.(float64)
if ok {
return float64Value, true
}
float32Value, ok := value.(float32)
if ok {
return float64(float32Value), true
}
intValue, ok := value.(int)
if ok {
return float64(intValue), true
}
uintValue, ok := value.(uint)
if ok {
return float64(uintValue), true
}
byteValue, ok := value.(byte)
if ok {
return float64(byteValue), true
}
return 0, false
}

View File

@@ -0,0 +1,8 @@
package common
type contextKey string
const RouterContextKey contextKey = contextKey("router")
const SourceContextKey contextKey = contextKey("source")
const ModulesContextKey contextKey = contextKey("modules")
const SenderContextKey contextKey = contextKey("sender")

View File

@@ -0,0 +1,15 @@
package common
import "context"
type RouteIO interface {
HandleInput(ctx context.Context, sourceId string, payload any) (bool, []RouteIOError)
HandleOutput(ctx context.Context, destinationId string, payload any) error
}
type RouteIOError struct {
Index int `json:"index"`
OutputError error `json:"outputError"`
ProcessError error `json:"processError"`
InputError error `json:"inputError"`
}

View File

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

View File

@@ -0,0 +1,297 @@
package config_test
import (
"encoding/json"
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
)
func TestGoodStringParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected string
}{
{
name: "string param",
paramsJSON: `{"key": "value"}`,
key: "key",
expected: "value",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetString(testCase.key)
if err != nil {
t.Fatalf("GetString returned error: %v", err)
}
if value != testCase.expected {
t.Fatalf("GetString got %s, expected %s", value, testCase.expected)
}
})
}
}
func TestGoodIntParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected int
}{
{
name: "int param",
paramsJSON: `{"key": 1}`,
key: "key",
expected: 1,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetInt(testCase.key)
if err != nil {
t.Fatalf("GetInt returned error: %v", err)
}
if value != testCase.expected {
t.Fatalf("GetInt got %d, expected %d", value, testCase.expected)
}
})
}
}
func TestGoodFloat32ParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected float32
}{
{
name: "no decimal param",
paramsJSON: `{"key": 1}`,
key: "key",
expected: 1,
},
{
name: "float param",
paramsJSON: `{"key": 1.23}`,
key: "key",
expected: 1.23,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetFloat32(testCase.key)
if err != nil {
t.Fatalf("GetFloat32 returned error: %v", err)
}
if value != testCase.expected {
t.Fatalf("GetFloat32 got %f, expected %f", value, testCase.expected)
}
})
}
}
func TestGoodFloat64ParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected float64
}{
{
name: "no decimal param",
paramsJSON: `{"key": 1}`,
key: "key",
expected: 1,
},
{
name: "float param",
paramsJSON: `{"key": 1.23}`,
key: "key",
expected: 1.23,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetFloat64(testCase.key)
if err != nil {
t.Fatalf("GetFloat64 returned error: %v", err)
}
if value != testCase.expected {
t.Fatalf("GetFloat64 got %f, expected %f", value, testCase.expected)
}
})
}
}
func TestGoodBoolParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected bool
}{
{
name: "bool param",
paramsJSON: `{"key": true}`,
key: "key",
expected: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetBool(testCase.key)
if err != nil {
t.Fatalf("GetBool returned error: %v", err)
}
if value != testCase.expected {
t.Fatalf("GetBool got %t, expected %t", value, testCase.expected)
}
})
}
}
func TestGoodStringSliceParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected []string
}{
{
name: "string array",
paramsJSON: `{"key": ["value1", "value2"]}`,
key: "key",
expected: []string{"value1", "value2"},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetStringSlice(testCase.key)
if err != nil {
t.Fatalf("GetStringSlice returned error: %v", err)
}
if !slices.Equal(value, testCase.expected) {
t.Fatalf("GetStringSlice got %v, expected %v", value, testCase.expected)
}
})
}
}
func TestGoodIntSliceParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected []int
}{
{
name: "int array",
paramsJSON: `{"key": [1, 2, 3]}`,
key: "key",
expected: []int{1, 2, 3},
},
{
name: "int array with floats",
paramsJSON: `{"key": [1.0, 2.0, 3.0]}`,
key: "key",
expected: []int{1, 2, 3},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetIntSlice(testCase.key)
if err != nil {
t.Fatalf("GetIntSlice returned error: %v", err)
}
if !slices.Equal(value, testCase.expected) {
t.Fatalf("GetIntSlice got %v, expected %v", value, testCase.expected)
}
})
}
}
func TestGoodByteSliceParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected []byte
}{
{
name: "byte array",
paramsJSON: `{"key": [1,2,3,4]}`,
key: "key",
expected: []byte{1, 2, 3, 4},
},
{
name: "byte array with floats",
paramsJSON: `{"key": [1.0,2.0,3.0,4.0]}`,
key: "key",
expected: []byte{1, 2, 3, 4},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetByteSlice(testCase.key)
if err != nil {
t.Fatalf("GetByteSlice returned error: %v", err)
}
if !slices.Equal(value, testCase.expected) {
t.Fatalf("GetByteSlice got %v, expected %v", value, testCase.expected)
}
})
}
}

140
internal/config/params.go Normal file
View File

@@ -0,0 +1,140 @@
package config
import (
"errors"
"fmt"
"github.com/jwetzell/showbridge-go/internal/common"
)
type Params map[string]any
var (
ErrParamNotFound = errors.New("not found")
ErrParamNotString = errors.New("not a string")
ErrParamNotNumber = errors.New("not a number")
ErrParamNotInteger = errors.New("not an integer")
ErrParamNotBool = errors.New("not a boolean")
ErrParamNotSlice = errors.New("not a slice")
ErrParamNotByteSlice = errors.New("not a byte slice")
ErrParamNotIntSlice = errors.New("not an int slice")
)
func (p Params) GetString(key string) (string, error) {
value, ok := p[key]
if !ok {
return "", ErrParamNotFound
}
stringValue, ok := value.(string)
if !ok {
return "", ErrParamNotString
}
return stringValue, nil
}
func (p Params) GetInt(key string) (int, error) {
value, ok := p[key]
if !ok {
return 0, ErrParamNotFound
}
intValue, ok := common.GetAnyAsInt(value)
if ok {
return intValue, nil
}
return 0, ErrParamNotNumber
}
func (p Params) GetFloat32(key string) (float32, error) {
value, ok := p[key]
if !ok {
return 0, ErrParamNotFound
}
floatValue, ok := common.GetAnyAsFloat32(value)
if ok {
return floatValue, nil
}
return 0, ErrParamNotNumber
}
func (p Params) GetFloat64(key string) (float64, error) {
value, ok := p[key]
if !ok {
return 0, ErrParamNotFound
}
floatValue, ok := common.GetAnyAsFloat64(value)
if ok {
return floatValue, nil
}
return 0, ErrParamNotNumber
}
func (p Params) GetBool(key string) (bool, error) {
value, ok := p[key]
if !ok {
return false, ErrParamNotFound
}
boolValue, ok := value.(bool)
if !ok {
return false, ErrParamNotBool
}
return boolValue, nil
}
func (p Params) GetStringSlice(key string) ([]string, error) {
value, ok := p[key]
if !ok {
return nil, ErrParamNotFound
}
interfaceSlice, ok := value.([]any)
if !ok {
return nil, ErrParamNotSlice
}
stringSlice := make([]string, len(interfaceSlice))
for i, v := range interfaceSlice {
str, ok := v.(string)
if !ok {
return nil, fmt.Errorf("element at index %d is not a string", i)
}
stringSlice[i] = str
}
return stringSlice, nil
}
func (p Params) GetIntSlice(key string) ([]int, error) {
value, ok := p[key]
if !ok {
return nil, ErrParamNotFound
}
intSlice, ok := common.GetAnyAsIntSlice(value)
if !ok {
return nil, ErrParamNotIntSlice
}
return intSlice, nil
}
func (p Params) GetByteSlice(key string) ([]byte, error) {
value, ok := p[key]
if !ok {
return nil, ErrParamNotFound
}
byteSlice, ok := common.GetAnyAsByteSlice(value)
if !ok {
return nil, ErrParamNotByteSlice
}
return byteSlice, nil
}

View File

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

View File

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

View File

@@ -59,9 +59,9 @@ func (sf *SlipFramer) Encode(data []byte) []byte {
for _, byteToEncode := range data {
switch byteToEncode {
case END:
encodedBytes = append(encodedBytes, ESC_END)
encodedBytes = append(encodedBytes, ESC, ESC_END)
case ESC:
encodedBytes = append(encodedBytes, ESC_ESC)
encodedBytes = append(encodedBytes, ESC, ESC_ESC)
default:
encodedBytes = append(encodedBytes, byteToEncode)
}

View File

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

View File

@@ -0,0 +1,15 @@
package framer_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/framer"
)
func TestNilGetFramer(t *testing.T) {
nilFramer := framer.GetFramer("asldfiudchuehrkbjbkjrbb")
if nilFramer != nil {
t.Fatalf("Expected nil framer, got %v", nilFramer)
}
}

View File

@@ -0,0 +1,78 @@
package framer_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/framer"
)
func TestGoodRawFramerDecode(t *testing.T) {
tests := []struct {
name string
framer framer.Framer
input []byte
expected [][]byte
}{
{
name: "basic raw framer",
framer: framer.GetFramer("RAW"),
input: []byte("Hello\nWorld\nThis is a test\n"),
expected: [][]byte{
[]byte("Hello\nWorld\nThis is a test\n"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frames := test.framer.Decode(test.input)
if len(frames) != len(test.expected) {
t.Fatalf("raw framer got %d frames, expected %d", len(frames), len(test.expected))
}
for i, frame := range frames {
if !slices.Equal(frame, test.expected[i]) {
t.Errorf("raw framer frame %d got %s, expected %s", i, frame, test.expected[i])
}
}
})
}
}
func TestGoodRawFramerEncode(t *testing.T) {
tests := []struct {
name string
framer framer.Framer
expected []byte
input []byte
}{
{
name: "basic raw framer",
framer: framer.GetFramer("RAW"),
expected: []byte("Hello\nWorld\nThis is a test\n"),
input: []byte("Hello\nWorld\nThis is a test\n"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frame := test.framer.Encode(test.input)
if !slices.Equal(frame, test.expected) {
t.Fatalf("raw frame encode got %s, expected %s", frame, test.expected)
}
})
}
}
func TestRawFramerBuffer(t *testing.T) {
framer := framer.GetFramer("RAW")
framer.Decode([]byte("Hello, World!"))
if !slices.Equal(framer.Buffer(), []byte{}) {
t.Fatalf("raw framer buffer got %s, expected empty", framer.Buffer())
}
framer.Clear()
if !slices.Equal(framer.Buffer(), []byte{}) {
t.Fatalf("raw framer buffer got %s, expected empty after clear", framer.Buffer())
}
}

View File

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

View File

@@ -0,0 +1,110 @@
package framer_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/framer"
)
func TestGoodSLIPFramerDecode(t *testing.T) {
tests := []struct {
name string
framer framer.Framer
input []byte
expected [][]byte
buffer []byte
}{
{
name: "OSC SLIP messages",
framer: framer.GetFramer("SLIP"),
input: []byte{0xc0, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xc0},
expected: [][]byte{
{0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00},
},
buffer: []byte{},
},
{
name: "SLIP decode escaped end",
framer: framer.GetFramer("SLIP"),
expected: [][]byte{{0xc0}},
input: []byte{0xc0, 0xdb, 0xdc, 0xc0},
buffer: []byte{},
},
{
name: "SLIP decode escaped escape",
framer: framer.GetFramer("SLIP"),
expected: [][]byte{{0xdb}},
input: []byte{0xc0, 0xdb, 0xdd, 0xc0},
buffer: []byte{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frames := test.framer.Decode(test.input)
if len(frames) != len(test.expected) {
t.Fatalf("SLIP framer got %d frames, expected %d", len(frames), len(test.expected))
}
for i, frame := range frames {
if !slices.Equal(frame, test.expected[i]) {
t.Errorf("SLIP framer frame %d got %s, expected %s", i, frame, test.expected[i])
}
}
if !slices.Equal(test.framer.Buffer(), test.buffer) {
t.Fatalf("SLIP framer buffer got %s, expected %s", test.framer.Buffer(), test.buffer)
}
})
}
}
func TestGoodSLIPFramerEncode(t *testing.T) {
tests := []struct {
name string
framer framer.Framer
input []byte
expected []byte
}{
{
name: "OSC SLIP messages",
framer: framer.GetFramer("SLIP"),
input: []byte{
0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00,
},
expected: []byte{0xc0, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xc0},
},
{
name: "SLIP encode end",
framer: framer.GetFramer("SLIP"),
input: []byte{0xc0},
expected: []byte{0xc0, 0xdb, 0xdc, 0xc0},
},
{
name: "SLIP encode esc",
framer: framer.GetFramer("SLIP"),
input: []byte{0xdb},
expected: []byte{0xc0, 0xdb, 0xdd, 0xc0},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frame := test.framer.Encode(test.input)
if !slices.Equal(frame, test.expected) {
t.Fatalf("SLIP framer frame got %s, expected %s", frame, test.expected)
}
})
}
}
func TestSlipFramerBuffer(t *testing.T) {
framer := framer.GetFramer("SLIP")
framer.Decode([]byte{0xc0, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xc0, 0xc0, 0x45})
if !slices.Equal(framer.Buffer(), []byte{0x45}) {
t.Fatalf("SLIP framer buffer got %s, expected %s", framer.Buffer(), []byte{0x45})
}
framer.Clear()
if !slices.Equal(framer.Buffer(), []byte{}) {
t.Fatalf("SLIP framer buffer got %s, expected empty slice", framer.Buffer())
}
}

View File

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

View File

@@ -3,43 +3,66 @@ package module
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/jwetzell/showbridge-go/internal/processor"
)
type HTTPServer struct {
config config.ModuleConfig
Port uint16
ctx context.Context
router route.RouteIO
router common.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
type ResponseData struct {
Message string `json:"message"`
Status string `json:"status"`
type ResponseIOError struct {
Index int `json:"index"`
OutputError *string `json:"outputError"`
ProcessError *string `json:"processError"`
InputError *string `json:"inputError"`
}
type IOResponseData struct {
IOErrors []ResponseIOError `json:"ioErrors"`
Message string `json:"message"`
Status string `json:"status"`
}
type httpServerContextKey string
type HTTPServerResponseWriter struct {
http.ResponseWriter
done bool
}
func (hsrw *HTTPServerResponseWriter) WriteHeader(status int) {
hsrw.done = true
hsrw.ResponseWriter.WriteHeader(status)
}
func (hsrw *HTTPServerResponseWriter) Write(data []byte) (int, error) {
hsrw.done = true
return hsrw.ResponseWriter.Write(data)
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.http.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
Type: "http.server",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.http.server requires a port parameter")
portNum, err := params.GetInt("port")
if err != nil {
return nil, fmt.Errorf("http.server port error: %w", err)
}
portNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("net.http.server port must be uint16")
}
return &HTTPServer{Port: uint16(portNum), config: config, ctx: ctx, router: router}, nil
return &HTTPServer{Port: uint16(portNum), config: config, logger: CreateLogger(config)}, nil
},
})
}
@@ -52,58 +75,137 @@ func (hs *HTTPServer) Type() string {
return hs.config.Type
}
func (hs *HTTPServer) HandleDefault(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
responseWriter := HTTPServerResponseWriter{ResponseWriter: w}
response := ResponseData{
response := IOResponseData{
Message: "routing successful",
Status: "ok",
}
if hs.router != nil {
routingErrors := hs.router.HandleInput(hs.config.Id, r)
if routingErrors != nil {
w.WriteHeader(http.StatusInternalServerError)
response.Status = "error"
response.Message = "routing failed"
} else {
w.WriteHeader(http.StatusOK)
response.Message = "routing successful"
inputContext := context.WithValue(hs.ctx, httpServerContextKey("responseWriter"), &responseWriter)
senderAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr)
if err == nil {
inputContext = context.WithValue(inputContext, common.SenderContextKey, senderAddr)
}
aRouteFound, routingErrors := hs.router.HandleInput(inputContext, hs.Id(), r)
if !responseWriter.done {
if aRouteFound {
if routingErrors != nil {
w.WriteHeader(http.StatusInternalServerError)
response.Status = "error"
response.Message = "routing failed"
response.IOErrors = []ResponseIOError{}
for _, responseIOError := range routingErrors {
errorToAdd := ResponseIOError{
Index: responseIOError.Index,
}
if responseIOError.InputError != nil {
errorMsg := responseIOError.InputError.Error()
errorToAdd.InputError = &errorMsg
}
if responseIOError.ProcessError != nil {
errorMsg := responseIOError.ProcessError.Error()
errorToAdd.ProcessError = &errorMsg
}
if responseIOError.OutputError != nil {
errorMsg := responseIOError.OutputError.Error()
errorToAdd.OutputError = &errorMsg
}
response.IOErrors = append(response.IOErrors, errorToAdd)
}
json.NewEncoder(w).Encode(response)
return
} else {
w.WriteHeader(http.StatusOK)
response.Message = "routing successful"
json.NewEncoder(w).Encode(response)
return
}
} else {
w.WriteHeader(http.StatusNotFound)
response.Status = "error"
response.Message = "no matching routes found"
json.NewEncoder(w).Encode(response)
return
}
}
} else {
w.WriteHeader(http.StatusInternalServerError)
response.Message = "no router registered"
response.Status = "error"
json.NewEncoder(w).Encode(response)
return
}
json.NewEncoder(w).Encode(response)
}
func (hs *HTTPServer) Run() error {
http.HandleFunc("/", hs.HandleDefault)
func (hs *HTTPServer) Start(ctx context.Context) error {
hs.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("http.server unable to get router from context")
}
hs.router = router
moduleContext, cancel := context.WithCancel(ctx)
hs.ctx = moduleContext
hs.cancel = cancel
httpServer := &http.Server{
Addr: fmt.Sprintf(":%d", hs.Port),
Handler: http.DefaultServeMux,
Handler: hs,
}
go func() {
<-hs.ctx.Done()
slog.Debug("router context done in module", "id", hs.config.Id)
httpServer.Close()
}()
err := httpServer.ListenAndServe()
slog.Debug("net.http.server closed", "id", hs.config.Id)
// TODO(jwetzell): handle server closed error differently
if err != nil {
return err
if err.Error() != "http: Server closed" {
return err
}
}
<-hs.ctx.Done()
hs.logger.Debug("done")
return nil
}
func (hs *HTTPServer) Output(payload any) error {
return fmt.Errorf("net.http.server output is not implemented")
func (hs *HTTPServer) Output(ctx context.Context, payload any) error {
responseWriter, ok := ctx.Value(httpServerContextKey("responseWriter")).(*HTTPServerResponseWriter)
if !ok {
return errors.New("http.server output must originate from an http.server input")
}
payloadResponse, ok := common.GetAnyAs[processor.HTTPResponse](payload)
if !ok {
return errors.New("http.server is only able to output HTTPResponse")
}
if responseWriter.done {
return errors.New("http.server response writer has already been written to")
}
responseWriter.WriteHeader(payloadResponse.Status)
responseWriter.Write(payloadResponse.Body)
return nil
}
func (hs *HTTPServer) Stop() {
hs.cancel()
}
func (hs *HTTPServer) Get(key string) (any, error) {
return nil, errors.New("http.server does not support Get")
}

View File

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

View File

@@ -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,100 @@
//go:build cgo
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)
type MIDIInput struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
Port string
SendFunc func(midi.Message) error
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "midi.input",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
portString, err := params.GetString("port")
if err != nil {
return nil, fmt.Errorf("midi.input port error: %w", err)
}
return &MIDIInput{config: config, Port: portString, logger: CreateLogger(config)}, nil
},
})
}
func (mi *MIDIInput) Id() string {
return mi.config.Id
}
func (mi *MIDIInput) Type() string {
return mi.config.Type
}
func (mi *MIDIInput) Start(ctx context.Context) error {
mi.logger.Debug("running")
defer midi.CloseDriver()
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("midi.input unable to get router from context")
}
mi.router = router
moduleContext, cancel := context.WithCancel(ctx)
mi.ctx = moduleContext
mi.cancel = cancel
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.ctx, mi.Id(), msg)
}
}, midi.UseSysEx())
if err != nil {
return err
}
defer stop()
<-mi.ctx.Done()
mi.logger.Debug("done")
return nil
}
func (mi *MIDIInput) Output(ctx context.Context, payload any) error {
return errors.New("midi.input output is not implemented")
}
func (mi *MIDIInput) Stop() {
mi.cancel()
}
func (mi *MIDIInput) Get(key string) (any, error) {
switch key {
case "port":
return mi.Port, nil
default:
return nil, errors.New("midi.input key not found")
}
}

View File

@@ -0,0 +1,107 @@
//go:build cgo
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)
type MIDIOutput struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
Port string
SendFunc func(midi.Message) error
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "midi.output",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
portString, err := params.GetString("port")
if err != nil {
return nil, fmt.Errorf("midi.output port error: %w", err)
}
return &MIDIOutput{config: config, Port: portString, logger: CreateLogger(config)}, nil
},
})
}
func (mo *MIDIOutput) Id() string {
return mo.config.Id
}
func (mo *MIDIOutput) Type() string {
return mo.config.Type
}
func (mo *MIDIOutput) Start(ctx context.Context) error {
mo.logger.Debug("running")
defer midi.CloseDriver()
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("midi.output unable to get router from context")
}
mo.router = router
moduleContext, cancel := context.WithCancel(ctx)
mo.ctx = moduleContext
mo.cancel = cancel
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("done")
return nil
}
func (mo *MIDIOutput) Output(ctx context.Context, payload any) error {
if mo.SendFunc == nil {
return errors.New("midi.output output is not setup")
}
payloadMessage, ok := common.GetAnyAs[midi.Message](payload)
if !ok {
return errors.New("midi.output can only ouptut midi.Message")
}
return mo.SendFunc(payloadMessage)
}
func (mo *MIDIOutput) Stop() {
mo.cancel()
}
func (mo *MIDIOutput) Get(key string) (any, error) {
switch key {
case "port":
return mo.Port, nil
default:
return nil, errors.New("midi.output key not found")
}
}

View File

@@ -3,28 +3,30 @@ package module
import (
"context"
"fmt"
"log/slog"
"sync"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
)
type ModuleError struct {
Index int
Config config.ModuleConfig
Error error
Index int `json:"index"`
Config config.ModuleConfig `json:"config"`
Error string `json:"error"`
}
type Module interface {
Id() string
Type() string
Run() error
Output(any) error
Start(context.Context) error
Stop()
Output(context.Context, any) error
Get(key string) (any, error)
}
type ModuleRegistration struct {
Type string `json:"type"`
New func(context.Context, config.ModuleConfig, route.RouteIO) (Module, error)
New func(config.ModuleConfig) (Module, error)
}
func RegisterModule(mod ModuleRegistration) {
@@ -49,3 +51,7 @@ var (
moduleRegistryMu sync.RWMutex
ModuleRegistry = make(map[string]ModuleRegistration)
)
func CreateLogger(config config.ModuleConfig) *slog.Logger {
return slog.Default().With("component", "module", "id", config.Id, "type", config.Type)
}

View File

@@ -2,67 +2,51 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
type MQTTClient struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
router common.RouteIO
Broker string
ClientID string
Topic string
client mqtt.Client
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.mqtt.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
Type: "mqtt.client",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
broker, ok := params["broker"]
brokerString, err := params.GetString("broker")
if !ok {
return nil, fmt.Errorf("net.mqtt.client requires a broker parameter")
if err != nil {
return nil, fmt.Errorf("mqtt.client broker error: %w", err)
}
brokerString, ok := broker.(string)
topicString, err := params.GetString("topic")
if !ok {
return nil, fmt.Errorf("net.mqtt.client broker must be string")
if err != nil {
return nil, fmt.Errorf("mqtt.client topic error: %w", err)
}
topic, ok := params["topic"]
clientIdString, err := params.GetString("clientId")
if !ok {
return nil, fmt.Errorf("net.mqtt.client requires a topic parameter")
if err != nil {
return nil, fmt.Errorf("mqtt.client clientId error: %w", err)
}
topicString, ok := topic.(string)
if !ok {
return nil, fmt.Errorf("net.mqtt.client topic must be string")
}
clientId, ok := params["clientId"]
if !ok {
return nil, fmt.Errorf("net.mqtt.client requires a clientId parameter")
}
clientIdString, ok := clientId.(string)
if !ok {
return nil, fmt.Errorf("net.mqtt.client clientId must be string")
}
return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, ctx: ctx, router: router}, nil
return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, logger: CreateLogger(config)}, nil
},
})
}
@@ -75,7 +59,18 @@ func (mc *MQTTClient) Type() string {
return mc.config.Type
}
func (mc *MQTTClient) Run() error {
func (mc *MQTTClient) Start(ctx context.Context) error {
mc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("mqtt.client unable to get router from context")
}
mc.router = router
moduleContext, cancel := context.WithCancel(ctx)
mc.ctx = moduleContext
mc.cancel = cancel
opts := mqtt.NewClientOptions()
opts.AddBroker(mc.Broker)
opts.SetClientID(mc.ClientID)
@@ -84,12 +79,13 @@ 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.ctx, mc.Id(), m)
})
token.Wait()
}
mc.client = mqtt.NewClient(opts)
defer mc.client.Disconnect(250)
token := mc.client.Connect()
@@ -100,28 +96,36 @@ func (mc *MQTTClient) Run() error {
}
<-mc.ctx.Done()
slog.Debug("router context done in module", "id", mc.config.Id)
mc.logger.Debug("done")
return nil
}
func (mc *MQTTClient) Output(payload any) error {
payloadMessage, ok := payload.(processor.MQTTMessage)
func (mc *MQTTClient) Output(ctx context.Context, payload any) error {
payloadMessage, ok := common.GetAnyAs[mqtt.Message](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()
return token.Error()
}
func (mc *MQTTClient) Stop() {
mc.cancel()
}
func (mc *MQTTClient) Get(key string) (any, error) {
return nil, errors.New("mqtt.client does not support Get")
}

View File

@@ -2,54 +2,43 @@ package module
import (
"context"
"fmt"
"errors"
"log/slog"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/nats-io/nats.go"
)
type NATSClient struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
router common.RouteIO
URL string
Subject string
client *nats.Conn
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.nats.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
Type: "nats.client",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
url, ok := params["url"]
if !ok {
return nil, fmt.Errorf("net.nats.client requires a url parameter")
urlString, err := params.GetString("url")
if err != nil {
return nil, errors.New("nats.client url error: " + err.Error())
}
urlString, ok := url.(string)
subjectString, err := params.GetString("subject")
if !ok {
return nil, fmt.Errorf("net.nats.client url must be string")
if err != nil {
return nil, errors.New("nats.client subject error: " + err.Error())
}
subject, ok := params["subject"]
if !ok {
return nil, fmt.Errorf("net.nats.client requires a subject parameter")
}
subjectString, ok := subject.(string)
if !ok {
return nil, fmt.Errorf("net.nats.client subject must be string")
}
return &NATSClient{config: config, URL: urlString, Subject: subjectString, ctx: ctx, router: router}, nil
return &NATSClient{config: config, URL: urlString, Subject: subjectString, logger: CreateLogger(config)}, nil
},
})
}
@@ -62,7 +51,19 @@ func (nc *NATSClient) Type() string {
return nc.config.Type
}
func (nc *NATSClient) Run() error {
func (nc *NATSClient) Start(ctx context.Context) error {
nc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("nats.client unable to get router from context")
}
nc.router = router
moduleContext, cancel := context.WithCancel(ctx)
nc.ctx = moduleContext
nc.cancel = cancel
client, err := nats.Connect(nc.URL, nats.RetryOnFailedConnect(true))
if err != 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.ctx, nc.Id(), msg)
}
})
@@ -87,27 +88,35 @@ 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("done")
return nil
}
func (nc *NATSClient) Output(payload any) error {
func (nc *NATSClient) Output(ctx context.Context, payload any) error {
payloadMessage, ok := payload.(processor.NATSMessage)
payloadMessage, ok := common.GetAnyAs[processor.NATSMessage](payload)
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)
return err
}
func (nc *NATSClient) Stop() {
nc.cancel()
}
func (nc *NATSClient) Get(key string) (any, error) {
return nil, errors.New("nats.client does not support Get")
}

View File

@@ -0,0 +1,118 @@
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"time"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/nats-io/nats-server/v2/server"
)
type NATSServer struct {
config config.ModuleConfig
ctx context.Context
Ip string
Port int
router common.RouteIO
server *server.Server
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "nats.server",
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
portNum = 4222
} else {
return nil, fmt.Errorf("nats.server port error: %w", err)
}
}
ipString, err := params.GetString("ip")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
ipString = "0.0.0.0"
} else {
return nil, fmt.Errorf("nats.server ip error: %w", err)
}
}
_, err = net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
if err != nil {
return nil, err
}
return &NATSServer{config: moduleConfig, logger: CreateLogger(moduleConfig), Ip: ipString, Port: portNum}, nil
},
})
}
func (ns *NATSServer) Id() string {
return ns.config.Id
}
func (ns *NATSServer) Type() string {
return ns.config.Type
}
func (ns *NATSServer) Start(ctx context.Context) error {
ns.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("nats.server unable to get router from context")
}
ns.router = router
moduleContext, cancel := context.WithCancel(ctx)
ns.ctx = moduleContext
ns.cancel = cancel
natsServer, err := server.NewServer(&server.Options{
Host: ns.Ip,
Port: ns.Port,
NoLog: true,
})
if err != nil {
return err
}
ns.server = natsServer
natsServer.Start()
defer natsServer.Shutdown()
if !natsServer.ReadyForConnections(5 * time.Second) {
return errors.New("nats.server failed to start")
}
ns.logger.Info("NATS server started", "client_url", natsServer.ClientURL())
<-ns.ctx.Done()
ns.logger.Debug("done")
return nil
}
func (ns *NATSServer) Output(ctx context.Context, payload any) error {
return errors.New("nats.server does not support output")
}
func (ns *NATSServer) Stop() {
ns.cancel()
if ns.server != nil {
ns.server.Shutdown()
}
}
func (ns *NATSServer) Get(key string) (any, error) {
return nil, errors.New("nats.server does not support Get")
}

View File

@@ -2,30 +2,33 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"time"
"github.com/jwetzell/psn-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
)
type PSNClient struct {
config config.ModuleConfig
conn *net.UDPConn
ctx context.Context
router route.RouteIO
router common.RouteIO
decoder *psn.Decoder
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.psn.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
Type: "psn.client",
New: func(config config.ModuleConfig) (Module, error) {
return &PSNClient{config: config, decoder: psn.NewDecoder(), ctx: ctx, router: router}, nil
return &PSNClient{config: config, decoder: psn.NewDecoder(), logger: CreateLogger(config)}, nil
},
})
}
@@ -38,7 +41,17 @@ func (pc *PSNClient) Type() string {
return pc.config.Type
}
func (pc *PSNClient) Run() error {
func (pc *PSNClient) Start(ctx context.Context) error {
pc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("psn.client unable to get router from context")
}
pc.router = router
moduleContext, cancel := context.WithCancel(ctx)
pc.ctx = moduleContext
pc.cancel = cancel
addr, err := net.ResolveUDPAddr("udp", "236.10.10.10:56565")
if err != nil {
@@ -58,7 +71,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("done")
return nil
default:
pc.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -76,21 +89,37 @@ 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("problem decoding psn traffic", "error", err)
}
if pc.router != nil {
// TODO(jwetzell): better input handling
for _, tracker := range pc.decoder.Trackers {
pc.router.HandleInput(pc.config.Id, tracker)
pc.router.HandleInput(pc.ctx, pc.Id(), tracker)
}
} else {
slog.Error("net.psn.client has no router", "id", pc.config.Id)
pc.logger.Error("has no router")
}
}
}
}
}
func (pc *PSNClient) Output(payload any) error {
return fmt.Errorf("net.psn.client output is not implemented")
func (pc *PSNClient) Output(ctx context.Context, payload any) error {
return fmt.Errorf("psn.client output is not implemented")
}
func (pc *PSNClient) Stop() {
pc.cancel()
}
func (pc *PSNClient) Get(key string) (any, error) {
switch key {
case "trackers":
return pc.decoder.Trackers, nil
case "name":
return pc.decoder.SystemName, nil
default:
return nil, errors.New("psn.client key not found")
}
}

View File

@@ -4,153 +4,146 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/framer"
"github.com/jwetzell/showbridge-go/internal/route"
"go.bug.st/serial"
)
type SerialClient struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
router common.RouteIO
Port string
Framer framer.Framer
Mode *serial.Mode
port serial.Port
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
//TODO(jwetzell): find a better namespace than "misc"
Type: "misc.serial.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
Type: "serial.client",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("misc.serial.client requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, fmt.Errorf("misc.serial.client port must be a string")
}
framingMethod := "RAW"
framingMethodRaw, ok := params["framing"]
if ok {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, fmt.Errorf("misc.serial.client framing method must be a string")
}
framingMethod = framingMethodString
}
framer, err := framer.GetFramer(framingMethod)
portString, err := params.GetString("port")
if err != nil {
return nil, err
return nil, fmt.Errorf("serial.client port error: %w", err)
}
buadRate, ok := params["baudRate"]
if !ok {
return nil, fmt.Errorf("misc.serial.client requires a baudRate parameter")
framingMethodString, err := params.GetString("framing")
if err != nil {
return nil, fmt.Errorf("serial.client framing error: %w", err)
}
baudRateNum, ok := buadRate.(float64)
if !ok {
return nil, fmt.Errorf("misc.serial.client baudRate must be a number")
framer := framer.GetFramer(framingMethodString)
if framer == nil {
return nil, fmt.Errorf("serial.client unknown framing method: %s", framingMethodString)
}
baudRateInt, err := params.GetInt("baudRate")
if err != nil {
return nil, fmt.Errorf("serial.client baudRate error: %w", err)
}
mode := serial.Mode{
BaudRate: int(baudRateNum),
BaudRate: baudRateInt,
}
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, logger: CreateLogger(config)}, 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) Start(ctx context.Context) error {
sc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("serial.client unable to get router from context")
}
sc.router = router
moduleContext, cancel := context.WithCancel(ctx)
sc.ctx = moduleContext
sc.cancel = cancel
// 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("done")
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("done")
return nil
}
slog.Error("misc.serial.client", "id", mc.config.Id, "error", err.Error())
sc.logger.Error("port setup error", "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("done")
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("done")
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.ctx, sc.Id(), message)
} else {
slog.Error("misc.serial.client has no router", "id", mc.config.Id)
sc.logger.Error("input received but no router is configured")
}
}
}
@@ -161,14 +154,27 @@ func (mc *SerialClient) Run() error {
}
}
func (mc *SerialClient) Output(payload any) error {
func (sc *SerialClient) Output(ctx context.Context, payload any) error {
payloadBytes, ok := payload.([]byte)
payloadBytes, ok := common.GetAnyAs[[]byte](payload)
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
}
func (sc *SerialClient) Stop() {
sc.cancel()
}
func (sc *SerialClient) Get(key string) (any, error) {
switch key {
case "port":
return sc.Port, nil
default:
return nil, errors.New("serial.client key not found")
}
}

View File

@@ -0,0 +1,227 @@
package module
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"sync"
"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/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
type SIPCallServer struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
IP string
Port int
Transport string
UserAgent string
dg *diago.Diago
logger *slog.Logger
cancel context.CancelFunc
}
type SIPCallMessage struct {
To string
}
type SIPCall struct {
inDialog *diago.DialogServerSession
lock sync.Mutex
}
type sipCallContextKey string
func init() {
RegisterModule(ModuleRegistration{
Type: "sip.call.server",
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
portNum = 5060
} else {
return nil, fmt.Errorf("sip.call.server port error: %w", err)
}
}
ipString, err := params.GetString("ip")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
ipString = "0.0.0.0"
} else {
return nil, fmt.Errorf("sip.call.server ip error: %w", err)
}
}
transportString, err := params.GetString("transport")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
transportString = "udp"
} else {
return nil, fmt.Errorf("sip.call.server transport error: %w", err)
}
}
userAgentString, err := params.GetString("userAgent")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
userAgentString = "showbridge"
} else {
return nil, fmt.Errorf("sip.call.server userAgent error: %w", err)
}
}
return &SIPCallServer{config: moduleConfig, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString, logger: CreateLogger(moduleConfig)}, nil
},
})
}
func (scs *SIPCallServer) Id() string {
return scs.config.Id
}
func (scs *SIPCallServer) Type() string {
return scs.config.Type
}
func (scs *SIPCallServer) Start(ctx context.Context) error {
scs.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("sip.call.server unable to get router from context")
}
scs.router = router
moduleContext, cancel := context.WithCancel(ctx)
scs.ctx = moduleContext
scs.cancel = cancel
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("done")
return nil
}
func (scs *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) {
inDialog.Trying()
inDialog.Ringing()
inDialog.Answer()
dialogContext := context.WithValue(scs.ctx, sipCallContextKey("call"), &SIPCall{
inDialog: inDialog,
})
scs.router.HandleInput(dialogContext, scs.Id(), SIPCallMessage{
To: inDialog.ToUser(),
})
}
func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
call, ok := ctx.Value(sipCallContextKey("call")).(*SIPCall)
if !ok {
return errors.New("sip.call.server output must originate from sip.call.server input")
}
gotLock := call.lock.TryLock()
if !gotLock {
return errors.New("sip.call.server call is already locked")
}
if call.inDialog.LoadState() == sip.DialogStateEnded {
return errors.New("sip.call.server inDialog already ended")
}
payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload)
if ok {
dtmfWriter := call.inDialog.AudioWriterDTMF()
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
for i, dtmfRune := range payloadDTMFResponse.Digits {
err := dtmfWriter.WriteDTMF(dtmfRune)
if err != nil {
return fmt.Errorf("sip.dtmf.server error output dtmf digit at index %d", i)
}
}
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PostWait))
return nil
}
payloadAudioFileResponse, ok := common.GetAnyAs[processor.SipAudioFileResponse](payload)
if ok {
audioFile, err := os.Open(payloadAudioFileResponse.AudioFile)
if err != nil {
return err
}
defer audioFile.Close()
playback, err := call.inDialog.PlaybackCreate()
if err != nil {
return err
}
time.Sleep(time.Millisecond * time.Duration(payloadAudioFileResponse.PreWait))
_, err = playback.Play(audioFile, "audio/wav")
time.Sleep(time.Millisecond * time.Duration(payloadAudioFileResponse.PostWait))
if err != nil {
return err
}
return nil
}
return errors.New("sip.dtmf.server can only output SipDTMFResponse or SipAudioFileResponse")
}
func (scs *SIPCallServer) Stop() {
scs.cancel()
}
func (scs *SIPCallServer) Get(key string) (any, error) {
return nil, errors.New("sip.call.server does not support Get")
}

View File

@@ -0,0 +1,255 @@
package module
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"strings"
"sync"
"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/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
type SIPDTMFServer struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
IP string
Port int
Transport string
UserAgent string
Separator string
logger *slog.Logger
cancel context.CancelFunc
}
type SIPDTMFMessage struct {
To string
Digits string
}
type SIPDTMFCall struct {
inDialog *diago.DialogServerSession
lock sync.Mutex
}
func init() {
RegisterModule(ModuleRegistration{
Type: "sip.dtmf.server",
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
portNum = 5060
} else {
return nil, fmt.Errorf("sip.dtmf.server port error: %w", err)
}
}
ipString, err := params.GetString("ip")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
ipString = "0.0.0.0"
} else {
return nil, fmt.Errorf("sip.dtmf.server ip error: %w", err)
}
}
transportString, err := params.GetString("transport")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
transportString = "udp"
} else {
return nil, fmt.Errorf("sip.dtmf.server transport error: %w", err)
}
}
userAgentString, err := params.GetString("userAgent")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
userAgentString = "showbridge"
} else {
return nil, fmt.Errorf("sip.dtmf.server userAgent error: %w", err)
}
}
separatorString, err := params.GetString("separator")
if err != nil {
return nil, fmt.Errorf("sip.dtmf.server separator error: %w", err)
}
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: moduleConfig, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString, Separator: separatorString, logger: CreateLogger(moduleConfig)}, nil
},
})
}
func (sds *SIPDTMFServer) Id() string {
return sds.config.Id
}
func (sds *SIPDTMFServer) Type() string {
return sds.config.Type
}
func (sds *SIPDTMFServer) Start(ctx context.Context) error {
sds.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("sip.dtmf.server unable to get router from context")
}
sds.router = router
moduleContext, cancel := context.WithCancel(ctx)
sds.ctx = moduleContext
sds.cancel = cancel
diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
ua, _ := sipgo.NewUA(
sipgo.WithUserAgent(sds.UserAgent),
sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)),
sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)),
)
defer ua.Close()
sip.SetDefaultLogger(diagoLogger)
media.SetDefaultLogger(diagoLogger)
dg := diago.NewDiago(ua, diago.WithLogger(diagoLogger), diago.WithTransport(
diago.Transport{
Transport: sds.Transport,
BindHost: sds.IP,
BindPort: sds.Port,
},
))
err := dg.Serve(sds.ctx, func(inDialog *diago.DialogServerSession) {
sds.HandleCall(inDialog)
})
if err != nil {
return err
}
<-sds.ctx.Done()
sds.logger.Debug("done")
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 {
dialogContext := context.WithValue(sds.ctx, sipCallContextKey("call"), &SIPDTMFCall{
inDialog: inDialog,
})
sds.router.HandleInput(dialogContext, sds.Id(), SIPDTMFMessage{
To: inDialog.ToUser(),
Digits: userString,
})
}
userString = ""
} else {
userString += string(dtmf)
}
return nil
}, 5*time.Second)
}
func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
call, ok := ctx.Value(sipCallContextKey("call")).(*SIPDTMFCall)
if !ok {
return errors.New("sip.dtmf.server output must originate from sip.dtmf.server input")
}
gotLock := call.lock.TryLock()
if !gotLock {
return errors.New("sip.dtmf.server call is already locked")
}
if call.inDialog.LoadState() == sip.DialogStateEnded {
return errors.New("sip.dtmf.server inDialog already ended")
}
payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload)
if ok {
dtmfWriter := call.inDialog.AudioWriterDTMF()
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
for i, dtmfRune := range payloadDTMFResponse.Digits {
err := dtmfWriter.WriteDTMF(dtmfRune)
if err != nil {
return fmt.Errorf("sip.dtmf.server error output dtmf digit at index %d", i)
}
}
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PostWait))
return nil
}
payloadAudioFileResponse, ok := common.GetAnyAs[processor.SipAudioFileResponse](payload)
if ok {
audioFile, err := os.Open(payloadAudioFileResponse.AudioFile)
if err != nil {
return err
}
defer audioFile.Close()
playback, err := call.inDialog.PlaybackCreate()
if err != nil {
return err
}
time.Sleep(time.Millisecond * time.Duration(payloadAudioFileResponse.PreWait))
_, err = playback.Play(audioFile, "audio/wav")
time.Sleep(time.Millisecond * time.Duration(payloadAudioFileResponse.PostWait))
if err != nil {
return err
}
return nil
}
return errors.New("sip.dtmf.server can only output SipDTMFResponse or SipAudioFileResponse")
}
func (sds *SIPDTMFServer) Stop() {
sds.cancel()
}
func (sds *SIPDTMFServer) Get(key string) (any, error) {
return nil, errors.New("sip.dtmf.server does not support Get")
}

View File

@@ -2,14 +2,15 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"time"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/framer"
"github.com/jwetzell/showbridge-go/internal/route"
)
type TCPClient struct {
@@ -17,36 +18,25 @@ type TCPClient struct {
framer framer.Framer
conn *net.TCPConn
ctx context.Context
router route.RouteIO
router common.RouteIO
Addr *net.TCPAddr
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.tcp.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
host, ok := params["host"]
if !ok {
return nil, fmt.Errorf("net.tcp.client requires a host parameter")
hostString, err := params.GetString("host")
if err != nil {
return nil, fmt.Errorf("net.tcp.client host error: %w", err)
}
hostString, ok := host.(string)
if !ok {
return nil, fmt.Errorf("net.tcp.client host must be string")
}
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.tcp.client requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("net.tcp.client port must be a number")
portNum, err := params.GetInt("port")
if err != nil {
return nil, fmt.Errorf("net.tcp.client port error: %w", err)
}
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
@@ -54,26 +44,17 @@ func init() {
return nil, err
}
framingMethod := "RAW"
framingMethodRaw, ok := params["framing"]
if ok {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, fmt.Errorf("misc.serial.client framing method must be a string")
}
framingMethod = framingMethodString
}
framer, err := framer.GetFramer(framingMethod)
framingMethodString, err := params.GetString("framing")
if err != nil {
return nil, err
return nil, fmt.Errorf("net.tcp.client framing error: %w", err)
}
return &TCPClient{framer: framer, Addr: addr, config: config, ctx: ctx, router: router}, nil
framer := framer.GetFramer(framingMethodString)
if framer == nil {
return nil, fmt.Errorf("net.tcp.client unknown framing method: %s", framingMethodString)
}
return &TCPClient{framer: framer, Addr: addr, config: config, logger: CreateLogger(config)}, nil
},
})
}
@@ -86,12 +67,22 @@ func (tc *TCPClient) Type() string {
return tc.config.Type
}
func (tc *TCPClient) Run() error {
func (tc *TCPClient) Start(ctx context.Context) error {
tc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.tcp.client unable to get router from context")
}
tc.router = router
moduleContext, cancel := context.WithCancel(ctx)
tc.ctx = moduleContext
tc.cancel = cancel
// 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("done")
if tc.conn != nil {
tc.conn.Close()
}
@@ -101,10 +92,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("done")
return nil
}
slog.Error("net.tcp.client", "id", tc.config.Id, "error", err.Error())
tc.logger.Error("connection error", "error", err.Error())
time.Sleep(time.Second * 2)
continue
}
@@ -112,14 +103,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("done")
return nil
default:
READ:
for {
select {
case <-tc.ctx.Done():
slog.Debug("router context done in module", "id", tc.config.Id)
tc.logger.Debug("done")
return nil
default:
byteCount, err := tc.conn.Read(buffer)
@@ -134,9 +125,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.ctx, tc.Id(), message)
} else {
slog.Error("net.tcp.client has no router", "id", tc.config.Id)
tc.logger.Error("input received but no router is configured")
}
}
}
@@ -153,7 +144,7 @@ func (tc *TCPClient) SetupConn() error {
return err
}
func (tc *TCPClient) Output(payload any) error {
func (tc *TCPClient) Output(ctx context.Context, payload any) error {
// NOTE(jwetzell): not sure how this would occur but
if tc.conn == nil {
err := tc.SetupConn()
@@ -161,10 +152,31 @@ func (tc *TCPClient) Output(payload any) error {
return err
}
}
payloadBytes, ok := payload.([]byte)
payloadBytes, ok := common.GetAnyAs[[]byte](payload)
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
}
func (tc *TCPClient) Stop() {
tc.cancel()
}
func (tc *TCPClient) Get(key string) (any, error) {
switch key {
case "host":
host, err := tc.config.Params.GetString("host")
if err != nil {
return nil, fmt.Errorf("net.tcp.client host error: %w", err)
}
return host, nil
case "ip":
return tc.Addr.IP.String(), nil
case "port":
return tc.Addr.Port, nil
default:
return nil, errors.New("net.tcp.client key not found")
}
}

View File

@@ -11,9 +11,9 @@ import (
"syscall"
"time"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/framer"
"github.com/jwetzell/showbridge-go/internal/route"
)
type TCPServer struct {
@@ -21,67 +21,50 @@ type TCPServer struct {
Addr *net.TCPAddr
Framer framer.Framer
ctx context.Context
router route.RouteIO
router common.RouteIO
quit chan interface{}
wg sync.WaitGroup
connections []*net.TCPConn
connectionsMu sync.RWMutex
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.tcp.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.tcp.server requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("net.tcp.server port must be a number")
}
framingMethod := "RAW"
framingMethodRaw, ok := params["framing"]
if ok {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, fmt.Errorf("misc.serial.client framing method must be a string")
}
framingMethod = framingMethodString
}
framer, err := framer.GetFramer(framingMethod)
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
return nil, err
return nil, fmt.Errorf("net.tcp.server port error: %w", err)
}
ipString := "0.0.0.0"
framingMethodString, err := params.GetString("framing")
if err != nil {
return nil, fmt.Errorf("net.tcp.server framing error: %w", err)
}
ip, ok := params["ip"]
if ok {
framer := framer.GetFramer(framingMethodString)
specificIpString, ok := ip.(string)
if framer == nil {
return nil, fmt.Errorf("net.tcp.server unknown framing method: %s", framingMethodString)
}
if !ok {
return nil, fmt.Errorf("net.tcp.server ip must be a string")
ipString, err := params.GetString("ip")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
ipString = "0.0.0.0"
} else {
return nil, fmt.Errorf("net.tcp.server ip error: %w", err)
}
ipString = specificIpString
}
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
if err != nil {
return nil, err
}
return &TCPServer{Framer: framer, Addr: addr, config: config, quit: make(chan interface{}), ctx: ctx, router: router}, nil
return &TCPServer{Framer: framer, Addr: addr, config: moduleConfig, quit: make(chan interface{}), logger: CreateLogger(moduleConfig)}, nil
},
})
}
@@ -98,7 +81,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)
@@ -106,6 +89,15 @@ ClientRead:
for {
select {
case <-ts.quit:
client.Close()
ts.connectionsMu.Lock()
for i := 0; i < len(ts.connections); i++ {
if ts.connections[i] == client {
ts.connections = slices.Delete(ts.connections, i, i+1)
break
}
}
ts.connectionsMu.Unlock()
return
default:
client.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -125,7 +117,7 @@ ClientRead:
break
}
}
slog.Debug("net.tcp.server connection reset", "id", ts.config.Id, "remoteAddr", client.RemoteAddr().String())
ts.logger.Debug("connection reset", "remoteAddr", client.RemoteAddr().String())
ts.connectionsMu.Unlock()
}
}
@@ -138,7 +130,7 @@ ClientRead:
break
}
}
slog.Debug("net.tcp.server stream ended", "id", ts.config.Id, "remoteAddr", client.RemoteAddr().String())
ts.logger.Debug("stream ended", "remoteAddr", client.RemoteAddr().String())
ts.connectionsMu.Unlock()
}
return
@@ -148,9 +140,15 @@ ClientRead:
messages := ts.Framer.Decode(buffer[0:byteCount])
for _, message := range messages {
if ts.router != nil {
ts.router.HandleInput(ts.config.Id, message)
senderAddr, ok := client.RemoteAddr().(*net.TCPAddr)
if ok {
senderCtx := context.WithValue(ts.ctx, common.SenderContextKey, senderAddr)
ts.router.HandleInput(senderCtx, ts.Id(), message)
} else {
ts.router.HandleInput(ts.ctx, ts.Id(), message)
}
} else {
slog.Error("net.tcp.server has no router", "id", ts.config.Id)
ts.logger.Error("input received but no router is configured")
}
}
}
@@ -159,7 +157,18 @@ ClientRead:
}
}
func (ts *TCPServer) Run() error {
func (ts *TCPServer) Start(ctx context.Context) error {
ts.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.tcp.server unable to get router from context")
}
ts.router = router
moduleContext, cancel := context.WithCancel(ctx)
ts.ctx = moduleContext
ts.cancel = cancel
listener, err := net.ListenTCP("tcp", ts.Addr)
if err != nil {
return err
@@ -170,7 +179,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("done")
}()
AcceptLoop:
@@ -181,7 +190,7 @@ AcceptLoop:
case <-ts.quit:
break AcceptLoop
default:
slog.Debug("net.tcp.server problem with listener", "error", err)
ts.logger.Debug("problem with listener", "error", err)
}
} else {
ts.wg.Add(1)
@@ -196,11 +205,11 @@ AcceptLoop:
return nil
}
func (ts *TCPServer) Output(payload any) error {
payloadBytes, ok := payload.([]byte)
func (ts *TCPServer) Output(ctx context.Context, payload any) error {
payloadBytes, ok := common.GetAnyAs[[]byte](payload)
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 +225,21 @@ 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)
}
func (ts *TCPServer) Stop() {
ts.cancel()
ts.wg.Wait()
}
func (ts *TCPServer) Get(key string) (any, error) {
switch key {
case "ip":
return ts.Addr.IP.String(), nil
case "port":
return ts.Addr.Port, nil
default:
return nil, errors.New("net.tcp.server key not found")
}
}

View File

@@ -0,0 +1,87 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestHTTPServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["http.server"]
if !ok {
t.Fatalf("http.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "http.server",
Params: map[string]any{
"port": 3000,
},
})
if err != nil {
t.Fatalf("failed to create http.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("http.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "http.server" {
t.Fatalf("http.server module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadHTTPServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{},
errorString: "http.server port error: not found",
},
{
name: "non-numeric port",
params: map[string]any{"port": "3000"},
errorString: "http.server port error: not a number",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["http.server"]
if !ok {
t.Fatalf("http.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "http.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("http.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("http.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("http.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,87 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestMIDIInputFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["midi.input"]
if !ok {
t.Fatalf("midi.input module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "midi.input",
Params: map[string]any{
"port": "test",
},
})
if err != nil {
t.Fatalf("failed to create midi.input module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("midi.input module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "midi.input" {
t.Fatalf("midi.input module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadMIDIInput(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{},
errorString: "midi.input port error: not found",
},
{
name: "non-string port",
params: map[string]any{"port": 123},
errorString: "midi.input port error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["midi.input"]
if !ok {
t.Fatalf("midi.input module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "midi.input",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("midi.input got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("midi.input expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("midi.input got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,87 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestMIDIOutputFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["midi.output"]
if !ok {
t.Fatalf("midi.output module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "midi.output",
Params: map[string]any{
"port": "test",
},
})
if err != nil {
t.Fatalf("failed to create midi.output module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("midi.output module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "midi.output" {
t.Fatalf("midi.output module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadMIDIOutput(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{},
errorString: "midi.output port error: not found",
},
{
name: "non-string port",
params: map[string]any{"port": 123},
errorString: "midi.output port error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["midi.output"]
if !ok {
t.Fatalf("midi.output module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "midi.output",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("midi.output got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("midi.output expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("midi.output got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,85 @@
package module_test
import (
"context"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
type TestModule struct {
}
func (m *TestModule) Output(ctx context.Context, payload any) error {
return nil
}
func (m *TestModule) Start(ctx context.Context) error {
<-ctx.Done()
return nil
}
func (m *TestModule) Stop() {}
func (m *TestModule) Type() string {
return "module.test"
}
func (m *TestModule) Id() string {
return "test"
}
func (m *TestModule) Get(key string) (any, error) {
return nil, nil
}
func TestModuleBadRegistrationNoType(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("module registration should have panicked but did not")
}
}()
module.RegisterModule(module.ModuleRegistration{
Type: "",
New: func(config config.ModuleConfig) (module.Module, error) {
return &TestModule{}, nil
},
})
}
func TestModuleBadRegistrationNoNew(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("processor registration should have panicked but did not")
}
}()
module.RegisterModule(module.ModuleRegistration{
Type: "module.test",
New: nil,
})
}
func TestModuleBadRegistrationExistingType(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("processor registration should have panicked but did not")
}
}()
module.RegisterModule(module.ModuleRegistration{
Type: "module.test",
New: func(config config.ModuleConfig) (module.Module, error) {
return &TestModule{}, nil
},
})
module.RegisterModule(module.ModuleRegistration{
Type: "module.test",
New: func(config config.ModuleConfig) (module.Module, error) {
return &TestModule{}, nil
},
})
}

View File

@@ -0,0 +1,130 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestMQTTClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["mqtt.client"]
if !ok {
t.Fatalf("mqtt.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "mqtt.client",
Params: map[string]any{
"broker": "mqtt://localhost:1883",
"topic": "test/topic",
"clientId": "test",
},
})
if err != nil {
t.Fatalf("failed to create mqtt.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("mqtt.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "mqtt.client" {
t.Fatalf("mqtt.client module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadMQTTClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no broker param",
params: map[string]any{
"topic": "test/topic",
"clientId": "test",
},
errorString: "mqtt.client broker error: not found",
},
{
name: "non-string broker",
params: map[string]any{
"broker": 123,
"topic": "test/topic",
"clientId": "test",
},
errorString: "mqtt.client broker error: not a string",
},
{
name: "no topic param",
params: map[string]any{
"broker": "mqtt://localhost:1883",
"clientId": "test",
},
errorString: "mqtt.client topic error: not found",
},
{
name: "non-string topic",
params: map[string]any{
"broker": "mqtt://localhost:1883",
"topic": 123,
"clientId": "test",
},
errorString: "mqtt.client topic error: not a string",
},
{
name: "no clientId param",
params: map[string]any{
"broker": "mqtt://localhost:1883",
"topic": "test/topic",
},
errorString: "mqtt.client clientId error: not found",
},
{
name: "non-string clientId",
params: map[string]any{
"broker": "mqtt://localhost:1883",
"topic": "test/topic",
"clientId": 123,
},
errorString: "mqtt.client clientId error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["mqtt.client"]
if !ok {
t.Fatalf("mqtt.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "mqtt.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("mqtt.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("mqtt.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("mqtt.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,108 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestNATSClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["nats.client"]
if !ok {
t.Fatalf("nats.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "nats.client",
Params: map[string]any{
"url": "nats://127.0.0.1:4222",
"subject": "test",
},
})
if err != nil {
t.Fatalf("failed to create nats.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("nats.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "nats.client" {
t.Fatalf("nats.client module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadNATSClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no url param",
params: map[string]any{
"subject": "test/subject",
},
errorString: "nats.client url error: not found",
},
{
name: "non-string url",
params: map[string]any{
"url": 123,
"subject": "test/subject",
},
errorString: "nats.client url error: not a string",
},
{
name: "no subject param",
params: map[string]any{
"url": "nats://127.0.0.1:4222",
},
errorString: "nats.client subject error: not found",
},
{
name: "non-string subject",
params: map[string]any{
"url": "nats://127.0.0.1:4222",
"subject": 123,
},
errorString: "nats.client subject error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["nats.client"]
if !ok {
t.Fatalf("nats.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "nats.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("nats.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("nats.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("nats.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,85 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestNATSServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["nats.server"]
if !ok {
t.Fatalf("nats.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "nats.server",
Params: map[string]any{
"ip": "127.0.0.1",
"port": 4222,
},
})
if err != nil {
t.Fatalf("failed to create nats.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("nats.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "nats.server" {
t.Fatalf("nats.server module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadNATSServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "non-string ip",
params: map[string]any{
"ip": 123,
},
errorString: "nats.server ip error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["nats.server"]
if !ok {
t.Fatalf("nats.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "nats.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("nats.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("nats.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("nats.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,73 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestPSNClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["psn.client"]
if !ok {
t.Fatalf("psn.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "psn.client",
})
if err != nil {
t.Fatalf("failed to create psn.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("psn.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "psn.client" {
t.Fatalf("psn.client module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadPSNClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["psn.client"]
if !ok {
t.Fatalf("psn.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "psn.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("psn.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("psn.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("psn.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,117 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestSerialClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["serial.client"]
if !ok {
t.Fatalf("serial.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "serial.client",
Params: map[string]any{
"port": "/dev/ttyUSB0",
"framing": "LF",
"baudRate": 9600,
},
})
if err != nil {
t.Fatalf("failed to create serial.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("serial.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "serial.client" {
t.Fatalf("serial.client module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadSerialClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{
"framing": "LF",
},
errorString: "serial.client port error: not found",
},
{
name: "non-string port param",
params: map[string]any{
"port": 8000,
"framing": "LF",
},
errorString: "serial.client port error: not a string",
},
{
name: "no framing param",
params: map[string]any{
"port": "/dev/ttyUSB0",
},
errorString: "serial.client framing error: not found",
},
{
name: "non-string framing param",
params: map[string]any{
"port": "/dev/ttyUSB0",
"framing": 1,
},
errorString: "serial.client framing error: not a string",
},
{
name: "unkown framing method",
params: map[string]any{
"port": "/dev/ttyUSB0",
"framing": "asdfasdfasdfasdflkj",
},
errorString: "serial.client unknown framing method: asdfasdfasdfasdflkj",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["serial.client"]
if !ok {
t.Fatalf("serial.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "serial.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("serial.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("serial.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("serial.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,102 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestSIPCallServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["sip.call.server"]
if !ok {
t.Fatalf("sip.call.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "sip.call.server",
})
if err != nil {
t.Fatalf("failed to create sip.call.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("sip.call.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "sip.call.server" {
t.Fatalf("sip.call.server module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadSIPCallServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "non-number port param",
params: map[string]any{
"port": "8000",
},
errorString: "sip.call.server port error: not a number",
},
{
name: "non-string ip param",
params: map[string]any{
"ip": 123,
},
errorString: "sip.call.server ip error: not a string",
},
{
name: "non-string transport param",
params: map[string]any{
"transport": 123,
},
errorString: "sip.call.server transport error: not a string",
},
{
name: "non-string userAgent param",
params: map[string]any{
"userAgent": 123,
},
errorString: "sip.call.server userAgent error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["sip.call.server"]
if !ok {
t.Fatalf("sip.call.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "sip.call.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("sip.call.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("sip.call.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("sip.call.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,121 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestSIPDTMFServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["sip.dtmf.server"]
if !ok {
t.Fatalf("sip.dtmf.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "sip.dtmf.server",
Params: map[string]any{
"separator": "#",
},
})
if err != nil {
t.Fatalf("failed to create sip.dtmf.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("sip.dtmf.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "sip.dtmf.server" {
t.Fatalf("sip.dtmf.server module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadSIPDTMFServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no separator param",
params: map[string]any{},
errorString: "sip.dtmf.server separator error: not found",
},
{
name: "non-string separator param",
params: map[string]any{
"separator": 123,
},
errorString: "sip.dtmf.server separator error: not a string",
},
{
name: "non-number port param",
params: map[string]any{
"separator": "#",
"port": "8000",
},
errorString: "sip.dtmf.server port error: not a number",
},
{
name: "non-string ip param",
params: map[string]any{
"separator": "#",
"ip": 123,
},
errorString: "sip.dtmf.server ip error: not a string",
},
{
name: "non-string transport param",
params: map[string]any{
"separator": "#",
"transport": 123,
},
errorString: "sip.dtmf.server transport error: not a string",
},
{
name: "non-string userAgent param",
params: map[string]any{
"separator": "#",
"userAgent": 123,
},
errorString: "sip.dtmf.server userAgent error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["sip.dtmf.server"]
if !ok {
t.Fatalf("sip.dtmf.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "sip.dtmf.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("sip.dtmf.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("sip.dtmf.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("sip.dtmf.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,109 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestTCPClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["net.tcp.client"]
if !ok {
t.Fatalf("net.tcp.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.tcp.client",
Params: map[string]any{
"host": "localhost",
"port": 8000,
"framing": "LF",
},
})
if err != nil {
t.Fatalf("failed to create net.tcp.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("net.tcp.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "net.tcp.client" {
t.Fatalf("net.tcp.client module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadTCPClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{
"host": "localhost",
},
errorString: "net.tcp.client port error: not found",
},
{
name: "non-number port param",
params: map[string]any{
"host": "localhost",
"port": "8000",
},
errorString: "net.tcp.client port error: not a number",
},
{
name: "no host param",
params: map[string]any{
"port": 8000,
},
errorString: "net.tcp.client host error: not found",
},
{
name: "non-string host param",
params: map[string]any{
"host": 123,
"port": 8000,
},
errorString: "net.tcp.client host error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["net.tcp.client"]
if !ok {
t.Fatalf("net.tcp.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.tcp.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("net.tcp.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("net.tcp.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("net.tcp.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,134 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestTCPServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["net.tcp.server"]
if !ok {
t.Fatalf("net.tcp.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.tcp.server",
Params: map[string]any{
"port": 8000,
"framing": "LF",
},
})
if err != nil {
t.Fatalf("failed to create net.tcp.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("net.tcp.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "net.tcp.server" {
t.Fatalf("net.tcp.server module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadTCPServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{
"framing": "LF",
},
errorString: "net.tcp.server port error: not found",
},
{
name: "non-number port param",
params: map[string]any{
"port": "8000",
"framing": "LF",
},
errorString: "net.tcp.server port error: not a number",
},
{
name: "no framing param",
params: map[string]any{
"port": 8000,
},
errorString: "net.tcp.server framing error: not found",
},
{
name: "non-string framing param",
params: map[string]any{
"port": 8000,
"framing": 1,
},
errorString: "net.tcp.server framing error: not a string",
},
{
name: "unkown framing method",
params: map[string]any{
"port": 8000,
"framing": "asdfasdfasdfasdflkj",
},
errorString: "net.tcp.server unknown framing method: asdfasdfasdfasdflkj",
},
{
name: "non-string ip param",
params: map[string]any{
"port": 8000,
"framing": "LF",
"ip": 123,
},
errorString: "net.tcp.server ip error: not a string",
},
{
name: "invalid addr",
params: map[string]any{
"ip": "127.0.0.",
"port": 8000,
"framing": "LF",
},
errorString: "lookup 127.0.0.: no such host",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["net.tcp.server"]
if !ok {
t.Fatalf("net.tcp.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.tcp.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("net.tcp.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("net.tcp.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("net.tcp.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,89 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestTimeIntervalFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["time.interval"]
if !ok {
t.Fatalf("time.interval module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "time.interval",
Params: map[string]any{
"duration": 1000,
},
})
if err != nil {
t.Fatalf("failed to create time.interval module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("time.interval module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "time.interval" {
t.Fatalf("time.interval module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadTimeInterval(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no duration param",
params: map[string]any{},
errorString: "time.interval duration error: not found",
},
{
name: "non-number duration param",
params: map[string]any{
"duration": "8000",
},
errorString: "time.interval duration error: not a number",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["time.interval"]
if !ok {
t.Fatalf("time.interval module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "time.interval",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("time.interval got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("time.interval expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("time.interval got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,89 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestTimeTimerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["time.timer"]
if !ok {
t.Fatalf("time.timer module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "time.timer",
Params: map[string]any{
"duration": 1000,
},
})
if err != nil {
t.Fatalf("failed to create time.timer module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("time.timer module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "time.timer" {
t.Fatalf("time.timer module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadTimeTimer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no duration param",
params: map[string]any{},
errorString: "time.timer duration error: not found",
},
{
name: "non-number duration param",
params: map[string]any{
"duration": "8000",
},
errorString: "time.timer duration error: not a number",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["time.timer"]
if !ok {
t.Fatalf("time.timer module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "time.timer",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("time.timer got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("time.timer expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("time.timer got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,109 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestUDPClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.client"]
if !ok {
t.Fatalf("udp.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.client",
Params: map[string]any{
"host": "localhost",
"port": 8000,
"framing": "LF",
},
})
if err != nil {
t.Fatalf("failed to create net.udp.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("net.udp.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "net.udp.client" {
t.Fatalf("net.udp.client module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadUDPClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{
"host": "localhost",
},
errorString: "net.udp.client port error: not found",
},
{
name: "non-number port param",
params: map[string]any{
"host": "localhost",
"port": "8000",
},
errorString: "net.udp.client port error: not a number",
},
{
name: "no host param",
params: map[string]any{
"port": 8000,
},
errorString: "net.udp.client host error: not found",
},
{
name: "non-string host param",
params: map[string]any{
"host": 123,
"port": 8000,
},
errorString: "net.udp.client host error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.client"]
if !ok {
t.Fatalf("net.udp.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("net.udp.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("net.udp.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("net.udp.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,116 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestUDPMulticastFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.multicast"]
if !ok {
t.Fatalf("udp.multicast module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.multicast",
Params: map[string]any{
"ip": "236.10.10.10",
"port": 56565,
},
})
if err != nil {
t.Fatalf("failed to create net.udp.multicast module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("net.udp.multicast module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "net.udp.multicast" {
t.Fatalf("net.udp.multicast module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadUDPMulticast(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{
"ip": "localhost",
},
errorString: "net.udp.multicast port error: not found",
},
{
name: "non-number port param",
params: map[string]any{
"ip": "localhost",
"port": "8000",
},
errorString: "net.udp.multicast port error: not a number",
},
{
name: "no ip param",
params: map[string]any{
"port": 8000,
},
errorString: "net.udp.multicast ip error: not found",
},
{
name: "non-string ip param",
params: map[string]any{
"ip": 123,
"port": 8000,
},
errorString: "net.udp.multicast ip error: not a string",
},
{
name: "invalid addr",
params: map[string]any{
"ip": "127.0.0.",
"port": 8000,
},
errorString: "lookup 127.0.0.: no such host",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.multicast"]
if !ok {
t.Fatalf("net.udp.multicast module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.multicast",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("net.udp.multicast got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("net.udp.multicast expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("net.udp.multicast got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,113 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestUDPServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.server"]
if !ok {
t.Fatalf("net.udp.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.server",
Params: map[string]any{
"port": 8000,
},
})
if err != nil {
t.Fatalf("failed to create udp.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("udp.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "net.udp.server" {
t.Fatalf("net.udp.server module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadUDPServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{},
errorString: "net.udp.server port error: not found",
},
{
name: "non-number port param",
params: map[string]any{
"port": "8000",
},
errorString: "net.udp.server port error: not a number",
},
{
name: "non-string ip param",
params: map[string]any{
"port": 8000,
"ip": 123,
},
errorString: "net.udp.server ip error: not a string",
},
{
name: "non-number bufferSize param",
params: map[string]any{
"port": 8000,
"bufferSize": "1024",
},
errorString: "net.udp.server bufferSize error: not a number",
},
{
name: "invalid addr",
params: map[string]any{
"ip": "127.0.0.",
"port": 8000,
},
errorString: "lookup 127.0.0.: no such host",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.server"]
if !ok {
t.Fatalf("net.udp.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("net.udp.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("net.udp.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("net.udp.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,93 @@
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type TimeInterval struct {
config config.ModuleConfig
Duration uint32
ctx context.Context
router common.RouteIO
ticker *time.Ticker
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "time.interval",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
durationInt, err := params.GetInt("duration")
if err != nil {
return nil, fmt.Errorf("time.interval duration error: %w", err)
}
return &TimeInterval{Duration: uint32(durationInt), config: config, logger: CreateLogger(config)}, nil
},
})
}
func (i *TimeInterval) Id() string {
return i.config.Id
}
func (i *TimeInterval) Type() string {
return i.config.Type
}
func (i *TimeInterval) Start(ctx context.Context) error {
i.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("time.interval unable to get router from context")
}
i.router = router
moduleContext, cancel := context.WithCancel(ctx)
i.ctx = moduleContext
i.cancel = cancel
ticker := time.NewTicker(time.Millisecond * time.Duration(i.Duration))
i.ticker = ticker
defer ticker.Stop()
for {
select {
case <-i.ctx.Done():
i.logger.Debug("done")
return nil
case <-ticker.C:
if i.router != nil {
i.router.HandleInput(i.ctx, i.Id(), time.Now())
}
}
}
}
func (i *TimeInterval) Output(ctx context.Context, payload any) error {
i.ticker.Reset(time.Millisecond * time.Duration(i.Duration))
return nil
}
func (i *TimeInterval) Stop() {
i.cancel()
}
func (i *TimeInterval) Get(key string) (any, error) {
switch key {
case "duration":
return i.Duration, nil
default:
return nil, errors.New("time.interval key not found")
}
}

View File

@@ -0,0 +1,92 @@
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type TimeTimer struct {
config config.ModuleConfig
Duration uint32
ctx context.Context
router common.RouteIO
timer *time.Timer
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "time.timer",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
durationNum, err := params.GetInt("duration")
if err != nil {
return nil, fmt.Errorf("time.timer duration error: %w", err)
}
return &TimeTimer{Duration: uint32(durationNum), config: config, logger: CreateLogger(config)}, nil
},
})
}
func (t *TimeTimer) Id() string {
return t.config.Id
}
func (t *TimeTimer) Type() string {
return t.config.Type
}
func (t *TimeTimer) Start(ctx context.Context) error {
t.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.tcp.client unable to get router from context")
}
t.router = router
moduleContext, cancel := context.WithCancel(ctx)
t.ctx = moduleContext
t.cancel = cancel
t.timer = time.NewTimer(time.Millisecond * time.Duration(t.Duration))
defer t.timer.Stop()
for {
select {
case <-t.ctx.Done():
t.timer.Stop()
t.logger.Debug("done")
return nil
case time := <-t.timer.C:
if t.router != nil {
t.router.HandleInput(t.ctx, t.Id(), time)
}
}
}
}
func (t *TimeTimer) Output(ctx context.Context, payload any) error {
t.timer.Reset(time.Millisecond * time.Duration(t.Duration))
return nil
}
func (t *TimeTimer) Stop() {
t.cancel()
}
func (t *TimeTimer) Get(key string) (any, error) {
switch key {
case "duration":
return t.Duration, nil
default:
return nil, errors.New("time.timer key not found")
}
}

View File

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

View File

@@ -2,12 +2,13 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
)
type UDPClient struct {
@@ -16,43 +17,31 @@ type UDPClient struct {
Port uint16
conn *net.UDPConn
ctx context.Context
router route.RouteIO
router common.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.udp.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
host, ok := params["host"]
if !ok {
return nil, fmt.Errorf("net.udp.client requires a host parameter")
hostString, err := params.GetString("host")
if err != nil {
return nil, fmt.Errorf("net.udp.client host error: %w", err)
}
hostString, ok := host.(string)
if !ok {
return nil, fmt.Errorf("net.udp.client host must be a string")
}
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.udp.client requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("net.udp.client port must be a number")
portNum, err := params.GetInt("port")
if err != nil {
return nil, fmt.Errorf("net.udp.client port error: %w", err)
}
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
if err != nil {
return nil, err
}
return &UDPClient{Addr: addr, config: config, ctx: ctx, router: router}, nil
return &UDPClient{Addr: addr, config: config, logger: CreateLogger(config)}, nil
},
})
}
@@ -71,7 +60,17 @@ func (uc *UDPClient) SetupConn() error {
return err
}
func (uc *UDPClient) Run() error {
func (uc *UDPClient) Start(ctx context.Context) error {
uc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.udp.client unable to get router from context")
}
uc.router = router
moduleContext, cancel := context.WithCancel(ctx)
uc.ctx = moduleContext
uc.cancel = cancel
err := uc.SetupConn()
if err != nil {
@@ -79,18 +78,18 @@ func (uc *UDPClient) Run() error {
}
<-uc.ctx.Done()
slog.Debug("router context done in module", "id", uc.config.Id)
uc.logger.Debug("done")
if uc.conn != nil {
uc.conn.Close()
}
return nil
}
func (uc *UDPClient) Output(payload any) error {
func (uc *UDPClient) Output(ctx context.Context, payload any) error {
payloadBytes, ok := payload.([]byte)
payloadBytes, ok := common.GetAnyAs[[]byte](payload)
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 +98,28 @@ 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
}
func (uc *UDPClient) Stop() {
uc.cancel()
}
func (uc *UDPClient) Get(key string) (any, error) {
switch key {
case "host":
host, err := uc.config.Params.GetString("host")
if err != nil {
return nil, fmt.Errorf("net.udp.client host error: %w", err)
}
return host, nil
case "ip":
return uc.Addr.IP.String(), nil
case "port":
return uc.Addr.Port, nil
default:
return nil, errors.New("net.udp.client key not found")
}
}

View File

@@ -2,56 +2,46 @@ package module
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"time"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
)
type UDPMulticast struct {
config config.ModuleConfig
conn *net.UDPConn
ctx context.Context
router route.RouteIO
router common.RouteIO
Addr *net.UDPAddr
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.udp.multicast",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params
ip, ok := params["ip"]
if !ok {
return nil, fmt.Errorf("net.udp.client requires am ip parameter")
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
ipString, err := params.GetString("ip")
if err != nil {
return nil, fmt.Errorf("net.udp.multicast ip error: %w", err)
}
ipString, ok := ip.(string)
if !ok {
return nil, fmt.Errorf("net.udp.client ip must be a string")
}
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.udp.client requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("net.udp.client port must be a number")
portNum, err := params.GetInt("port")
if err != nil {
return nil, fmt.Errorf("net.udp.multicast port error: %w", err)
}
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: moduleConfig, Addr: addr, logger: CreateLogger(moduleConfig)}, nil
},
})
}
@@ -64,7 +54,17 @@ func (um *UDPMulticast) Type() string {
return um.config.Type
}
func (um *UDPMulticast) Run() error {
func (um *UDPMulticast) Start(ctx context.Context) error {
um.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.udp.multicast unable to get router from context")
}
um.router = router
moduleContext, cancel := context.WithCancel(ctx)
um.ctx = moduleContext
um.cancel = cancel
client, err := net.ListenMulticastUDP("udp", nil, um.Addr)
if err != nil {
@@ -79,7 +79,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("done")
return nil
default:
um.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -97,26 +97,41 @@ func (um *UDPMulticast) Run() error {
message := buffer[:numBytes]
if um.router != nil {
um.router.HandleInput(um.config.Id, message)
um.router.HandleInput(um.ctx, um.Id(), message)
} else {
slog.Error("net.udp.multicast has no router", "id", um.config.Id)
um.logger.Error("input received but no router is configured")
}
}
}
}
}
func (um *UDPMulticast) Output(payload any) error {
func (um *UDPMulticast) Output(ctx context.Context, payload any) error {
payloadBytes, ok := payload.([]byte)
payloadBytes, ok := common.GetAnyAs[[]byte](payload)
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)
return err
}
func (um *UDPMulticast) Stop() {
um.cancel()
}
func (um *UDPMulticast) Get(key string) (any, error) {
switch key {
case "ip":
return um.Addr.IP.String(), nil
case "port":
return um.Addr.Port, nil
default:
return nil, errors.New("net.udp.multicast key not found")
}
}

View File

@@ -2,58 +2,59 @@ package module
import (
"context"
"errors"
"fmt"
"log"
"log/slog"
"net"
"time"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
)
type UDPServer struct {
Addr *net.UDPAddr
config config.ModuleConfig
ctx context.Context
router route.RouteIO
Addr *net.UDPAddr
BufferSize int
config config.ModuleConfig
ctx context.Context
router common.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.udp.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("net.udp.server requires a port parameter")
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
return nil, fmt.Errorf("net.udp.server port error: %w", err)
}
portNum, ok := port.(float64)
if !ok {
return nil, fmt.Errorf("net.udp.server port must be a number")
}
ipString := "0.0.0.0"
ip, ok := params["ip"]
if ok {
specificIpString, ok := ip.(string)
if !ok {
return nil, fmt.Errorf("net.udp.server ip must be a string")
ipString, err := params.GetString("ip")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
ipString = "0.0.0.0"
} else {
return nil, fmt.Errorf("net.udp.server ip error: %w", err)
}
ipString = specificIpString
}
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
if err != nil {
log.Fatalf("error resolving UDP address: %v", err)
return nil, err
}
return &UDPServer{Addr: addr, config: config, ctx: ctx, router: router}, nil
bufferSizeNum, err := params.GetInt("bufferSize")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
bufferSizeNum = 2048
} else {
return nil, fmt.Errorf("net.udp.server bufferSize error: %w", err)
}
}
return &UDPServer{Addr: addr, BufferSize: bufferSizeNum, config: moduleConfig, logger: CreateLogger(moduleConfig)}, nil
},
})
}
@@ -63,10 +64,20 @@ func (us *UDPServer) Id() string {
}
func (us *UDPServer) Type() string {
return us.config.Id
return us.config.Type
}
func (us *UDPServer) Run() error {
func (us *UDPServer) Start(ctx context.Context) error {
us.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.udp.server unable to get router from context")
}
us.router = router
moduleContext, cancel := context.WithCancel(ctx)
us.ctx = moduleContext
us.cancel = cancel
listener, err := net.ListenUDP("udp", us.Addr)
if err != nil {
@@ -75,18 +86,17 @@ 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("done")
return nil
default:
listener.SetDeadline(time.Now().Add(time.Millisecond * 200))
numBytes, _, err := listener.ReadFromUDP(buffer)
numBytes, senderAddr, err := listener.ReadFromUDP(buffer)
if err != nil {
//NOTE(jwetzell) we hit deadline
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
@@ -96,15 +106,31 @@ func (us *UDPServer) Run() error {
}
message := buffer[:numBytes]
if us.router != nil {
us.router.HandleInput(us.config.Id, message)
senderCtx := context.WithValue(us.ctx, common.SenderContextKey, senderAddr)
us.router.HandleInput(senderCtx, us.Id(), message)
} else {
slog.Error("net.udp.server has no router", "id", us.config.Id)
us.logger.Error("input received but no router is configured")
}
}
}
}
func (us *UDPServer) Output(payload any) error {
return fmt.Errorf("net.udp.server output is not implemented")
func (us *UDPServer) Output(ctx context.Context, payload any) error {
return errors.New("net.udp.server output is not implemented")
}
func (us *UDPServer) Stop() {
us.cancel()
}
func (us *UDPServer) Get(key string) (any, error) {
switch key {
case "ip":
return us.Addr.IP.String(), nil
case "port":
return us.Addr.Port, nil
default:
return nil, errors.New("net.udp.server key not found")
}
}

View File

@@ -0,0 +1,43 @@
package processor
import (
"context"
"fmt"
"github.com/jwetzell/artnet-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type ArtNetPacketDecode struct {
config config.ProcessorConfig
}
func (apd *ArtNetPacketDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok {
return nil, fmt.Errorf("artnet.packet.decode processor only accepts a []byte")
}
payloadMessage, err := artnet.Decode(payloadBytes)
if err != nil {
return nil, err
}
return payloadMessage, nil
}
func (apd *ArtNetPacketDecode) Type() string {
return apd.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.decode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketDecode{config: config}, nil
},
})
}

View File

@@ -0,0 +1,42 @@
package processor
import (
"context"
"fmt"
"github.com/jwetzell/artnet-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type ArtNetPacketEncode struct {
config config.ProcessorConfig
}
func (ape *ArtNetPacketEncode) Process(ctx context.Context, payload any) (any, error) {
payloadPacket, ok := common.GetAnyAs[artnet.ArtNetPacket](payload)
if !ok {
return nil, fmt.Errorf("artnet.packet.encode processor only accepts an ArtNetPacket")
}
payloadBytes, err := payloadPacket.MarshalBinary()
if err != nil {
return nil, err
}
return payloadBytes, nil
}
func (ape *ArtNetPacketEncode) Type() string {
return ape.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketEncode{config: config}, nil
},
})
}

View File

@@ -10,10 +10,13 @@ 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))
payloadString := fmt.Sprintf("%+v", payload)
payloadType := fmt.Sprintf("%T", payload)
dl.logger.Debug("", "payload", payloadString, "payloadType", payloadType)
return payload, nil
}
@@ -25,7 +28,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", "type", config.Type)}, nil
},
})
}

View File

@@ -0,0 +1,61 @@
package processor
import (
"context"
"fmt"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/jwetzell/showbridge-go/internal/config"
)
// NOTE(jwetzell): see language definition https://expr-lang.org/docs/language-definition
type FilterExpr struct {
config config.ProcessorConfig
Program *vm.Program
}
func (fe *FilterExpr) Process(ctx context.Context, payload any) (any, error) {
exprEnv := GetEnvData(ctx, payload)
output, err := expr.Run(fe.Program, exprEnv)
if err != nil {
return nil, err
}
outputBool, ok := output.(bool)
if !ok {
return nil, fmt.Errorf("filter.expr expression did not return a boolean")
}
if !outputBool {
return nil, nil
}
return payload, nil
}
func (fe *FilterExpr) Type() string {
return fe.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.expr",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
expressionString, err := params.GetString("expression")
if err != nil {
return nil, fmt.Errorf("filter.expr expression error: %w", err)
}
program, err := expr.Compile(expressionString)
if err != nil {
return nil, err
}
return &FilterExpr{config: config, Program: program}, nil
},
})
}

View File

@@ -0,0 +1,56 @@
package processor
import (
"context"
"errors"
"fmt"
"regexp"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type FilterRegex struct {
config config.ProcessorConfig
Pattern *regexp.Regexp
}
func (fr *FilterRegex) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := common.GetAnyAs[string](payload)
if !ok {
return nil, errors.New("filter.regex processor only accepts a string")
}
if !fr.Pattern.MatchString(payloadString) {
return nil, nil
}
return payloadString, nil
}
func (fr *FilterRegex) Type() string {
return fr.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.regex",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
patternString, err := params.GetString("pattern")
if err != nil {
return nil, fmt.Errorf("filter.regex pattern error: %w", err)
}
patternRegexp, err := regexp.Compile(patternString)
if err != nil {
return nil, err
}
return &FilterRegex{config: config, Pattern: patternRegexp}, nil
},
})
}

View File

@@ -2,25 +2,27 @@ package processor
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type FloatParse struct {
config config.ProcessorConfig
BitSize int
config config.ProcessorConfig
}
func (fp *FloatParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string)
payloadString, ok := common.GetAnyAs[string](payload)
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
}
@@ -34,8 +36,18 @@ func (fp *FloatParse) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "float.parse",
New: func(config config.ProcessorConfig) (Processor, error) {
return &FloatParse{config: config}, nil
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params
bitSizeNum, err := params.GetInt("bitSize")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
bitSizeNum = 64
} else {
return nil, fmt.Errorf("float.parse bitSize error: %w", err)
}
}
return &FloatParse{config: moduleConfig, BitSize: bitSizeNum}, nil
},
})
}

View File

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

View File

@@ -0,0 +1,71 @@
package processor
import (
"context"
"errors"
"fmt"
"math/rand/v2"
"github.com/jwetzell/showbridge-go/internal/config"
)
type FloatRandom struct {
BitSize int
Min float64
Max float64
config config.ProcessorConfig
}
func (fr *FloatRandom) Process(ctx context.Context, payload any) (any, error) {
if fr.BitSize == 32 {
payloadFloat := rand.Float32()*(float32(fr.Max)-float32(fr.Min)) + float32(fr.Min)
return payloadFloat, nil
}
if fr.BitSize == 64 {
payloadFloat := rand.Float64()*(fr.Max-fr.Min) + fr.Min
return payloadFloat, nil
}
return nil, errors.New("float.random bitSize error: must be 32 or 64")
}
func (fr *FloatRandom) Type() string {
return fr.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "float.random",
New: func(processorConfig config.ProcessorConfig) (Processor, error) {
params := processorConfig.Params
bitSizeInt, err := params.GetInt("bitSize")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
bitSizeInt = 32
} else {
return nil, fmt.Errorf("float.random bitSize error: %w", err)
}
}
if bitSizeInt != 32 && bitSizeInt != 64 {
return nil, errors.New("float.random bitSize error: must be 32 or 64")
}
minFloat, err := params.GetFloat64("min")
if err != nil {
return nil, fmt.Errorf("float.random min error: %w", err)
}
maxFloat, err := params.GetFloat64("max")
if err != nil {
return nil, fmt.Errorf("float.random max error: %w", err)
}
if maxFloat < minFloat {
return nil, errors.New("float.random max must be greater than min")
}
return &FloatRandom{config: processorConfig, Min: minFloat, Max: maxFloat, BitSize: bitSizeInt}, nil
},
})
}

View File

@@ -26,8 +26,10 @@ type FreeDCreate struct {
func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload)
var idBuffer bytes.Buffer
err := fc.Id.Execute(&idBuffer, payload)
err := fc.Id.Execute(&idBuffer, templateData)
if err != nil {
return nil, err
@@ -42,7 +44,7 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
}
var panBuffer bytes.Buffer
err = fc.Pan.Execute(&panBuffer, payload)
err = fc.Pan.Execute(&panBuffer, templateData)
if err != nil {
return nil, err
@@ -57,7 +59,7 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
}
var tiltBuffer bytes.Buffer
err = fc.Tilt.Execute(&tiltBuffer, payload)
err = fc.Tilt.Execute(&tiltBuffer, templateData)
if err != nil {
return nil, err
@@ -72,7 +74,7 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
}
var rollBuffer bytes.Buffer
err = fc.Tilt.Execute(&rollBuffer, payload)
err = fc.Tilt.Execute(&rollBuffer, templateData)
if err != nil {
return nil, err
@@ -87,7 +89,7 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
}
var posXBuffer bytes.Buffer
err = fc.PosX.Execute(&posXBuffer, payload)
err = fc.PosX.Execute(&posXBuffer, templateData)
if err != nil {
return nil, err
@@ -102,7 +104,7 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
}
var posYBuffer bytes.Buffer
err = fc.PosY.Execute(&posYBuffer, payload)
err = fc.PosY.Execute(&posYBuffer, templateData)
if err != nil {
return nil, err
@@ -117,7 +119,7 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
}
var posZBuffer bytes.Buffer
err = fc.PosZ.Execute(&posZBuffer, payload)
err = fc.PosZ.Execute(&posZBuffer, templateData)
if err != nil {
return nil, err
@@ -132,7 +134,7 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
}
var zoomBuffer bytes.Buffer
err = fc.Zoom.Execute(&zoomBuffer, payload)
err = fc.Zoom.Execute(&zoomBuffer, templateData)
if err != nil {
return nil, err
@@ -147,7 +149,7 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
}
var focusBuffer bytes.Buffer
err = fc.Zoom.Execute(&focusBuffer, payload)
err = fc.Focus.Execute(&focusBuffer, templateData)
if err != nil {
return nil, err
@@ -187,16 +189,9 @@ func init() {
// TODO(jwetzell): make some params optional
params := config.Params
id, ok := params["id"]
if !ok {
return nil, fmt.Errorf("freed.create requires an id parameter")
}
idString, ok := id.(string)
if !ok {
return nil, fmt.Errorf("freed.create id must be a string")
idString, err := params.GetString("id")
if err != nil {
return nil, fmt.Errorf("freed.create id error: %w", err)
}
idTemplate, err := template.New("id").Parse(idString)
@@ -205,44 +200,23 @@ func init() {
return nil, err
}
pan, ok := params["pan"]
if !ok {
return nil, fmt.Errorf("freed.create requires an pan parameter")
}
panString, ok := pan.(string)
if !ok {
return nil, fmt.Errorf("freed.create pan must be a string")
panString, err := params.GetString("pan")
if err != nil {
return nil, fmt.Errorf("freed.create pan error: %w", err)
}
panTemplate, err := template.New("pan").Parse(panString)
tilt, ok := params["tilt"]
if !ok {
return nil, fmt.Errorf("freed.create requires an tilt parameter")
}
tiltString, ok := tilt.(string)
if !ok {
return nil, fmt.Errorf("freed.create tilt must be a string")
tiltString, err := params.GetString("tilt")
if err != nil {
return nil, fmt.Errorf("freed.create tilt error: %w", err)
}
tiltTemplate, err := template.New("tilt").Parse(tiltString)
roll, ok := params["roll"]
if !ok {
return nil, fmt.Errorf("freed.create requires an roll parameter")
}
rollString, ok := roll.(string)
if !ok {
return nil, fmt.Errorf("freed.create roll must be a string")
rollString, err := params.GetString("roll")
if err != nil {
return nil, fmt.Errorf("freed.create roll error: %w", err)
}
rollTemplate, err := template.New("roll").Parse(rollString)
@@ -251,16 +225,9 @@ func init() {
return nil, err
}
posX, ok := params["posX"]
if !ok {
return nil, fmt.Errorf("freed.create requires a posX parameter")
}
posXString, ok := posX.(string)
if !ok {
return nil, fmt.Errorf("freed.create posX must be a string")
posXString, err := params.GetString("posX")
if err != nil {
return nil, fmt.Errorf("freed.create posX error: %w", err)
}
posXTemplate, err := template.New("posX").Parse(posXString)
@@ -269,16 +236,9 @@ func init() {
return nil, err
}
posY, ok := params["posY"]
if !ok {
return nil, fmt.Errorf("freed.create requires a posY parameter")
}
posYString, ok := posY.(string)
if !ok {
return nil, fmt.Errorf("freed.create posY must be a string")
posYString, err := params.GetString("posY")
if err != nil {
return nil, fmt.Errorf("freed.create posY error: %w", err)
}
posYTemplate, err := template.New("posY").Parse(posYString)
@@ -287,16 +247,9 @@ func init() {
return nil, err
}
posZ, ok := params["posZ"]
if !ok {
return nil, fmt.Errorf("freed.create requires a posZ parameter")
}
posZString, ok := posZ.(string)
if !ok {
return nil, fmt.Errorf("freed.create posZ must be a string")
posZString, err := params.GetString("posZ")
if err != nil {
return nil, fmt.Errorf("freed.create posZ error: %w", err)
}
posZTemplate, err := template.New("posZ").Parse(posZString)
@@ -305,30 +258,16 @@ func init() {
return nil, err
}
zoom, ok := params["zoom"]
if !ok {
return nil, fmt.Errorf("freed.create requires an zoom parameter")
}
zoomString, ok := zoom.(string)
if !ok {
return nil, fmt.Errorf("freed.create zoom must be a string")
zoomString, err := params.GetString("zoom")
if err != nil {
return nil, fmt.Errorf("freed.create zoom error: %w", err)
}
zoomTemplate, err := template.New("zoom").Parse(zoomString)
focus, ok := params["focus"]
if !ok {
return nil, fmt.Errorf("freed.create requires an focus parameter")
}
focusString, ok := focus.(string)
if !ok {
return nil, fmt.Errorf("freed.create focus must be a string")
focusString, err := params.GetString("focus")
if err != nil {
return nil, fmt.Errorf("freed.create focus error: %w", err)
}
focusTemplate, err := template.New("focus").Parse(focusString)

View File

@@ -2,10 +2,10 @@ package processor
import (
"context"
"fmt"
"log/slog"
"errors"
freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -13,22 +13,22 @@ type FreeDDecode struct {
config config.ProcessorConfig
}
func (fdd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := payload.([]byte)
func (fd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := common.GetAnyAs[[]byte](payload)
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
}
func (fdd *FreeDDecode) Type() string {
return fdd.config.Type
func (fd *FreeDDecode) Type() string {
return fd.config.Type
}
func init() {

View File

@@ -2,9 +2,10 @@ package processor
import (
"context"
"fmt"
"errors"
freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -12,19 +13,19 @@ type FreeDEncode struct {
config config.ProcessorConfig
}
func (fde *FreeDEncode) Process(ctx context.Context, payload any) (any, error) {
payloadPosition, ok := payload.(freeD.FreeDPosition)
func (fe *FreeDEncode) Process(ctx context.Context, payload any) (any, error) {
payloadPosition, ok := common.GetAnyAs[freeD.FreeDPosition](payload)
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)
return payloadBytes, nil
}
func (fde *FreeDEncode) Type() string {
return fde.config.Type
func (fe *FreeDEncode) Type() string {
return fe.config.Type
}
func init() {

View File

@@ -1,82 +0,0 @@
package processor
import (
"bytes"
"context"
"fmt"
"net/http"
"text/template"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPRequestCreate struct {
config config.ProcessorConfig
Method string
URL *template.Template
}
func (hre *HTTPRequestCreate) Process(ctx context.Context, payload any) (any, error) {
var urlBuffer bytes.Buffer
err := hre.URL.Execute(&urlBuffer, payload)
if err != nil {
return nil, err
}
urlString := urlBuffer.String()
//TODO(jwetzell): support body
request, err := http.NewRequest(hre.Method, urlString, bytes.NewBuffer([]byte{}))
if err != nil {
return nil, err
}
return request, nil
}
func (hre *HTTPRequestCreate) Type() string {
return hre.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.create",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
method, ok := params["method"]
if !ok {
return nil, fmt.Errorf("http.request.create requires an method parameter")
}
methodString, ok := method.(string)
if !ok {
return nil, fmt.Errorf("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")
}
urlString, ok := url.(string)
if !ok {
return nil, fmt.Errorf("http.request.create url must be a string")
}
urlTemplate, err := template.New("url").Parse(urlString)
if err != nil {
return nil, err
}
return &HTTPRequestCreate{config: config, URL: urlTemplate, Method: methodString}, nil
},
})
}

View File

@@ -0,0 +1,93 @@
package processor
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"text/template"
"time"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPRequestDo struct {
config config.ProcessorConfig
client *http.Client
Method string
URL *template.Template
}
func (hrd *HTTPRequestDo) Process(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload)
var urlBuffer bytes.Buffer
err := hrd.URL.Execute(&urlBuffer, templateData)
if err != nil {
return nil, err
}
urlString := urlBuffer.String()
//TODO(jwetzell): support body
request, err := http.NewRequest(hrd.Method, urlString, bytes.NewBuffer([]byte{}))
if err != nil {
return nil, err
}
response, err := hrd.client.Do(request)
if err != nil {
return nil, err
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
//TODO(jwetzell): support headers, etc
return HTTPResponse{
Status: response.StatusCode,
Body: body,
}, nil
}
func (hrd *HTTPRequestDo) Type() string {
return hrd.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.do",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
methodString, err := params.GetString("method")
if err != nil {
return nil, fmt.Errorf("http.request.do method error: %w", err)
}
urlString, err := params.GetString("url")
if err != nil {
return nil, fmt.Errorf("http.request.do url error: %w", err)
}
urlTemplate, err := template.New("url").Parse(urlString)
if err != nil {
return nil, err
}
client := &http.Client{
Timeout: 10 * time.Second,
}
return &HTTPRequestDo{config: config, URL: urlTemplate, Method: methodString, client: client}, nil
},
})
}

View File

@@ -1,42 +0,0 @@
package processor
import (
"context"
"fmt"
"io"
"net/http"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPRequestEncode struct {
config config.ProcessorConfig
}
func (hre *HTTPRequestEncode) Process(ctx context.Context, payload any) (any, error) {
payloadRequest, ok := payload.(*http.Request)
if !ok {
return nil, fmt.Errorf("http.request.encode processor only accepts an http.Request")
}
bytes, err := io.ReadAll(payloadRequest.Body)
if err != nil {
return nil, err
}
return bytes, nil
}
func (hre *HTTPRequestEncode) Type() string {
return hre.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &HTTPRequestEncode{config: config}, nil
},
})
}

View File

@@ -1,80 +0,0 @@
package processor
import (
"context"
"fmt"
"net/http"
"regexp"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPRequestFilter struct {
config config.ProcessorConfig
Path *regexp.Regexp
Method string
}
func (hrf *HTTPRequestFilter) Process(ctx context.Context, payload any) (any, error) {
payloadRequest, ok := payload.(*http.Request)
if !ok {
return nil, fmt.Errorf("http.request.filter can only operate on http.Request payloads")
}
if hrf.Method != "" {
if payloadRequest.Method != hrf.Method {
return nil, nil
}
}
if !hrf.Path.MatchString(payloadRequest.URL.Path) {
return nil, nil
}
return payloadRequest, nil
}
func (hrf *HTTPRequestFilter) Type() string {
return hrf.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.filter",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
path, ok := params["path"]
if !ok {
return nil, fmt.Errorf("http.request.filter requires an path parameter")
}
pathString, ok := path.(string)
if !ok {
return nil, fmt.Errorf("http.request.filter path must be a string")
}
pathRegexp, err := regexp.Compile(fmt.Sprintf("^%s$", pathString))
if err != nil {
return nil, err
}
method, ok := params["method"]
if ok {
methodString, ok := method.(string)
if !ok {
return nil, fmt.Errorf("http.request.filter method must be a string")
}
return &HTTPRequestFilter{config: config, Path: pathRegexp, Method: methodString}, nil
}
return &HTTPRequestFilter{config: config, Path: pathRegexp}, nil
},
})
}

View File

@@ -0,0 +1,69 @@
package processor
import (
"bytes"
"context"
"fmt"
"text/template"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPResponseCreate struct {
Status int
BodyTmpl *template.Template
config config.ProcessorConfig
}
type HTTPResponse struct {
Status int
Body []byte
}
func (hrc *HTTPResponseCreate) Process(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload)
var bodyBuffer bytes.Buffer
err := hrc.BodyTmpl.Execute(&bodyBuffer, templateData)
if err != nil {
return nil, err
}
return HTTPResponse{
Status: hrc.Status,
Body: bodyBuffer.Bytes(),
}, nil
}
func (hrc *HTTPResponseCreate) Type() string {
return hrc.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.response.create",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
statusNum, err := params.GetInt("status")
if err != nil {
return nil, fmt.Errorf("http.response.create status error: %w", err)
}
bodyTemplateString, err := params.GetString("bodyTemplate")
if err != nil {
return nil, fmt.Errorf("http.response.create bodyTemplate error: %w", err)
}
bodyTemplate, err := template.New("body").Parse(bodyTemplateString)
if err != nil {
return nil, err
}
// TODO(jwetzell): support other body kind (direct bytes from input, from file?)
return &HTTPResponseCreate{config: config, Status: int(statusNum), BodyTmpl: bodyTemplate}, nil
},
})
}

View File

@@ -1,43 +0,0 @@
package processor
import (
"context"
"fmt"
"io"
"net/http"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPResponseEncode struct {
config config.ProcessorConfig
}
func (hre *HTTPResponseEncode) Process(ctx context.Context, payload any) (any, error) {
payloadResponse, ok := payload.(*http.Response)
if !ok {
return nil, fmt.Errorf("http.response.encode processor only accepts an http.Response")
}
defer payloadResponse.Body.Close()
bytes, err := io.ReadAll(payloadResponse.Body)
if err != nil {
return nil, err
}
return bytes, nil
}
func (hre *HTTPResponseEncode) Type() string {
return hre.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.response.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &HTTPResponseEncode{config: config}, nil
},
})
}

View File

@@ -2,25 +2,28 @@ package processor
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type IntParse struct {
config config.ProcessorConfig
Base int
BitSize int
config config.ProcessorConfig
}
func (ip *IntParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string)
payloadString, ok := common.GetAnyAs[string](payload)
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
}
@@ -34,8 +37,27 @@ func (ip *IntParse) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "int.parse",
New: func(config config.ProcessorConfig) (Processor, error) {
return &IntParse{config: config}, nil
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params
baseNum, err := params.GetInt("base")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
baseNum = 10
} else {
return nil, fmt.Errorf("int.parse base error: %w", err)
}
}
bitSizeNum, err := params.GetInt("bitSize")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
bitSizeNum = 64
} else {
return nil, fmt.Errorf("int.parse bitSize error: %w", err)
}
}
return &IntParse{config: moduleConfig, Base: baseNum, BitSize: bitSizeNum}, nil
},
})
}

View File

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

View File

@@ -0,0 +1,50 @@
package processor
import (
"context"
"errors"
"fmt"
"math/rand/v2"
"github.com/jwetzell/showbridge-go/internal/config"
)
type IntRandom struct {
Min int
Max int
config config.ProcessorConfig
}
func (ir *IntRandom) Process(ctx context.Context, payload any) (any, error) {
payloadInt := rand.IntN(ir.Max-ir.Min+1) + ir.Min
return payloadInt, nil
}
func (ir *IntRandom) Type() string {
return ir.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "int.random",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
minInt, err := params.GetInt("min")
if err != nil {
return nil, fmt.Errorf("int.random min error: %w", err)
}
maxInt, err := params.GetInt("max")
if err != nil {
return nil, fmt.Errorf("int.random max error: %w", err)
}
if maxInt < minInt {
return nil, errors.New("int.random max must be greater than min")
}
return &IntRandom{config: config, Min: int(minInt), Max: int(maxInt)}, nil
},
})
}

View File

@@ -0,0 +1,71 @@
package processor
import (
"context"
"errors"
"fmt"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type IntScale struct {
OutMin int
OutMax int
InMin int
InMax int
config config.ProcessorConfig
}
func (ir *IntScale) Process(ctx context.Context, payload any) (any, error) {
payloadInt, ok := common.GetAnyAs[int](payload)
if !ok {
return nil, errors.New("int.scale can only process an int")
}
payloadInt = (payloadInt-ir.InMin)*(ir.OutMax-ir.OutMin)/(ir.InMax-ir.InMin) + ir.OutMin
return payloadInt, nil
}
func (ir *IntScale) Type() string {
return ir.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "int.scale",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
inMinInt, err := params.GetInt("inMin")
if err != nil {
return nil, fmt.Errorf("int.scale inMin error: %w", err)
}
inMaxInt, err := params.GetInt("inMax")
if err != nil {
return nil, fmt.Errorf("int.scale inMax error: %w", err)
}
if inMaxInt < inMinInt {
return nil, errors.New("int.scale inMax must be greater than inMin")
}
outMinInt, err := params.GetInt("outMin")
if err != nil {
return nil, fmt.Errorf("int.scale outMin error: %w", err)
}
outMaxInt, err := params.GetInt("outMax")
if err != nil {
return nil, fmt.Errorf("int.scale outMax error: %w", err)
}
if outMaxInt < outMinInt {
return nil, errors.New("int.scale outMax must be greater than outMin")
}
return &IntScale{config: config, InMin: inMinInt, InMax: inMaxInt, OutMin: outMinInt, OutMax: outMaxInt}, nil
},
})
}

View File

@@ -0,0 +1,50 @@
package processor
import (
"context"
"encoding/json"
"errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type JsonDecode struct {
config config.ProcessorConfig
}
func (jd *JsonDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
if !ok {
payloadString, ok := common.GetAnyAs[string](payload)
if !ok {
return nil, errors.New("json.decode can only process a string or []byte")
}
payloadBytes = []byte(payloadString)
}
payloadJson := make(map[string]any)
err := json.Unmarshal(payloadBytes, &payloadJson)
if err != nil {
return nil, err
}
return payloadJson, nil
}
func (jd *JsonDecode) Type() string {
return jd.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "json.decode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &JsonDecode{config: config}, nil
},
})
}

View File

@@ -0,0 +1,42 @@
package processor
import (
"bytes"
"context"
"encoding/json"
"github.com/jwetzell/showbridge-go/internal/config"
)
type JsonEncode struct {
config config.ProcessorConfig
}
func (je *JsonEncode) Process(ctx context.Context, payload any) (any, error) {
var payloadBuffer bytes.Buffer
err := json.NewEncoder(&payloadBuffer).Encode(payload)
if err != nil {
return nil, err
}
payloadBytes := payloadBuffer.Bytes()
payloadBytes = payloadBytes[0 : len(payloadBytes)-1]
return payloadBytes, nil
}
func (je *JsonEncode) Type() string {
return je.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "json.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &JsonEncode{config: config}, nil
},
})
}

View File

@@ -0,0 +1,324 @@
//go:build cgo
package processor
import (
"bytes"
"context"
"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 (mmc *MIDIMessageCreate) Process(ctx context.Context, payload any) (any, error) {
return mmc.ProcessFunc(ctx, payload)
}
func (mmc *MIDIMessageCreate) Type() string {
return mmc.config.Type
}
func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
params := config.Params
channelString, err := params.GetString("channel")
if err != nil {
return nil, fmt.Errorf("midi.message.create channel error: %w", err)
}
channelTemplate, err := template.New("channel").Parse(channelString)
if err != nil {
return nil, err
}
noteString, err := params.GetString("note")
if err != nil {
return nil, fmt.Errorf("midi.message.create note error: %w", err)
}
noteTemplate, err := template.New("note").Parse(noteString)
if err != nil {
return nil, err
}
velocityString, err := params.GetString("velocity")
if err != nil {
return nil, fmt.Errorf("midi.message.create velocity error: %w", err)
}
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) {
templateData := GetTemplateData(ctx, payload)
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil {
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var noteBuffer bytes.Buffer
err = noteTemplate.Execute(&noteBuffer, templateData)
if err != nil {
return nil, err
}
noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8)
var velocityBuffer bytes.Buffer
err = velocityTemplate.Execute(&velocityBuffer, templateData)
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
channelString, err := params.GetString("channel")
if err != nil {
return nil, fmt.Errorf("midi.message.create channel error: %w", err)
}
channelTemplate, err := template.New("channel").Parse(channelString)
if err != nil {
return nil, err
}
noteString, err := params.GetString("note")
if err != nil {
return nil, fmt.Errorf("midi.message.create note error: %w", err)
}
noteTemplate, err := template.New("note").Parse(noteString)
if err != nil {
return nil, err
}
velocityString, err := params.GetString("velocity")
if err != nil {
return nil, fmt.Errorf("midi.message.create velocity error: %w", err)
}
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) {
templateData := GetTemplateData(ctx, payload)
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil {
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var noteBuffer bytes.Buffer
err = noteTemplate.Execute(&noteBuffer, templateData)
if err != nil {
return nil, err
}
noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8)
var velocityBuffer bytes.Buffer
err = velocityTemplate.Execute(&velocityBuffer, templateData)
if err != nil {
return nil, err
}
velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8)
payloadMessage := midi.NoteOffVelocity(uint8(channelValue), uint8(noteValue), uint8(velocityValue))
return payloadMessage, nil
}}, nil
}
func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error) {
params := config.Params
channelString, err := params.GetString("channel")
if err != nil {
return nil, fmt.Errorf("midi.message.create channel error: %w", err)
}
channelTemplate, err := template.New("channel").Parse(channelString)
if err != nil {
return nil, err
}
controlString, err := params.GetString("control")
if err != nil {
return nil, fmt.Errorf("midi.message.create control error: %w", err)
}
controlTemplate, err := template.New("control").Parse(controlString)
if err != nil {
return nil, err
}
valueString, err := params.GetString("value")
if err != nil {
return nil, fmt.Errorf("midi.message.create value error: %w", err)
}
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) {
templateData := GetTemplateData(ctx, payload)
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil {
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var controlBuffer bytes.Buffer
err = controlTemplate.Execute(&controlBuffer, templateData)
if err != nil {
return nil, err
}
controlValue, err := strconv.ParseUint(controlBuffer.String(), 10, 8)
var valueBuffer bytes.Buffer
err = valueTemplate.Execute(&valueBuffer, templateData)
if err != nil {
return nil, err
}
valueValue, err := strconv.ParseUint(valueBuffer.String(), 10, 8)
payloadMessage := midi.ControlChange(uint8(channelValue), uint8(controlValue), uint8(valueValue))
return payloadMessage, nil
}}, nil
}
func newMidiProgramChangeCreate(config config.ProcessorConfig) (Processor, error) {
params := config.Params
channelString, err := params.GetString("channel")
if err != nil {
return nil, fmt.Errorf("midi.message.create channel error: %w", err)
}
channelTemplate, err := template.New("channel").Parse(channelString)
if err != nil {
return nil, err
}
programString, err := params.GetString("program")
if err != nil {
return nil, fmt.Errorf("midi.message.create program error: %w", err)
}
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) {
templateData := GetTemplateData(ctx, payload)
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil {
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var programBuffer bytes.Buffer
err = programTemplate.Execute(&programBuffer, templateData)
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
msgTypeString, err := params.GetString("type")
if err != nil {
return nil, fmt.Errorf("midi.message.create type error: %w", err)
}
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,8 +4,9 @@ package processor
import (
"context"
"fmt"
"errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
@@ -15,10 +16,10 @@ type MIDIMessageDecode struct {
}
func (mmd *MIDIMessageDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := payload.([]byte)
payloadBytes, ok := common.GetAnyAs[[]byte](payload)
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,8 +4,9 @@ package processor
import (
"context"
"fmt"
"errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
@@ -15,10 +16,10 @@ type MIDIMessageEncode struct {
}
func (mme *MIDIMessageEncode) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := payload.(midi.Message)
payloadMessage, ok := common.GetAnyAs[midi.Message](payload)
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 a midi.Message")
}
return payloadMessage.Bytes(), nil

View File

@@ -0,0 +1,92 @@
//go:build cgo
package processor
import (
"context"
"errors"
"fmt"
"github.com/jwetzell/showbridge-go/internal/common"
"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 := common.GetAnyAs[midi.Message](payload)
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,17 @@ package processor
import (
"context"
"errors"
"fmt"
"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 +20,54 @@ type MQTTMessageCreate struct {
Topic string
QoS byte
Retained bool
Payload any
Payload []byte
}
func NewMQTTMessage(topic string, qos byte, retained bool, payload []byte) MQTTMessage {
return MQTTMessage{
topic: topic,
qos: qos,
retained: retained,
payload: payload,
}
}
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) {
// TODO(jwetzell): support templating
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
@@ -41,52 +80,40 @@ func (mmc *MQTTMessageCreate) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "mqtt.message.create",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
topic, ok := params["topic"]
if !ok {
return nil, fmt.Errorf("mqtt.message.create requires an topic parameter")
New: func(processorConfig config.ProcessorConfig) (Processor, error) {
params := processorConfig.Params
topicString, err := params.GetString("topic")
if err != nil {
return nil, fmt.Errorf("mqtt.message.create topic error: %w", err)
}
topicString, ok := topic.(string)
if !ok {
return nil, fmt.Errorf("mqtt.message.create topic must be a string")
qosByte, err := params.GetInt("qos")
if err != nil {
return nil, fmt.Errorf("mqtt.message.create qos error: %w", err)
}
qos, ok := params["qos"]
if !ok {
return nil, fmt.Errorf("mqtt.message.create requires an qos parameter")
}
qosByte, ok := qos.(float64)
if !ok {
return nil, fmt.Errorf("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")
}
retainedBool, ok := retained.(bool)
if !ok {
return nil, fmt.Errorf("mqtt.message.create retained must be a boolean")
retainedBool, err := params.GetBool("retained")
if err != nil {
return nil, fmt.Errorf("mqtt.message.create retained error: %w", err)
}
//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")
payloadString, err := params.GetString("payload")
if err != nil {
if errors.Is(err, config.ErrParamNotString) {
payloadBytes, err := params.GetByteSlice("payload")
if err != nil {
return nil, fmt.Errorf("mqtt.message.create payload error: %w", err)
}
return &MQTTMessageCreate{config: processorConfig, Topic: topicString, QoS: byte(qosByte), Retained: retainedBool, Payload: payloadBytes}, nil
} else {
return nil, fmt.Errorf("mqtt.message.create payload error: %w", err)
}
}
return &MQTTMessageCreate{config: config, Topic: topicString, QoS: byte(qosByte), Retained: retainedBool, Payload: payload}, nil
payloadBytes := []byte(payloadString)
return &MQTTMessageCreate{config: processorConfig, Topic: topicString, QoS: byte(qosByte), Retained: retainedBool, Payload: payloadBytes}, nil
},
})
}

View File

@@ -2,9 +2,10 @@ package processor
import (
"context"
"fmt"
"errors"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -13,10 +14,10 @@ type MQTTMessageEncode struct {
}
func (mme *MQTTMessageEncode) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := payload.(mqtt.Message)
payloadMessage, ok := common.GetAnyAs[mqtt.Message](payload)
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

Some files were not shown because too many files have changed in this diff Show More