├── .devcontainer └── devcontainer.json ├── .eslintrc.cjs ├── .github ├── FUNDING.yml ├── dependabot.yml ├── labeler.yml ├── release.yml └── workflows │ ├── check-docs-build.yml │ ├── check-format-lint.yml │ ├── check-webui-build.yml │ ├── label-pr.yml │ ├── publish-cli-image.yml │ ├── publish-cli-npm.yml │ ├── publish-cloud-image.yml │ ├── publish-cloud-npm.yml │ ├── publish-demo-webui.yml │ ├── publish-docs.yml │ ├── publish-launcher.yml │ ├── publish-lib-npm.yml │ ├── publish-schema-docs.yml │ ├── publish-site.yml │ ├── publish-types-npm.yml │ ├── publish-webui-npm.yml │ └── run-lib-tests.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc.cjs ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cli ├── .dockerignore ├── .gitignore ├── Dockerfile ├── main.js ├── package-lock.json ├── package.json └── setup_dev.js ├── cloud ├── .dockerignore ├── .gitignore ├── .prettierignore ├── Dockerfile ├── README.md ├── package-lock.json ├── package.json └── src │ ├── main.js │ └── utils.js ├── docs ├── .gitignore ├── astro.config.mjs ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── index.md │ │ │ ├── reference │ │ │ ├── actions.md │ │ │ ├── config.md │ │ │ ├── dictionary.md │ │ │ ├── messages.md │ │ │ ├── params.md │ │ │ ├── structure.md │ │ │ ├── templating.mdx │ │ │ ├── transforms.md │ │ │ └── triggers.md │ │ │ ├── run │ │ │ ├── cli.md │ │ │ ├── desktop.md │ │ │ ├── docker.md │ │ │ └── source.mdx │ │ │ └── showbridge │ │ │ ├── cloud.md │ │ │ └── overview.md │ └── env.d.ts └── tsconfig.json ├── launcher ├── build │ └── entitlements.mac.plist ├── electron_bundle.js ├── package-lock.json ├── package.json ├── src │ ├── assets │ │ ├── css │ │ │ └── index.css │ │ ├── images │ │ │ ├── icon16x16.png │ │ │ └── icon512x512.png │ │ └── js │ │ │ ├── dateFormat.js │ │ │ ├── logger.js │ │ │ └── main.js │ ├── html │ │ ├── logger.html │ │ └── main.html │ ├── main.js │ └── preload.js ├── tailwind.config.js └── tailwind.css ├── lib ├── package-lock.json ├── package.json ├── src │ ├── actions │ │ ├── action.ts │ │ ├── cloud-output-action.ts │ │ ├── delay-action.ts │ │ ├── forward-action.ts │ │ ├── http-request-action.ts │ │ ├── http-response-action.ts │ │ ├── index.ts │ │ ├── log-action.ts │ │ ├── midi-output-action.ts │ │ ├── mqtt-output-action.ts │ │ ├── osc-output-action.ts │ │ ├── random-action.ts │ │ ├── shell-action.ts │ │ ├── store-action.ts │ │ ├── tcp-output-action.ts │ │ └── udp-output-action.ts │ ├── config.ts │ ├── index.ts │ ├── messages │ │ ├── http-message.ts │ │ ├── index.ts │ │ ├── midi-message.ts │ │ ├── mqtt-message.ts │ │ ├── osc-message.ts │ │ ├── tcp-message.ts │ │ ├── udp-message.ts │ │ └── websocket-message.ts │ ├── protocols │ │ ├── cloud-protocol.ts │ │ ├── http-protocol.ts │ │ ├── index.ts │ │ ├── midi-protocol.ts │ │ ├── mqtt-protocol.ts │ │ ├── protocol.ts │ │ ├── tcp-protocol.ts │ │ └── udp-protocol.ts │ ├── router.ts │ ├── transforms │ │ ├── floor-transform.ts │ │ ├── index.ts │ │ ├── log-transform.ts │ │ ├── map-transform.ts │ │ ├── power-transform.ts │ │ ├── round-transform.ts │ │ ├── scale-transform.ts │ │ ├── template-transform.ts │ │ └── transform.ts │ ├── triggers │ │ ├── any-trigger.ts │ │ ├── bytes-equal-trigger.ts │ │ ├── http-request-trigger.ts │ │ ├── index.ts │ │ ├── midi-control-change-trigger.ts │ │ ├── midi-note-off-trigger.ts │ │ ├── midi-note-on-trigger.ts │ │ ├── midi-pitch-bend-trigger.ts │ │ ├── midi-program-change-trigger.ts │ │ ├── midi-trigger.ts │ │ ├── mqtt-topic-trigger.ts │ │ ├── osc-address-trigger.ts │ │ ├── regex-trigger.ts │ │ ├── sender-trigger.ts │ │ └── trigger.ts │ └── utils │ │ ├── disabling.ts │ │ ├── index.ts │ │ ├── logging.ts │ │ ├── migrations.ts │ │ └── templating.ts ├── tests │ ├── config │ │ ├── bad_config.json │ │ ├── cloud_single_room_config.json │ │ ├── config.test.js │ │ └── good_config.json │ ├── messages │ │ ├── http-message.test.js │ │ ├── midi-message.test.js │ │ ├── mqtt-message.test.js │ │ ├── osc-message.test.js │ │ ├── tcp-message.test.js │ │ ├── udp-message.test.js │ │ └── websocket-message.test.js │ ├── transforms │ │ ├── floor-transform.test.js │ │ ├── log-transform.test.js │ │ ├── map-transform.test.js │ │ ├── power-transform.test.js │ │ ├── round-transform.test.js │ │ ├── scale-transform.test.js │ │ ├── template-transform.test.js │ │ └── transform.test.js │ ├── triggers │ │ ├── any-trigger.test.js │ │ ├── bytes-equal-trigger.test.js │ │ ├── http-request-trigger.test.js │ │ ├── midi-control-change-trigger.test.js │ │ ├── midi-note-off-trigger.test.js │ │ ├── midi-note-on-trigger.test.js │ │ ├── midi-pitch-bend-trigger.test.js │ │ ├── midi-program-change-trigger.test.js │ │ ├── midi-trigger.test.js │ │ ├── mqtt-topic-trigger.test.js │ │ ├── osc-address-trigger.test.js │ │ ├── regex-trigger.test.js │ │ ├── sender-trigger.test.js │ │ └── trigger.test.js │ └── utils │ │ ├── index.test.js │ │ └── templating.test.js └── tsconfig.json ├── package-lock.json ├── package.json ├── sample ├── config │ ├── all-to-cloud.json │ ├── default.json │ ├── dev.json │ ├── midi-to-cue.json │ └── midi-to-visca.json ├── snippet │ └── triggers │ │ └── http │ │ └── http-to-visca.json └── vars │ ├── default.json │ └── dev.json ├── schema └── config.schema.json ├── scripts ├── check_format_lint.js ├── install_all.js └── setup_dev.js ├── site ├── package-lock.json ├── package.json ├── src │ ├── assets │ │ ├── images │ │ │ ├── favicon.ico │ │ │ └── icon512x512.png │ │ └── js │ │ │ └── tracking.js │ └── index.html ├── tailwind.config.js └── tailwind.css ├── types ├── package-lock.json ├── package.json ├── src │ ├── index.ts │ └── models │ │ ├── action.ts │ │ ├── config.ts │ │ ├── params │ │ ├── actions.ts │ │ ├── index.ts │ │ ├── protocols.ts │ │ ├── transforms.ts │ │ └── triggers.ts │ │ ├── protocol.ts │ │ ├── router.ts │ │ ├── sender.ts │ │ ├── transform.ts │ │ └── trigger.ts └── tsconfig.json └── webui ├── .postcssrc.json ├── angular.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ ├── action │ │ │ ├── action.component.css │ │ │ ├── action.component.html │ │ │ └── action.component.ts │ │ ├── array-form │ │ │ ├── array-form.component.css │ │ │ ├── array-form.component.html │ │ │ └── array-form.component.ts │ │ ├── clipboard-dialog │ │ │ ├── clipboard-dialog.component.css │ │ │ ├── clipboard-dialog.component.html │ │ │ └── clipboard-dialog.component.ts │ │ ├── config │ │ │ ├── config.component.css │ │ │ ├── config.component.html │ │ │ └── config.component.ts │ │ ├── import-json │ │ │ ├── import-json.component.css │ │ │ ├── import-json.component.html │ │ │ └── import-json.component.ts │ │ ├── message-type │ │ │ ├── message-type.component.css │ │ │ ├── message-type.component.html │ │ │ └── message-type.component.ts │ │ ├── midi-info-dialog │ │ │ ├── midi-info-dialog.component.css │ │ │ ├── midi-info-dialog.component.html │ │ │ └── midi-info-dialog.component.ts │ │ ├── params-form │ │ │ ├── params-form.component.css │ │ │ ├── params-form.component.html │ │ │ └── params-form.component.ts │ │ ├── patch-editor │ │ │ ├── patch-editor.component.css │ │ │ ├── patch-editor.component.html │ │ │ └── patch-editor.component.ts │ │ ├── transform │ │ │ ├── transform.component.css │ │ │ ├── transform.component.html │ │ │ └── transform.component.ts │ │ └── trigger │ │ │ ├── trigger.component.css │ │ │ ├── trigger.component.html │ │ │ └── trigger.component.ts │ ├── models │ │ ├── config.models.ts │ │ ├── copy-object.model.ts │ │ ├── events.model.ts │ │ ├── form.model.ts │ │ └── template.model.ts │ ├── services │ │ ├── config.service.ts │ │ ├── copy.service.ts │ │ ├── event.service.ts │ │ ├── lists.service.ts │ │ ├── schema.service.ts │ │ ├── settings.service.ts │ │ └── vars.service.ts │ └── utils │ │ └── utils.ts ├── assets │ ├── .gitkeep │ ├── css │ │ ├── fonts.css │ │ └── styles.css │ ├── fonts │ │ ├── KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmEU9fBBc4.woff2 │ │ ├── KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmSU5fBBc4.woff2 │ │ ├── KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2 │ │ ├── KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2 │ │ ├── KFOmCnqEu92Fr1Mu4WxKOzY.woff2 │ │ ├── KFOmCnqEu92Fr1Mu4mxK.woff2 │ │ ├── KFOmCnqEu92Fr1Mu5mxKOzY.woff2 │ │ ├── KFOmCnqEu92Fr1Mu72xKOzY.woff2 │ │ ├── KFOmCnqEu92Fr1Mu7GxKOzY.woff2 │ │ ├── KFOmCnqEu92Fr1Mu7WxKOzY.woff2 │ │ ├── KFOmCnqEu92Fr1Mu7mxKOzY.woff2 │ │ ├── flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2 │ │ └── gok-H7zzDkdnRel8-DQ6KAXJ69wP1tGnf4ZGhUce.woff2 │ └── images │ │ └── favicon.ico ├── index.demo.html ├── index.html └── main.ts ├── tailwind.config.js ├── tailwind.css ├── theme.scss ├── tsconfig.app.json └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/javascript-node:20" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | commonjs: true, 5 | es2021: true, 6 | }, 7 | extends: ['airbnb', 'prettier'], 8 | parserOptions: { 9 | ecmaVersion: 'latest', 10 | }, 11 | rules: { 12 | 'prefer-destructuring': 'off', 13 | 'no-bitwise': 'off', 14 | 'class-methods-use-this': 'off', 15 | 'import/extensions': 'off', 16 | 'import/no-relative-packages': 'off', 17 | 'import/no-cycle': 'off', 18 | 'no-underscore-dangle': 'warn', 19 | }, 20 | ignorePatterns: ['dist'], 21 | }; 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: [jwetzell] 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # labels 4 | - package-ecosystem: 'npm' 5 | directory: '/cli' 6 | schedule: 7 | interval: 'weekly' 8 | ignore: 9 | - dependency-name: '@showbridge/lib' 10 | - dependency-name: '@showbridge/webui' 11 | - package-ecosystem: 'npm' 12 | directory: '/lib' 13 | schedule: 14 | interval: 'weekly' 15 | ignore: 16 | - dependency-name: '@showbridge/types' 17 | - package-ecosystem: 'npm' 18 | directory: '/launcher' 19 | schedule: 20 | interval: 'weekly' 21 | ignore: 22 | - dependency-name: '@showbridge/cli' 23 | - package-ecosystem: 'npm' 24 | directory: '/site' 25 | schedule: 26 | interval: 'weekly' 27 | - package-ecosystem: 'npm' 28 | directory: '/webui' 29 | schedule: 30 | interval: 'weekly' 31 | ignore: 32 | - dependency-name: '@showbridge/types' 33 | groups: 34 | angular: 35 | applies-to: version-updates 36 | patterns: 37 | - '@angular*' 38 | update-types: 39 | - 'minor' 40 | - 'patch' 41 | - package-ecosystem: 'npm' 42 | directory: '/cloud' 43 | schedule: 44 | interval: 'weekly' 45 | - package-ecosystem: 'npm' 46 | directory: '/docs' 47 | schedule: 48 | interval: 'weekly' 49 | - package-ecosystem: 'npm' 50 | directory: '/types' 51 | schedule: 52 | interval: 'weekly' 53 | - package-ecosystem: 'github-actions' 54 | directory: '/' 55 | schedule: 56 | interval: 'weekly' 57 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | cli: 2 | - changed-files: 3 | - any-glob-to-any-file: 'cli/**' 4 | 5 | cloud: 6 | - changed-files: 7 | - any-glob-to-any-file: 'cloud/**' 8 | 9 | docs: 10 | - changed-files: 11 | - any-glob-to-any-file: 'docs/**' 12 | 13 | launcher: 14 | - changed-files: 15 | - any-glob-to-any-file: 'launcher/**' 16 | 17 | lib: 18 | - changed-files: 19 | - any-glob-to-any-file: 'lib/**' 20 | 21 | schema: 22 | - changed-files: 23 | - any-glob-to-any-file: 'schema/**' 24 | 25 | site: 26 | - changed-files: 27 | - any-glob-to-any-file: 'site/**' 28 | 29 | types: 30 | - changed-files: 31 | - any-glob-to-any-file: 'types/**' 32 | 33 | webui: 34 | - changed-files: 35 | - any-glob-to-any-file: 'webui/**' 36 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | categories: 6 | - title: Cloud ☁️ 7 | labels: 8 | - cloud 9 | - title: Core Library 🛠 10 | labels: 11 | - lib 12 | - title: Electron Launcher 🖥️ 13 | labels: 14 | - launcher 15 | - title: Config Schema {} 16 | labels: 17 | - schema 18 | - title: CLI ⌨️ 19 | labels: 20 | - cli 21 | - title: Web UI 🌐 22 | labels: 23 | - webui 24 | - title: Docs 📄 25 | labels: 26 | - docs 27 | - title: Other Changes 28 | labels: 29 | - '*' 30 | -------------------------------------------------------------------------------- /.github/workflows/check-docs-build.yml: -------------------------------------------------------------------------------- 1 | name: Check @showbridge/docs builds 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | paths: 7 | - 'docs/**' 8 | jobs: 9 | build-docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out repository 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 17 | with: 18 | node-version-file: '.nvmrc' 19 | cache: 'npm' 20 | cache-dependency-path: 'docs/package-lock.json' 21 | 22 | - name: Install Node.js dependencies 23 | run: npm ci 24 | working-directory: ./docs 25 | 26 | - run: npm run build 27 | working-directory: ./docs 28 | -------------------------------------------------------------------------------- /.github/workflows/check-format-lint.yml: -------------------------------------------------------------------------------- 1 | name: Check formatting and linting rules 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | run-check-script: 9 | name: Run Format Check Script 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out repository 14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 18 | with: 19 | node-version-file: '.nvmrc' 20 | cache: 'npm' 21 | cache-dependency-path: '**/package-lock.json' 22 | 23 | - name: Install root Node.js dependencies 24 | run: npm ci 25 | 26 | - name: Install all dependencies 27 | run: npm run install:all 28 | 29 | - name: Build lib 30 | run: npm run build 31 | working-directory: ./lib 32 | 33 | - name: Install eslint 34 | run: npm install -g eslint@8.57.0 35 | 36 | - name: Install prettier 37 | run: npm install -g prettier@3.2.5 38 | 39 | - name: Run check script 40 | run: node scripts/check_format_lint.js 41 | -------------------------------------------------------------------------------- /.github/workflows/check-webui-build.yml: -------------------------------------------------------------------------------- 1 | name: Check @showbridge/webui builds 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | paths: 7 | - 'webui/**' 8 | jobs: 9 | build-webui: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out repository 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 17 | with: 18 | node-version-file: '.nvmrc' 19 | cache: 'npm' 20 | cache-dependency-path: 'webui/package-lock.json' 21 | 22 | - name: Install Node.js dependencies 23 | run: npm ci 24 | working-directory: ./webui 25 | 26 | - run: npm run build 27 | working-directory: ./webui 28 | -------------------------------------------------------------------------------- /.github/workflows/label-pr.yml: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/go-gitea/gitea 2 | name: Add labels to PR 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize, reopened] 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 8 | cancel-in-progress: true 9 | jobs: 10 | labeler: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | pull-requests: write 15 | steps: 16 | - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 17 | with: 18 | sync-labels: true 19 | -------------------------------------------------------------------------------- /.github/workflows/publish-cli-image.yml: -------------------------------------------------------------------------------- 1 | name: Publish cli docker image 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - 'cli/**' 9 | tags: 10 | - 'cli/v**' 11 | jobs: 12 | build-cli-docker: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out repository 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | 18 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 19 | with: 20 | node-version-file: '.nvmrc' 21 | registry-url: 'https://registry.npmjs.org' 22 | cache: 'npm' 23 | cache-dependency-path: 'cli/package-lock.json' 24 | 25 | - name: install Node.js dependencies 26 | run: npm ci 27 | working-directory: ./cli 28 | 29 | - name: setup cli folder 30 | run: npm run build 31 | working-directory: ./cli 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 38 | 39 | - name: Login to Docker Hub 40 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 41 | with: 42 | username: ${{ secrets.DOCKERHUB_USERNAME }} 43 | password: ${{ secrets.DOCKERHUB_TOKEN }} 44 | 45 | - name: Setup Docker metadata 46 | id: meta 47 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 48 | with: 49 | images: | 50 | jwetzell/showbridge 51 | tags: | 52 | type=ref,event=branch 53 | type=match,pattern=cli/v(\d+.\d+.\d+),group=1 54 | 55 | - name: Build and push 56 | uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 57 | with: 58 | push: true 59 | context: ./cli 60 | file: ./cli/Dockerfile 61 | tags: jwetzell/showbridge:main 62 | cache-from: type=gha 63 | cache-to: type=gha,mode=max 64 | platforms: linux/amd64,linux/arm64,linux/arm/v7 65 | -------------------------------------------------------------------------------- /.github/workflows/publish-cli-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish @showbridge/cli to npmjs 2 | on: 3 | push: 4 | tags: 5 | - 'cli/**' 6 | jobs: 7 | publish-cli: 8 | permissions: 9 | id-token: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 14 | with: 15 | node-version-file: '.nvmrc' 16 | registry-url: 'https://registry.npmjs.org' 17 | cache: 'npm' 18 | cache-dependency-path: 'cli/package-lock.json' 19 | 20 | - name: Install Node.js dependencies 21 | run: npm ci 22 | working-directory: ./cli 23 | 24 | - name: Publish to NPM 25 | run: npm publish --provenance --access=public 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | working-directory: ./cli 29 | -------------------------------------------------------------------------------- /.github/workflows/publish-cloud-image.yml: -------------------------------------------------------------------------------- 1 | name: Publish cloud docker image 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - 'cloud/**' 9 | tags: 10 | - 'cloud/v*' 11 | jobs: 12 | build-cloud-docker: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out repository 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 23 | 24 | - name: Login to Docker Hub 25 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 26 | with: 27 | username: ${{ secrets.DOCKERHUB_USERNAME }} 28 | password: ${{ secrets.DOCKERHUB_TOKEN }} 29 | 30 | - name: Setup Docker metadata 31 | id: meta 32 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 33 | with: 34 | images: | 35 | jwetzell/showbridge-cloud 36 | tags: | 37 | type=ref,event=branch 38 | type=match,pattern=cloud/v(\d+.\d+.\d+),group=1 39 | 40 | - name: Build and push 41 | uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 42 | with: 43 | push: true 44 | context: ./cloud/ 45 | file: ./cloud/Dockerfile 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | cache-from: type=gha 49 | cache-to: type=gha,mode=max 50 | platforms: linux/amd64,linux/arm64,linux/arm/v7 51 | -------------------------------------------------------------------------------- /.github/workflows/publish-cloud-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish @showbridge/cloud to npmjs 2 | on: 3 | push: 4 | tags: 5 | - 'cloud/**' 6 | jobs: 7 | publish-cloud: 8 | permissions: 9 | id-token: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 14 | with: 15 | node-version-file: '.nvmrc' 16 | registry-url: 'https://registry.npmjs.org' 17 | cache: 'npm' 18 | cache-dependency-path: 'cloud/package-lock.json' 19 | - name: Install Node.js dependencies 20 | run: npm ci 21 | working-directory: ./cloud 22 | - name: Publish to NPM 23 | run: npm publish --provenance --access=public 24 | working-directory: ./cloud 25 | env: 26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/publish-demo-webui.yml: -------------------------------------------------------------------------------- 1 | name: Publish webui to demo site 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'webui/**' 9 | - 'schema/**' 10 | - 'sample/config/default.json' 11 | jobs: 12 | build-webui: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: check out repository 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | 18 | - name: set up Node.js 19 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 20 | with: 21 | node-version-file: '.nvmrc' 22 | cache: 'npm' 23 | cache-dependency-path: 'webui/package-lock.json' 24 | 25 | - name: install Node.js dependencies 26 | run: npm ci 27 | working-directory: ./webui 28 | 29 | - name: build demo site 30 | run: npm run build:demo 31 | working-directory: ./webui 32 | 33 | - name: copy default config 34 | run: cp sample/config/default.json ./webui/dist/webui/browser/config.json 35 | 36 | - name: copy schema 37 | run: cp schema/config.schema.json ./webui/dist/webui/browser/config.schema.json 38 | 39 | - name: Publish demo webui 40 | uses: SamKirkland/FTP-Deploy-Action@8e83cea8672e3fbcbb9fdafff34debf6ae4c5f65 # v4.3.5 41 | with: 42 | server: ${{secrets.FTP_URL}} 43 | username: ${{secrets.DEMO_FTP_USERNAME}} 44 | password: ${{secrets.DEMO_FTP_PASSWORD}} 45 | local-dir: ./webui/dist/webui/browser/ 46 | server-dir: / 47 | port: 21 48 | protocol: ftps 49 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish @showbridge/docs to docs site 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - 'docs/**' 7 | jobs: 8 | build-docs: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 12 | 13 | - name: Set up Node.js 14 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 15 | with: 16 | node-version-file: '.nvmrc' 17 | cache: 'npm' 18 | cache-dependency-path: 'docs/package-lock.json' 19 | 20 | - name: Install Node.js dependencies 21 | run: npm ci 22 | working-directory: ./docs 23 | 24 | - name: build docs 25 | run: npm run build:prod 26 | working-directory: ./docs 27 | 28 | - name: publish docs 29 | uses: SamKirkland/FTP-Deploy-Action@8e83cea8672e3fbcbb9fdafff34debf6ae4c5f65 # v4.3.5 30 | with: 31 | server: ${{secrets.FTP_URL}} 32 | username: ${{secrets.DOCS_FTP_USERNAME}} 33 | password: ${{secrets.DOCS_FTP_PASSWORD}} 34 | local-dir: ./docs/dist/ 35 | server-dir: docs/ 36 | port: 21 37 | protocol: ftps 38 | -------------------------------------------------------------------------------- /.github/workflows/publish-launcher.yml: -------------------------------------------------------------------------------- 1 | name: Publish electron launcher 2 | on: 3 | workflow_dispatch: 4 | 5 | env: 6 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 7 | 8 | jobs: 9 | build-linux: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 14 | with: 15 | node-version-file: '.nvmrc' 16 | cache: 'npm' 17 | cache-dependency-path: 'launcher/package-lock.json' 18 | 19 | - run: sudo apt-get install -y libasound2-dev 20 | 21 | - name: Install launcher Node.js Dependencies 22 | run: npm ci 23 | working-directory: './launcher' 24 | 25 | - run: npm run release 26 | working-directory: ./launcher 27 | 28 | build-windows: 29 | runs-on: windows-latest 30 | steps: 31 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 32 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 33 | with: 34 | node-version-file: '.nvmrc' 35 | cache: 'npm' 36 | cache-dependency-path: 'launcher/package-lock.json' 37 | 38 | - name: Install launcher Node.js Dependencies 39 | run: npm ci 40 | working-directory: './launcher' 41 | 42 | - run: npm run release 43 | working-directory: ./launcher 44 | 45 | build-macos: 46 | runs-on: macos-latest 47 | env: 48 | CSC_LINK: ${{ secrets.MACOS_CSC_LINK }} 49 | CSC_KEY_PASSWORD: ${{ secrets.MACOS_CSC_KEY_PASSWORD }} 50 | APPLE_ID: ${{ secrets.APPLE_ID }} 51 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 52 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} 53 | steps: 54 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 55 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 56 | with: 57 | node-version-file: '.nvmrc' 58 | cache: 'npm' 59 | cache-dependency-path: 'launcher/package-lock.json' 60 | 61 | - name: Install launcher Node.js Dependencies 62 | run: npm ci 63 | working-directory: './launcher' 64 | 65 | - run: npm run release 66 | working-directory: ./launcher 67 | -------------------------------------------------------------------------------- /.github/workflows/publish-lib-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish @showbridge/lib to npmjs 2 | on: 3 | push: 4 | tags: 5 | - 'lib/**' 6 | jobs: 7 | publish-lib: 8 | permissions: 9 | id-token: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 14 | with: 15 | node-version-file: '.nvmrc' 16 | registry-url: 'https://registry.npmjs.org' 17 | cache: 'npm' 18 | cache-dependency-path: 'lib/package-lock.json' 19 | - name: Install Node.js dependencies 20 | run: npm ci 21 | working-directory: ./lib 22 | - name: Publish to NPM 23 | run: npm publish --provenance --access=public 24 | working-directory: ./lib 25 | env: 26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/publish-schema-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish schema docs to docs site 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'schema/**' 9 | jobs: 10 | build-docs: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | - name: build docs 15 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 #v5.6.0 16 | with: 17 | python-version: '3.10' 18 | - run: pip install json-schema-for-humans==0.47 19 | - run: mkdir -p html/docs/schema/config 20 | - run: generate-schema-doc --config expand_buttons=true schema/config.schema.json html/docs/schema/config/index.html 21 | - run: ls html/docs/schema/config 22 | - name: publish docs 23 | uses: SamKirkland/FTP-Deploy-Action@8e83cea8672e3fbcbb9fdafff34debf6ae4c5f65 # v4.3.5 24 | with: 25 | server: ${{secrets.FTP_URL}} 26 | username: ${{secrets.DOCS_FTP_USERNAME}} 27 | password: ${{secrets.DOCS_FTP_PASSWORD}} 28 | local-dir: ./html/docs/schema/config/ 29 | server-dir: schema/config/ 30 | port: 21 31 | protocol: ftps 32 | -------------------------------------------------------------------------------- /.github/workflows/publish-site.yml: -------------------------------------------------------------------------------- 1 | name: Publish site 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'site/**' 9 | jobs: 10 | publish-site: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 17 | with: 18 | node-version-file: '.nvmrc' 19 | cache: 'npm' 20 | cache-dependency-path: 'site/package-lock.json' 21 | 22 | - name: Install Node.js dependencies 23 | run: npm ci 24 | working-directory: ./site 25 | 26 | - name: Build Tailwind CSS 27 | run: npm run tailwind 28 | working-directory: ./site 29 | 30 | - name: setup tracking script 31 | env: 32 | WWW_TRACKING_SCRIPT: ${{ secrets.WWW_TRACKING_SCRIPT }} 33 | run: echo $WWW_TRACKING_SCRIPT > src/assets/js/tracking.js 34 | working-directory: ./site 35 | 36 | - name: publish site 37 | uses: SamKirkland/FTP-Deploy-Action@8e83cea8672e3fbcbb9fdafff34debf6ae4c5f65 # v4.3.5 38 | with: 39 | server: ${{secrets.FTP_URL}} 40 | username: ${{secrets.WWW_FTP_USERNAME}} 41 | password: ${{secrets.WWW_FTP_PASSWORD}} 42 | local-dir: ./site/src/ 43 | server-dir: / 44 | port: 21 45 | protocol: ftps 46 | -------------------------------------------------------------------------------- /.github/workflows/publish-types-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish @showbridge/types to npmjs 2 | on: 3 | push: 4 | tags: 5 | - 'types/**' 6 | jobs: 7 | publish-types: 8 | permissions: 9 | id-token: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 14 | with: 15 | node-version-file: '.nvmrc' 16 | registry-url: 'https://registry.npmjs.org' 17 | cache: 'npm' 18 | cache-dependency-path: 'types/package-lock.json' 19 | - name: Install Node.js dependencies 20 | run: npm ci 21 | working-directory: ./types 22 | - name: Publish to NPM 23 | run: npm publish --provenance --access=public 24 | working-directory: ./types 25 | env: 26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/publish-webui-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish @showbridge/webui to npmjs 2 | on: 3 | push: 4 | tags: 5 | - 'webui/**' 6 | jobs: 7 | publish-webui: 8 | permissions: 9 | id-token: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 14 | with: 15 | node-version-file: '.nvmrc' 16 | registry-url: 'https://registry.npmjs.org' 17 | cache: 'npm' 18 | cache-dependency-path: 'webui/package-lock.json' 19 | - name: Install Node.js dependencies 20 | run: npm ci 21 | working-directory: ./webui 22 | - name: Publish to NPM 23 | run: npm publish --provenance --access=public 24 | working-directory: ./webui 25 | env: 26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/run-lib-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run lib tests 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | paths: 7 | - 'lib/**' 8 | jobs: 9 | test-lib: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out repository 13 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 17 | with: 18 | node-version-file: '.nvmrc' 19 | cache: 'npm' 20 | cache-dependency-path: 'lib/package-lock.json' 21 | 22 | - name: Install Node.js dependencies 23 | run: npm ci 24 | working-directory: ./lib 25 | 26 | - run: npm run test 27 | working-directory: ./lib 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_store 4 | .angular 5 | **/assets/css/tailwind.css -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | git-tag-version=false -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.12.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | *.mdx 3 | *.min.* 4 | sample/**/dev.json 5 | .dockerignore 6 | .env 7 | .gitignore 8 | .prettierignore -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | bracketSameLine: true, 7 | printWidth: 120, 8 | }; 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": "explicit", 6 | "source.organizeImports": "explicit" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # showbridge 2 | 3 | 4 | ## Running for development 5 | After cloning the repo and running `npm install` it would be useful to run `npm run install:all` this will install the dependencies for all the sub projects (see below) that need them. This gets things into a good starting place for development. 6 | 7 | 8 | I have done my best to include a dev script for all of the folders below where it makes sense. So simply running `npm run dev` should get you into a running state. This will either be a live-reload process where changes will be detected and rebuilt or the current piece will be built with what is in your working directory and launched (launcher). The script should also take care of the linking for the libraries (@showbridge/cli, @showbridge/lib) where necessary. 9 | 10 | # Summary of folders 11 | ## [lib](./lib/) 12 | lib is the actual meat of showbridge defining a library that is then wrapped up and fronted by other sections 13 | 14 | published as [@showbridge/lib](https://npmjs.com/package/@showbridge/lib) 15 | 16 | ## [cli](./cli) 17 | cli is a script using [commander.js](https://github.com/tj/commander.js) that wraps the library above into an executable script with options like where to load a config from, log levels, etc. 18 | 19 | published as [@showbridge/cli](https://npmjs.com/package/@showbrige/cli) 20 | 21 | ## [launcher](./launcher/) 22 | the launcher is an electron app that wraps the main.js script into a desktop app setting up things like config directory, logs, etc. 23 | 24 | ## [webui](./webui/) 25 | an angular web interface for managing a running instance of the main.js executable this is bundled into the launcher and served on whatever port the user has configured for the http protocol 26 | 27 | published as [showbridge-webui](https://npmjs.com/package/showbridge-webui) 28 | 29 | ## [scripts](./scripts/) 30 | general build/dev scripts 31 | 32 | ## [schema](./schema/) 33 | schema for the router config JSON file 34 | - would be nice to split this up 35 | 36 | ## [cloud](./cloud/) 37 | source for cloud server portion of showbridge which is basically just a [socket.io](https://socket.io/) server with some config options for admin-ui auth, redis adapter, log-level, etc. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Joel Wetzell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cli/.dockerignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | tests/ 3 | node_modules/ 4 | .angular/ -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | sample 2 | schema 3 | -------------------------------------------------------------------------------- /cli/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.12.0-slim 2 | ENV NODE_ENV production 3 | RUN apt-get update && apt-get install -y tini libasound2 4 | WORKDIR /app 5 | COPY package*.json . 6 | RUN npm ci 7 | COPY main.js main.js 8 | COPY schema schema 9 | COPY sample/config/default.json sample/config/default.json 10 | COPY sample/config/default.json /data/config.json 11 | COPY sample/vars/default.json sample/vars/default.json 12 | COPY sample/vars/default.json /data/vars.json 13 | ENTRYPOINT [ "/usr/bin/tini", "--", "/app/main.js"] 14 | CMD [ "--disable-protocol", "midi", "-c", "/data/config.json", "-v", "/data/vars.json"] 15 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@showbridge/cli", 3 | "version": "0.7.0", 4 | "description": "Simple Protocol Router /s", 5 | "main": "main.js", 6 | "type": "module", 7 | "bin": { 8 | "showbridge": "main.js" 9 | }, 10 | "files": [ 11 | "sample/*/default.json", 12 | "schema" 13 | ], 14 | "scripts": { 15 | "predev": "node ../scripts/setup_dev.js cli", 16 | "dev": "nodemon --exec 'node --inspect main.js -l debug -c sample/config/dev.json -v sample/vars/dev.json --webui ../webui/dist/webui/browser | pino-pretty'", 17 | "build": "rimraf sample schema && cp -R ../sample ./ && cp -R ../schema ./", 18 | "build:dev": "node setup_dev.js", 19 | "start": "node main.js | pino-pretty", 20 | "prepack": "npm run build" 21 | }, 22 | "author": { 23 | "name": "Joel Wetzell", 24 | "email": "me@jwetzell.com", 25 | "url": "https://jwetzell.com" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/jwetzell/showbridge.git" 30 | }, 31 | "keywords": [ 32 | "show", 33 | "control", 34 | "protocol", 35 | "router", 36 | "theatre" 37 | ], 38 | "license": "MIT", 39 | "dependencies": { 40 | "@showbridge/lib": "0.17.0", 41 | "@showbridge/webui": "0.10.0", 42 | "commander": "14.0.0" 43 | }, 44 | "devDependencies": { 45 | "nodemon": "3.1.10", 46 | "pino-pretty": "13.0.0", 47 | "rimraf": "6.0.1" 48 | }, 49 | "nodemonConfig": { 50 | "ignoreRoot": [ 51 | ".git" 52 | ], 53 | "ignore": [ 54 | "dist", 55 | "sample" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cli/setup_dev.js: -------------------------------------------------------------------------------- 1 | import { existsSync, rmSync, symlinkSync } from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | const fileList = [ 5 | { 6 | src: '../sample', 7 | dest: './sample', 8 | }, 9 | { 10 | src: '../schema', 11 | dest: './schema', 12 | }, 13 | ]; 14 | 15 | fileList.forEach((bundledFile) => { 16 | const src = path.join(import.meta.dirname, bundledFile.src); 17 | const dest = path.join(import.meta.dirname, bundledFile.dest); 18 | if (existsSync(dest)) { 19 | rmSync(dest, { 20 | force: true, 21 | recursive: true, 22 | }); 23 | } 24 | symlinkSync(src, dest); 25 | }); 26 | -------------------------------------------------------------------------------- /cloud/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | Dockerfile 5 | .vscode 6 | .drone.yml 7 | .nvmrc 8 | .prettierignore 9 | .prettierrc.js 10 | README.md 11 | .git 12 | .gitignore 13 | -------------------------------------------------------------------------------- /cloud/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | src/html/admin -------------------------------------------------------------------------------- /cloud/.prettierignore: -------------------------------------------------------------------------------- 1 | *.md -------------------------------------------------------------------------------- /cloud/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.12.0-slim 2 | RUN apt-get update && apt-get install tini 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | RUN npm ci 6 | COPY src ./ 7 | ENTRYPOINT [ "/usr/bin/tini", "/app/main.js" ] -------------------------------------------------------------------------------- /cloud/README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ### Source 4 | `npm install` 5 | 6 | add and configure a `.env` file if you would like to provide any of the environment variables below. 7 | 8 | `npm start` 9 | 10 | ### Docker 11 | `docker build -t showbridge-cloud .` 12 | 13 | `docker run -p 8888:8888 showbridge-cloud` 14 | 15 | ### NPX 16 | `npx @showbridge/cloud` 17 | 18 | ## Environment Variables 19 | - `LOG_LEVEL` (optional): log level to pass to pino logger i.e 10, 20, 30, etc. 20 | - `PORT` (optional): the port to run on defuault: 8888 21 | - `ADMIN_UI_USERNAME` (optional): Username for the admin ui 22 | - `ADMIN_UI_PASSWORD` (optional): Password for the admin ui 23 | - `REDIS_URL` (optional): redis client url for socket.io redis-streams adapter if no url is provided then socket.io starts up as a solo instance 24 | - `DISCORD_WEBHOOK_URL`(optional): discord webhook url to send some status messages to 25 | - `DISCORD_EVENTS` (optional): comma separated list of events to send to the configured discord webhook 26 | - events: `connect`,`disconnect`,`leave`,`join` (defaults to all) 27 | 28 | 29 | ## Info 30 | - connect to admin ui using [socket.io hosted version](https://admin.socket.io) or [host the admin UI yourself](https://github.com/socketio/socket.io-admin-ui/) 31 | - server ready endpoint at `/ready` return 200 when server is up and running, 503 while starting up for use in things like k8s readinessProbe 32 | -------------------------------------------------------------------------------- /cloud/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@showbridge/cloud", 3 | "version": "0.5.1", 4 | "description": "Cloud server for proxying messages between showbridge instances", 5 | "main": "src/main.js", 6 | "type": "module", 7 | "bin": { 8 | "showbridge-cloud": "src/main.js" 9 | }, 10 | "files": [ 11 | "src" 12 | ], 13 | "scripts": { 14 | "dev": "nodemon src/main.js | pino-pretty", 15 | "start": "node src/main.js" 16 | }, 17 | "author": { 18 | "name": "Joel Wetzell", 19 | "email": "me@jwetzell.com", 20 | "url": "https://jwetzell.com" 21 | }, 22 | "keywords": [ 23 | "show", 24 | "control", 25 | "protocol", 26 | "bridge", 27 | "cloud", 28 | "router", 29 | "theatre" 30 | ], 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/jwetzell/showbridge.git" 34 | }, 35 | "license": "MIT", 36 | "dependencies": { 37 | "@socket.io/admin-ui": "0.5.1", 38 | "@socket.io/redis-streams-adapter": "0.2.2", 39 | "bcryptjs": "3.0.2", 40 | "discord.js": "14.19.3", 41 | "dotenv": "16.5.0", 42 | "express": "5.1.0", 43 | "ioredis": "5.6.1", 44 | "pino": "9.7.0", 45 | "redis": "5.0.1", 46 | "socket.io": "4.8.1" 47 | }, 48 | "devDependencies": { 49 | "nodemon": "3.1.10", 50 | "pino-pretty": "13.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cloud/src/utils.js: -------------------------------------------------------------------------------- 1 | import { WebhookClient } from 'discord.js'; 2 | import 'dotenv/config'; 3 | import pino from 'pino'; 4 | 5 | export const logger = pino(); 6 | 7 | if (process.env.LOG_LEVEL) { 8 | try { 9 | const logLevel = parseInt(process.env.LOG_LEVEL, 10); 10 | logger.level = logLevel; 11 | } catch (error) { 12 | logger.error( 13 | `cloud: unable to set logger level to <${process.env.LOG_LEVEL}>. see pino log levels for valid options` 14 | ); 15 | } 16 | } 17 | 18 | let discord; 19 | let discordEvents = ['connect', 'disconnect', 'leave', 'join']; 20 | 21 | if (process.env.DISCORD_WEBHOOK_URL) { 22 | logger.info('cloud: setting up discord client'); 23 | discord = new WebhookClient({ url: process.env.DISCORD_WEBHOOK_URL }); 24 | if (process.env.DISCORD_EVENTS) { 25 | try { 26 | const eventsToSend = process.env.DISCORD_EVENTS.trim() 27 | .split(',') 28 | .map((event) => event.trim()); 29 | discordEvents = discordEvents.filter((event) => eventsToSend.includes(event)); 30 | } catch (error) { 31 | logger.error('cloud: failed to parse discord events from ENV'); 32 | } 33 | } 34 | } else { 35 | discordEvents = []; 36 | } 37 | 38 | export function sendToDiscord(event, data) { 39 | if (discord) { 40 | if (discordEvents.includes(event)) { 41 | discord.send(data).catch(logger.error); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import starlight from '@astrojs/starlight'; 2 | import { defineConfig } from 'astro/config'; 3 | 4 | // https://astro.build/config 5 | export default defineConfig({ 6 | integrations: [ 7 | starlight({ 8 | title: 'showbridge', 9 | favicon: '/favicon.ico', 10 | social: [ 11 | { 12 | icon: 'github', 13 | label: 'GitHub', 14 | href: 'https://github.com/jwetzell/showbridge', 15 | }, 16 | ], 17 | sidebar: [ 18 | { 19 | label: 'showbridge', 20 | autogenerate: { directory: '/showbridge' }, 21 | }, 22 | { 23 | label: 'Run', 24 | autogenerate: { directory: 'run' }, 25 | }, 26 | { 27 | label: 'Reference', 28 | autogenerate: { directory: 'reference' }, 29 | }, 30 | { label: 'Demo', link: 'https://demo.showbridge.io/', attrs: { target: '_blank' } }, 31 | { 32 | label: 'More Docs', 33 | items: [ 34 | { 35 | label: 'Config JSON Schema', 36 | link: 'https://docs.showbridge.io/schema/config', 37 | attrs: { target: '_blank' }, 38 | }, 39 | ], 40 | }, 41 | ], 42 | }), 43 | ], 44 | }); 45 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@showbridge/docs", 3 | "type": "module", 4 | "version": "0.2.1", 5 | "files": [ 6 | "dist" 7 | ], 8 | "scripts": { 9 | "dev": "astro dev", 10 | "start": "astro dev", 11 | "build": "astro check && astro build", 12 | "build:prod": "astro check && astro build --base /docs", 13 | "preview": "astro preview", 14 | "astro": "astro" 15 | }, 16 | "devDependencies": { 17 | "@astrojs/starlight": "0.34.3", 18 | "astro": "5.7.13", 19 | "sharp": "0.34.1", 20 | "@astrojs/check": "0.9.4", 21 | "typescript": "5.8.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsSchema } from '@astrojs/starlight/schema'; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | }; 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Showbridge 3 | sidebar: 4 | order: 1 5 | --- 6 | 7 | Welcome to the showbridge docs, this is an every changing store of information related to showbride. If you find something that doesn't make sense or needs more documentation/clarification please open an issue on [github](https://github.com/jwetzell/showbridge/issues). -------------------------------------------------------------------------------- /docs/src/content/docs/reference/config.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Config 3 | sidebar: 4 | order: 2 5 | --- 6 | The showbridge router's config is entirely controlled by a JSON config file. This file can be made by hand or edited via the web interface included with the launcher. The router WILL NOT start up with an invalid config file. I do provide some starter/example configs to look at to get a general idea of what one entails. 7 | 8 | Resources 9 | - the [JSON Schema](https://docs.showbridge.io/schema/config) used to validate the config file 10 | - good idea to start with [default.json](https://github.com/jwetzell/showbridge/blob/main/sample/config/default.json) 11 | - [random examples](https://github.com/jwetzell/showbridge/blob/main/sample/config/) 12 | - the [demo](https://demo.showbridge.io) site can be used to import/edit/create configs that can be downloaded -------------------------------------------------------------------------------- /docs/src/content/docs/reference/dictionary.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dictionary 3 | sidebar: 4 | order: 1 5 | --- 6 | 7 | - **router**: throughout documentation I will use the term router to refer to configured/running instance of showbridge 8 | - **triggers**: when a message comes in triggers enforce some criteria on the incoming message. If a message "ticks all the boxes" the actions of the trigger are then performed. Triggers can have subTriggers which are further evaluated if the trigger is "fired". Triggers do not stack each trigger in the array is evaluated in isolation. 9 | - **actions**: actions are what should be done as a result of a trigger being well triggered, actions can transform the message that they act on using transforms 10 | - **transforms**: transforms transform messages, the transformations are localized to the action the transform is a part of 11 | - **message**: any incoming communication into showbridge bundled into a message object i.e HTTP Request, MIDI Note On, UDP Packet, etc. -------------------------------------------------------------------------------- /docs/src/content/docs/reference/messages.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Messages 3 | sidebar: 4 | order: 9 5 | --- 6 | For templating purposes (any param starting with an underscore `_`) the incoming message is made available as `msg` and the properties available under this `msg` object are outlined below. See [templating](/reference/templating/) for some examples using a midi message as an example of how these properties can be accessed in a template. 7 | 8 | ## **http** 9 | - originalUrl: express.js req.originalUrl 10 | - baseUrl: express.js req.baseUrl 11 | - path: express.js req.path 12 | - body: express.js req.body 13 | ## **websocket** 14 | - payload: ws message content (if this is JSON it will be parsed into an object) 15 | ## **midi** 16 | - port: the name of the midi port that the message came in on 17 | - status: midi status i.e. note_on, note_off, program_change, control_change, etc. 18 | - channel: midi channel 1-16 19 | - note: midi note 1-127 20 | - velocity: midi velocity 1-127 21 | - pressure: midi pressure 1-127 22 | - control: midi control number 1-127 23 | - value: value portion of control_change, pitch_bend, mtc messages 24 | - program: program number 1- 127 25 | - type: timecode type from mtc messages 26 | - song: song number from song_select messages 27 | - beats: MIDI beats for song_position mesages 28 | - bytes: the 3 MIDI data bytes 29 | ## **mqtt** 30 | - payload: the contents of the MQTT message either an object if parsable JSON or the raw contents as a string 31 | - topic: the topic of the published MQTT message 32 | ## **osc** 33 | - address: address of the incoming osc message /an/osc/address 34 | - addressParts: an array of address segments i.e. ["an","osc","address"] 35 | - args: array of args of the incoming osc message [0,"1",2.0] 36 | - bytes: the osc message as bytes 37 | ## **tcp** 38 | - bytes: UInt8Array of the TCP packet 39 | - string: string representation of the TCP packet 40 | ## **udp** 41 | - bytes: UInt8Array of the UDP packet 42 | - string: string representation of the UDP packet -------------------------------------------------------------------------------- /docs/src/content/docs/reference/params.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Params 3 | sidebar: 4 | order: 4 5 | --- 6 | 7 | Each piece uses the params property to store its configurations as outlined in the following pages. An **\*** means the param can be [templated](/reference/templating) by prefixing it with an underscore (`_`) (i.e `address` -> `_address`). You can assume that the templated param will take priority over its not templated version if they are both defined. 8 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/structure.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Structure 3 | sidebar: 4 | order: 3 5 | --- 6 | Every piece (triggers, actions, transforms) have a shared JSON structure 7 | - **comment**: string used only for future reference 8 | - **type**: string that denotes the type of the trigger/action/transform 9 | - **params**: an object that holds the config for the trigger/action/transform 10 | - **enabled**: boolean - if false the piece is skipped and so are the underlying pieces. i.e. if a trigger is disabled no actions under that trigger will be performed, if an action is disabled no transforms under that action will be performed -------------------------------------------------------------------------------- /docs/src/content/docs/reference/templating.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Templating 3 | sidebar: 4 | order: 8 5 | --- 6 | 7 | Alright there has been a some references to templating. There is no secret sauce it is simply [lodash templating](https://lodash.com/docs/4.17.15#template) which is compatible with JS template literals (backtick strings). The incoming `msg` and the global `vars` objects are available when templates are processed. For properties of incoming messages [see messages](/reference/messages/). The `vars` object will contain the contents of the `vars.json` file passed in with `-v` flag or any value set using the `store` [action](/reference/actions/#store). 8 | 9 | import { Aside } from '@astrojs/starlight/components'; 10 | 11 | 15 | 16 | ## **Examples**: 17 | 18 | - `"/midi/${msg.channel}/${msg.status}/${msg.note}"` -> `/midi/1/note_on/60` 19 | - `"${msg.velocity - 10}"` -> `117` 20 | - `"${msg.note + 12}"` -> `72` 21 | -------------------------------------------------------------------------------- /docs/src/content/docs/reference/transforms.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Transforms 3 | sidebar: 4 | order: 7 5 | --- 6 | ## **floor** 7 | - property: the path to the property in the incoming msg object 8 | ## **log** 9 | - property: the path to the property in the incoming msg object 10 | - base: the base of the log 11 | ## **map** 12 | - property: the path to the property in the incoming msg object 13 | - map: an object representing a mapping between incoming msg.property values and their output i.e {"MON":"Monday"} so if msg.property === "MON" then msg.property will be set to "Monday" and passed along 14 | ## **power** 15 | - property: the path to the property in the incoming msg object 16 | - exponent: the exponent to raise the value of msg.property to 17 | ## **round** 18 | - property: the path to the property in the incoming msg object 19 | ## **scale** 20 | - property: the path to the property in the incoming msg object 21 | - inRange: the range of values for the incoming msg.property value i.e [0,100] 22 | - outRange: the range of values to scale msg.property value into [1,10] 23 | ## **template** 24 | - property: the path to the property in the incoming msg object 25 | - template: the template that will be evaluated and then set as the value of the msg.property -------------------------------------------------------------------------------- /docs/src/content/docs/run/cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CLI 3 | sidebar: 4 | order: 2 5 | --- 6 | - create a config file (see [config](/reference/config/)) 7 | - optionally install globally: `npm install -g @showbridge/cli` 8 | - run 9 | - if installed globally: `showbridge -c config.json` 10 | - via npx: `npx @showbridge/cli@latest -c config.json` 11 | - see below for all flags 12 | - this method still has the web interface available via HTTP 13 | - use the `-h` flag to see other available flags 14 | 15 | ``` 16 | Usage: @showbridge/cli [options] 17 | 18 | Simple protocol router /s 19 | 20 | Options: 21 | -V, --version output the version number 22 | -c, --config location of config file 23 | -v, --vars location of file containing vars 24 | -w, --webui location of webui html to serve 25 | --disable-action action type(s) to disable 26 | --disable-protocol protocol type(s) to disable 27 | --disable-trigger trigger type(s) to disable 28 | --disable-transform transform type(s) to disable 29 | -l, --log-level log level (choices: "trace", "debug", "info", "warn", "error", "fatal", default: "info") 30 | -h, --help display help for command 31 | 32 | ``` -------------------------------------------------------------------------------- /docs/src/content/docs/run/desktop.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Desktop 3 | sidebar: 4 | order: 1 5 | --- 6 | 7 | - download/install [launcher](https://github.com/jwetzell/showbridge/releases) this is the easiest method to get up and running and includes the web interface and logging 8 | - run showbridge! 9 | 10 | 11 | ## Supported Platforms 12 | 13 | I am currently targeting these platforms for both the desktop launcher and CLI. 14 | 15 | - MacOS 16 | - x64 17 | - arm64 18 | - Linux 19 | - x64 20 | - arm64 21 | - armv7l 22 | - Windows 23 | - x64 24 | - Docker 25 | - linux/amd64 26 | - linux/arm64 27 | - linux/arm/v7 28 | -------------------------------------------------------------------------------- /docs/src/content/docs/run/docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Docker 3 | sidebar: 4 | order: 3 5 | --- 6 | 7 | The application can be run in Docker with some limitations. 8 | 9 | ## Limitations 10 | 11 | - MIDI is not supported 12 | 13 | ## Usage 14 | 15 | - `docker run -p 3000:3000 -p 8000:8000 jwetzell/showbridge:v0.6.2` 16 | 17 | ## Persistance 18 | 19 | The config and vars JSON files are stored in `/data` so any method supported by docker to persist data should be supported here. 20 | 21 | ### Docker Volume 22 | - `docker run -p 3000:3000 -p 8000:8000 -v showbridge:/data jwetzell/showbridge:v0.6.2` 23 | - `docker run -p 3000:3000 -p 8000:8000 -v /some/location/on/your/setup:/data jwetzell/showbridge:v0.6.2` 24 | -------------------------------------------------------------------------------- /docs/src/content/docs/run/source.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Source 3 | sidebar: 4 | order: 4 5 | --- 6 | 7 | import { Steps } from '@astrojs/starlight/components'; 8 | 9 | 10 | 1. clone repo 11 | 12 | 2. install dependencies: `npm install && npm run install:all` 13 | 14 | 3. run cli: 15 | 16 | - `cd cli` 17 | - `npm run start -- -c config.json` 18 | 19 | - see [CLI Usage](/guides/cli-usage) for more flags 20 | - if no config file is specified then a [default config](https://github.com/jwetzell/showbridge/blob/main/sample/config/default.json) will be used 21 | 22 | 4. run launcher 23 | 24 | - `cd launcher` 25 | - `npm run start` 26 | 27 | -------------------------------------------------------------------------------- /docs/src/content/docs/showbridge/cloud.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cloud 3 | sidebar: 4 | order: 99 5 | --- 6 | 7 | ## Connecting instances remotely? 8 | Remotely connecting two or more router instances is supported via [showbridge cloud](https://github.com/jwetzell/showbridge/tree/main/cloud). The only configuration necessary is the url of the cloud server (a **public** cloud server is available by using https://cloud.showbridge.io) the other necessary configuration option is rooms explained below. 9 | 10 | Routers can send messages through the cloud server using the cloud-output action mentioned in [actions](/reference/actions/#cloud-output). To control what messages routers are listening to when multiple routers are connected to the same cloud server the concept of rooms is used. A room is simply a string i.e 'room1', 'super-secret-room-name', etc. when a cloud-output action is used the configured room(s) property of that action controls what room(s) the message will be sent to. The room(s) property of the cloud params controls what room(s) a router is joined to. When a cloud-output sends a message to a room that a router is configured to be in then the router will receive the message sent and process it as if it was a native message. 11 | 12 | ### Cloud Example: not sure this will make things any more clear but.... 13 | - assume all routers are configured to connect to the same cloud server 14 | - router1 is setup to join `room1` and log all midi-note-on messages 15 | - router2 is setup to join `room1` and `room2` and log all midi-note-on messages 16 | - router3 is not setup to be a part of any room 17 | - router3 is configured with a midi-note-on trigger with a cloud-output action that has rooms = ["room1","room2"] 18 | - router3 now receives a midi note_on message. The cloud-output action will cause the following. 19 | - router1 will log a midi-note-on message 20 | - router2 will log 2 midi-note-on messages (this is because it is joined to 2 rooms and the cloud-output action was configured to send the message to both of the rooms that router2 was joined to) -------------------------------------------------------------------------------- /docs/src/content/docs/showbridge/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | sidebar: 4 | order: 1 5 | --- 6 | 7 | showbridge is a kind of re-imagining of [OSCulator](https://osculator.net/) taken way too far. 8 | 9 | ### Supported Protocols 10 | - HTTP & WebSocket 11 | - incoming websocket connections only 12 | - OSC (via UDP and TCP) 13 | - UDP 14 | - TCP 15 | - MQTT 16 | - only one broker connection is currently supported 17 | - MIDI 18 | - virtual MIDI input/output is not supported on Windows and Docker -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } 4 | -------------------------------------------------------------------------------- /launcher/build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.disable-library-validation 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.network.server 14 | 15 | com.apple.security.files.user-selected.read-only 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /launcher/electron_bundle.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | // eslint-disable-next-line no-unused-vars 4 | exports.default = async function (context) { 5 | console.log('bundling things up for electron app'); 6 | 7 | console.log('building tailwind.css'); 8 | execSync(`npm run tailwind`, { 9 | stdio: 'inherit', 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /launcher/src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | margin: 0; 4 | padding: 0; 5 | -webkit-app-region: drag; 6 | } 7 | 8 | #contents { 9 | user-select: none; 10 | } 11 | 12 | #drag-drop-container { 13 | -webkit-app-region: no-drag !important; 14 | } 15 | 16 | select, 17 | input, 18 | button { 19 | -webkit-app-region: no-drag !important; 20 | } 21 | 22 | .clickable { 23 | -webkit-app-region: no-drag !important; 24 | cursor: pointer; 25 | } 26 | 27 | input { 28 | margin: 1px; 29 | border-radius: 3px; 30 | padding-left: 3px; 31 | } 32 | 33 | input:invalid { 34 | border: 2px solid red; 35 | } 36 | -------------------------------------------------------------------------------- /launcher/src/assets/images/icon16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/launcher/src/assets/images/icon16x16.png -------------------------------------------------------------------------------- /launcher/src/assets/images/icon512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/launcher/src/assets/images/icon512x512.png -------------------------------------------------------------------------------- /launcher/src/assets/js/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const levelMap = { 3 | 10: '\u001b[90mTRACE\u001b[39m ', 4 | 20: '\u001b[34mDEBUG\u001b[39m ', 5 | 30: '\u001b[32mINFO\u001b[39m ', 6 | 40: '\u001b[33mWARN\u001b[39m ', 7 | 50: '\u001b[31mERROR\u001b[39m ', 8 | 60: '\u001b[41mFATAL\u001b[49m ', 9 | }; 10 | 11 | const term = new Terminal(); 12 | const fitAddon = new FitAddon.FitAddon(); 13 | term.loadAddon(fitAddon); 14 | term.open(document.getElementById('terminal')); 15 | fitAddon.fit(); 16 | 17 | electron.on('log', (event, log) => { 18 | try { 19 | const logObj = JSON.parse(log); 20 | if (logObj.time && logObj.level && logObj.msg) { 21 | const logDate = new Date(logObj.time); 22 | const logLine = `[${logDate.format('HH:MM:ss.l')}] ${levelMap[logObj.level]}: \u001b[36m${logObj.msg}\u001b[39m`; 23 | term.writeln(logLine); 24 | } else { 25 | term.writeln(log); 26 | } 27 | } catch (error) { 28 | term.writeln(log); 29 | } 30 | }); 31 | 32 | electron.send('logWinLoaded'); 33 | 34 | window.addEventListener('resize', () => { 35 | if (fitAddon) { 36 | fitAddon.fit(); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /launcher/src/assets/js/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable no-undef */ 3 | const dragContainer = document.getElementById('drag-drop-container'); 4 | const messageIndicator = document.getElementById('message-indicator'); 5 | 6 | dragContainer.onclick = () => { 7 | window.electron.send('loadConfigFromFileBrowser'); 8 | }; 9 | 10 | dragContainer.ondrop = (event) => { 11 | event.preventDefault(); 12 | if (event.dataTransfer.files.length > 0) { 13 | const file = event.dataTransfer.files[0]; 14 | window.electron.send('loadConfigFromFile', { name: file.name, path: file.path }); 15 | } 16 | }; 17 | 18 | dragContainer.ondragover = (event) => { 19 | event.preventDefault(); 20 | event.stopPropagation(); 21 | }; 22 | 23 | electron.on('messageIn', (event, message) => { 24 | // NOTE(jwetzell) flash message indicator 25 | messageIndicator.style.display = 'block'; 26 | setTimeout(() => { 27 | messageIndicator.style.display = 'none'; 28 | }, 100); 29 | }); 30 | 31 | electron.on('protocolStarted', (event, protocol) => { 32 | if (protocol === 'http') { 33 | document.getElementById('open-webui').disabled = false; 34 | } 35 | }); 36 | 37 | function showLogs() { 38 | electron.send('showLogs'); 39 | } 40 | 41 | function showWebUI() { 42 | electron.send('showUI'); 43 | } 44 | 45 | function quitApp() { 46 | electron.send('quit'); 47 | } 48 | -------------------------------------------------------------------------------- /launcher/src/html/logger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | showbridge logs 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /launcher/src/html/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | showbridge 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | showbridge 14 | 15 | 16 | 17 | 18 | Update Config 19 | 20 | 21 | 22 | 23 | 26 | Quit 27 | 28 | 29 | 34 | Web UI 35 | 36 | 37 | 40 | Logs 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /launcher/src/preload.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const { contextBridge, ipcRenderer } = require('electron'); 3 | 4 | contextBridge.exposeInMainWorld('electron', { 5 | send: ipcRenderer.send, 6 | on: (eventName, callback) => { 7 | ipcRenderer.on(eventName, (event, ...args) => { 8 | callback(event, ...args); 9 | }); 10 | }, 11 | invoke: ipcRenderer.invoke, 12 | }); 13 | -------------------------------------------------------------------------------- /launcher/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/*.html', './src/html/*.html'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /launcher/tailwind.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@showbridge/lib", 3 | "version": "0.17.0", 4 | "description": "Main library for showbridge protocol router", 5 | "main": "dist/lib/index.js", 6 | "type": "module", 7 | "files": [ 8 | "dist/lib" 9 | ], 10 | "scripts": { 11 | "pretest": "npm run build", 12 | "test": "node --test --experimental-test-coverage", 13 | "prebuild": "rimraf dist/lib", 14 | "prepack": "npm run test", 15 | "build": "tsc", 16 | "build:dev": "tsc" 17 | }, 18 | "author": { 19 | "name": "Joel Wetzell", 20 | "email": "me@jwetzell.com", 21 | "url": "https://jwetzell.com" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/jwetzell/showbridge.git" 26 | }, 27 | "keywords": [ 28 | "show", 29 | "control", 30 | "protocol", 31 | "router", 32 | "theatre" 33 | ], 34 | "license": "MIT", 35 | "dependencies": { 36 | "@julusian/midi": "3.6.1", 37 | "ajv": "8.17.1", 38 | "cors": "2.8.5", 39 | "express": "5.1.0", 40 | "lodash-es": "4.17.21", 41 | "mqtt": "5.13.0", 42 | "osc-min": "2.1.1", 43 | "pino": "9.7.0", 44 | "slip": "1.0.2", 45 | "socket.io-client": "4.8.1", 46 | "superagent": "10.2.1", 47 | "ws": "8.18.2" 48 | }, 49 | "devDependencies": { 50 | "@showbridge/types": "0.5.0", 51 | "@types/express": "5.0.2", 52 | "@types/node": "22.15.19", 53 | "@types/osc-min": "npm:@2bit/types-osc-min@1.0.1", 54 | "rimraf": "6.0.1", 55 | "typescript": "5.8.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/actions/cloud-output-action.ts: -------------------------------------------------------------------------------- 1 | import { CloudOutputActionParams, RouterVars } from '@showbridge/types'; 2 | import { Message } from '../messages/index.js'; 3 | import { RouterProtocols } from '../router.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Action from './action.js'; 6 | 7 | class CloudOutputAction extends Action { 8 | _run(_msg: Message, vars: RouterVars, protocols: RouterProtocols) { 9 | const msg = this.getTransformedMessage(_msg, vars); 10 | 11 | try { 12 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 13 | 14 | if (resolvedParams.room) { 15 | protocols.cloud.send(resolvedParams.room, msg); 16 | } else if (resolvedParams.rooms) { 17 | resolvedParams.rooms.forEach((room) => { 18 | protocols.cloud.send(room, msg); 19 | }); 20 | } else { 21 | logger.error('action: cloud-output action has no room specified'); 22 | } 23 | } catch (error) { 24 | logger.error(`action: problem executing cloud-output action - ${error}`); 25 | } 26 | this.emit('finished'); 27 | } 28 | } 29 | export default CloudOutputAction; 30 | -------------------------------------------------------------------------------- /lib/src/actions/forward-action.ts: -------------------------------------------------------------------------------- 1 | import { ForwardActionParms, RouterVars } from '@showbridge/types'; 2 | import { ByteMessage } from '../messages/index.js'; 3 | import { RouterProtocols } from '../router.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Action from './action.js'; 6 | 7 | class ForwardAction extends Action { 8 | _run(_msg: ByteMessage, vars: RouterVars, protocols: RouterProtocols) { 9 | const msg = this.getTransformedMessage(_msg, vars); 10 | try { 11 | const msgToForward = msg.bytes; 12 | 13 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 14 | if (msgToForward === undefined) { 15 | logger.error('action: this is not a forwardable message type'); 16 | return; 17 | } 18 | 19 | if (resolvedParams.protocol === 'udp') { 20 | protocols.udp.send(Buffer.from(msgToForward), resolvedParams.port, resolvedParams.host); 21 | } else if (resolvedParams.protocol === 'tcp') { 22 | // TODO(jwetzell): osc messages are always slip encoded? 23 | protocols.tcp.send( 24 | Buffer.from(msgToForward), 25 | resolvedParams.port, 26 | resolvedParams.host, 27 | msg.messageType === 'osc' 28 | ); 29 | } else { 30 | logger.error(`action: unhandled forward protocol = ${resolvedParams.protocol}`); 31 | } 32 | } catch (error) { 33 | logger.error(`action: problem executing forward action - ${error}`); 34 | } 35 | this.emit('finished'); 36 | } 37 | } 38 | export default ForwardAction; 39 | -------------------------------------------------------------------------------- /lib/src/actions/http-request-action.ts: -------------------------------------------------------------------------------- 1 | import { HTTPRequestActionParams, RouterVars } from '@showbridge/types'; 2 | import { Message } from '../messages/index.js'; 3 | import { RouterProtocols } from '../router.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Action from './action.js'; 6 | 7 | class HTTPRequestAction extends Action { 8 | _run(_msg: Message, vars: RouterVars, protocols: RouterProtocols) { 9 | const msg = this.getTransformedMessage(_msg, vars); 10 | // TODO(jwetzell): add other http things like query parameters though they can just be included in the url field 11 | try { 12 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 13 | if (resolvedParams.url && resolvedParams.url !== '') { 14 | protocols.http.send(resolvedParams.url, resolvedParams.method, resolvedParams.body, resolvedParams.contentType); 15 | } else { 16 | logger.error('action: url is empty'); 17 | } 18 | } catch (error) { 19 | logger.error(`action: problem executing http action - ${error}`); 20 | } 21 | this.emit('finished'); 22 | } 23 | } 24 | export default HTTPRequestAction; 25 | -------------------------------------------------------------------------------- /lib/src/actions/http-response-action.ts: -------------------------------------------------------------------------------- 1 | import { HTTPResponseActionParams } from '@showbridge/types'; 2 | import { existsSync, readFileSync } from 'node:fs'; 3 | import path from 'node:path'; 4 | import { HTTPMessage } from '../messages/index.js'; 5 | import { logger } from '../utils/index.js'; 6 | import Action from './action.js'; 7 | 8 | class HTTPResponseAction extends Action { 9 | _run(_msg: HTTPMessage, vars) { 10 | const msg = this.getTransformedMessage(_msg, vars); 11 | 12 | try { 13 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 14 | if (msg.response) { 15 | if (resolvedParams.contentType) { 16 | msg.response.setHeader('content-type', resolvedParams.contentType); 17 | } 18 | 19 | if ('body' in resolvedParams) { 20 | msg.response.status(200).send(resolvedParams.body); 21 | } else if ('path' in resolvedParams) { 22 | const resolvedPath = path.resolve(resolvedParams.path); 23 | if (existsSync(resolvedPath)) { 24 | const fileData = readFileSync(resolvedPath); 25 | msg.response.status(200).send(fileData); 26 | } else { 27 | msg.response.status(404).send(); 28 | } 29 | } 30 | } else { 31 | logger.error('action: http-response action called from a non http context'); 32 | } 33 | } catch (error) { 34 | logger.error(`action: problem executing http-response action - ${error}`); 35 | if (msg.response) { 36 | msg.response.status(500).send(error); 37 | } 38 | } 39 | this.emit('finished'); 40 | } 41 | } 42 | export default HTTPResponseAction; 43 | -------------------------------------------------------------------------------- /lib/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | import CloudOutputAction from './cloud-output-action.js'; 2 | import DelayAction from './delay-action.js'; 3 | import ForwardAction from './forward-action.js'; 4 | import HTTPRequestAction from './http-request-action.js'; 5 | import HTTPResponseAction from './http-response-action.js'; 6 | import LogAction from './log-action.js'; 7 | import MIDIOutputAction from './midi-output-action.js'; 8 | import MQTTOutputAction from './mqtt-output-action.js'; 9 | import OSCOutputAction from './osc-output-action.js'; 10 | import RandomAction from './random-action.js'; 11 | import ShellAction from './shell-action.js'; 12 | import StoreAction from './store-action.js'; 13 | import TCPOutputAction from './tcp-output-action.js'; 14 | import UDPOutputAction from './udp-output-action.js'; 15 | 16 | export { 17 | CloudOutputAction, 18 | DelayAction, 19 | ForwardAction, 20 | HTTPRequestAction, 21 | HTTPResponseAction, 22 | LogAction, 23 | MIDIOutputAction, 24 | MQTTOutputAction, 25 | OSCOutputAction, 26 | RandomAction, 27 | ShellAction, 28 | StoreAction, 29 | TCPOutputAction, 30 | UDPOutputAction, 31 | }; 32 | 33 | export const ActionTypeClassMap = { 34 | 'cloud-output': CloudOutputAction, 35 | delay: DelayAction, 36 | forward: ForwardAction, 37 | 'http-request': HTTPRequestAction, 38 | log: LogAction, 39 | 'midi-output': MIDIOutputAction, 40 | 'mqtt-output': MQTTOutputAction, 41 | 'http-response': HTTPResponseAction, 42 | 'osc-output': OSCOutputAction, 43 | random: RandomAction, 44 | shell: ShellAction, 45 | store: StoreAction, 46 | 'tcp-output': TCPOutputAction, 47 | 'udp-output': UDPOutputAction, 48 | }; 49 | -------------------------------------------------------------------------------- /lib/src/actions/log-action.ts: -------------------------------------------------------------------------------- 1 | import { LogActionParams } from '@showbridge/types'; 2 | import { Message } from '../messages/index.js'; 3 | import { logger } from '../utils/index.js'; 4 | import Action from './action.js'; 5 | 6 | class LogAction extends Action { 7 | _run(_msg: Message, vars) { 8 | const msg = this.getTransformedMessage(_msg, vars); 9 | logger.info(`log: ${msg.messageType} - ${msg}`); 10 | this.emit('finished'); 11 | } 12 | } 13 | export default LogAction; 14 | -------------------------------------------------------------------------------- /lib/src/actions/midi-output-action.ts: -------------------------------------------------------------------------------- 1 | import { MIDIOutputActionParams, RouterVars } from '@showbridge/types'; 2 | import { MIDIMessage, Message } from '../messages/index.js'; 3 | import { RouterProtocols } from '../router.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Action from './action.js'; 6 | 7 | class MIDIOutputAction extends Action { 8 | _run(_msg: Message, vars: RouterVars, protocols: RouterProtocols) { 9 | try { 10 | const msg = this.getTransformedMessage(_msg, vars); 11 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 12 | const midiToSend = MIDIMessage.parseActionParams(resolvedParams); 13 | 14 | if (midiToSend !== undefined) { 15 | protocols.midi.send(midiToSend.bytes, resolvedParams.port); 16 | } 17 | } catch (error) { 18 | logger.error(`action: problem executing midi-output action - ${error}`); 19 | } 20 | this.emit('finished'); 21 | } 22 | } 23 | export default MIDIOutputAction; 24 | -------------------------------------------------------------------------------- /lib/src/actions/mqtt-output-action.ts: -------------------------------------------------------------------------------- 1 | import { MQTTOutputActionParams, RouterVars } from '@showbridge/types'; 2 | import { Message } from '../messages/index.js'; 3 | import { RouterProtocols } from '../router.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Action from './action.js'; 6 | 7 | class MQTTOutputAction extends Action { 8 | _run(_msg: Message, vars: RouterVars, protocols: RouterProtocols) { 9 | const msg = this.getTransformedMessage(_msg, vars); 10 | 11 | try { 12 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 13 | 14 | if (resolvedParams.topic !== undefined && resolvedParams.payload !== undefined) { 15 | protocols.mqtt.send(resolvedParams.topic, resolvedParams.payload); 16 | } else { 17 | logger.error('action: mqtt-output missing either topic or payload'); 18 | } 19 | } catch (error) { 20 | logger.error(`action: problem executing mqtt-output action - ${error}`); 21 | } 22 | this.emit('finished'); 23 | } 24 | } 25 | export default MQTTOutputAction; 26 | -------------------------------------------------------------------------------- /lib/src/actions/random-action.ts: -------------------------------------------------------------------------------- 1 | import { ActionObj, RandomActionParams, RouterVars } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import { Message } from '../messages/index.js'; 4 | import { RouterProtocols } from '../router.js'; 5 | import { logger } from '../utils/index.js'; 6 | import Action from './action.js'; 7 | import { ActionTypeClassMap } from './index.js'; 8 | 9 | class RandomAction extends Action { 10 | subActions: Action[]; 11 | 12 | constructor(obj: ActionObj) { 13 | super(obj); 14 | 15 | // NOTE(jwetzell): turn subAction JSON into class instances 16 | this.subActions = this.params.actions 17 | .filter((action) => has(ActionTypeClassMap, action.type)) 18 | .map((action) => new ActionTypeClassMap[action.type](action)); 19 | } 20 | 21 | _run(_msg: Message, vars: RouterVars, protocols: RouterProtocols) { 22 | const msg = this.getTransformedMessage(_msg, vars); 23 | if (this.params.actions !== undefined) { 24 | const subActionIndex = Math.floor(Math.random() * this.params.actions.length); 25 | const subAction = this.subActions[subActionIndex]; 26 | 27 | logger.trace(`random-action: ${subActionIndex}: ${subAction.enabled ? 'fired' : 'skipped'}`); 28 | 29 | subAction.on('action', (actionPath, fired) => { 30 | this.emit('action', subAction, `actions/${subActionIndex}/${actionPath}`, fired); 31 | }); 32 | subAction.on('transform', (transformPath, enabled) => { 33 | this.emit('transform', `actions/${subActionIndex}/${transformPath}`, enabled); 34 | }); 35 | subAction.once('finished', () => { 36 | this.cleanupAfterFinished(); 37 | this.emit('finished'); 38 | }); 39 | this.emit('action', subAction, `actions/${subActionIndex}`, subAction.enabled); 40 | subAction.run(msg, vars, protocols); 41 | } 42 | } 43 | 44 | cleanupAfterFinished() { 45 | // NOTE(jwetzell): remove listeners 46 | this.subActions.forEach((subAction) => { 47 | subAction.removeAllListeners('action'); 48 | subAction.removeAllListeners('transform'); 49 | }); 50 | } 51 | } 52 | export default RandomAction; 53 | -------------------------------------------------------------------------------- /lib/src/actions/shell-action.ts: -------------------------------------------------------------------------------- 1 | import { ShellActionParams } from '@showbridge/types'; 2 | import { exec } from 'node:child_process'; 3 | import { Message } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Action from './action.js'; 6 | 7 | class ShellAction extends Action { 8 | _run(_msg: Message, vars) { 9 | const msg = this.getTransformedMessage(_msg, vars); 10 | 11 | try { 12 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 13 | 14 | if (resolvedParams.command !== undefined && resolvedParams.command !== '') { 15 | exec(resolvedParams.command, (error, stdout) => { 16 | if (error) { 17 | logger.error(`action: problem executing shell action - ${error}`); 18 | return; 19 | } 20 | logger.debug(`shell: ${stdout}`); 21 | }); 22 | } 23 | } catch (error) { 24 | logger.error(`action: problem executing shell action - ${error}`); 25 | } 26 | this.emit('finished'); 27 | } 28 | } 29 | export default ShellAction; 30 | -------------------------------------------------------------------------------- /lib/src/actions/store-action.ts: -------------------------------------------------------------------------------- 1 | import { StoreActionParams } from '@showbridge/types'; 2 | import { set } from 'lodash-es'; 3 | import { Message } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Action from './action.js'; 6 | 7 | class StoreAction extends Action { 8 | _run(_msg: Message, vars) { 9 | const msg = this.getTransformedMessage(_msg, vars); 10 | try { 11 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 12 | if (resolvedParams.key !== undefined) { 13 | set(vars, resolvedParams.key, resolvedParams.value); 14 | } else { 15 | logger.error('action: store action missing a key'); 16 | } 17 | } catch (error) { 18 | logger.error(`action: problem executing store action - ${error}`); 19 | } 20 | this.emit('finished'); 21 | } 22 | } 23 | export default StoreAction; 24 | -------------------------------------------------------------------------------- /lib/src/actions/tcp-output-action.ts: -------------------------------------------------------------------------------- 1 | import { RouterVars, TCPOutputActionParams } from '@showbridge/types'; 2 | import { Message } from '../messages/index.js'; 3 | import { RouterProtocols } from '../router.js'; 4 | import { hexToBytes, logger } from '../utils/index.js'; 5 | import Action from './action.js'; 6 | 7 | class TCPOutputAction extends Action { 8 | _run(_msg: Message, vars: RouterVars, protocols: RouterProtocols) { 9 | const msg = this.getTransformedMessage(_msg, vars); 10 | let tcpSend; 11 | try { 12 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 13 | 14 | if ('bytes' in resolvedParams) { 15 | tcpSend = resolvedParams.bytes; 16 | } else if ('hex' in resolvedParams) { 17 | tcpSend = hexToBytes(resolvedParams.hex); 18 | } else if ('string' in resolvedParams) { 19 | tcpSend = resolvedParams.string; 20 | } 21 | 22 | if (tcpSend !== undefined) { 23 | protocols.tcp.send(Buffer.from(tcpSend), resolvedParams.port, resolvedParams.host, resolvedParams.slip); 24 | } else { 25 | logger.error('action: tcp-output has nothing to send'); 26 | } 27 | } catch (error) { 28 | logger.error(`action: problem executing tcp-output action - ${error}`); 29 | } 30 | this.emit('finished'); 31 | } 32 | } 33 | export default TCPOutputAction; 34 | -------------------------------------------------------------------------------- /lib/src/actions/udp-output-action.ts: -------------------------------------------------------------------------------- 1 | import { RouterVars, UDPOutputActionParams } from '@showbridge/types'; 2 | import { Message } from '../messages/index.js'; 3 | import { RouterProtocols } from '../router.js'; 4 | import { hexToBytes, logger } from '../utils/index.js'; 5 | import Action from './action.js'; 6 | 7 | class UDPOutputAction extends Action { 8 | _run(_msg: Message, vars: RouterVars, protocols: RouterProtocols) { 9 | const msg = this.getTransformedMessage(_msg, vars); 10 | let udpSend; 11 | try { 12 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 13 | 14 | if ('bytes' in resolvedParams) { 15 | udpSend = resolvedParams.bytes; 16 | } else if ('hex' in resolvedParams) { 17 | udpSend = hexToBytes(resolvedParams.hex); 18 | } else if ('string' in resolvedParams) { 19 | udpSend = resolvedParams.string; 20 | } 21 | 22 | if (udpSend !== undefined) { 23 | protocols.udp.send(Buffer.from(udpSend), resolvedParams.port, resolvedParams.host); 24 | } else { 25 | logger.error('action: udp-output has nothing to send'); 26 | } 27 | } catch (error) { 28 | logger.error(`action: problem executing udp-output action - ${error}`); 29 | } 30 | this.emit('finished'); 31 | } 32 | } 33 | export default UDPOutputAction; 34 | -------------------------------------------------------------------------------- /lib/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Config } from './config.js'; 2 | export * as Messages from './messages/index.js'; 3 | export { default as Router } from './router.js'; 4 | export * as Utils from './utils/index.js'; 5 | -------------------------------------------------------------------------------- /lib/src/messages/http-message.ts: -------------------------------------------------------------------------------- 1 | import { HTTPSender } from '@showbridge/types'; 2 | import { Response } from 'express'; 3 | 4 | class HTTPMessage { 5 | msg: any; 6 | response: Response; 7 | sender: HTTPSender; 8 | 9 | constructor(msg, response: Response) { 10 | this.msg = msg; 11 | 12 | this.response = response; 13 | 14 | this.sender = { 15 | protocol: 'tcp', 16 | address: msg.headers['x-forwarded-for'] || msg.connection.remoteAddress, 17 | }; 18 | if (this.sender?.address?.substr(0, 7) === '::ffff:') { 19 | this.sender.address = this.sender.address.substr(7); 20 | } 21 | } 22 | 23 | get messageType() { 24 | return 'http'; 25 | } 26 | 27 | get originalUrl(): string { 28 | return this.msg.originalUrl; 29 | } 30 | 31 | set originalUrl(value: string) { 32 | this.msg.originalUrl = value; 33 | } 34 | 35 | get baseUrl(): string { 36 | return this.msg.baseUrl; 37 | } 38 | 39 | set baseUrl(value: string) { 40 | this.msg.baseUrl = value; 41 | } 42 | 43 | get path(): string { 44 | return this.msg.path; 45 | } 46 | 47 | set path(value: string) { 48 | this.msg.path = value; 49 | } 50 | 51 | get body() { 52 | return this.msg.body; 53 | } 54 | 55 | set body(value) { 56 | this.msg.body = value; 57 | } 58 | 59 | get method(): string { 60 | return this.msg.method; 61 | } 62 | 63 | set method(value: string) { 64 | this.msg.method = value; 65 | } 66 | 67 | toString() { 68 | return `${this.originalUrl}`; 69 | } 70 | 71 | toJSON() { 72 | return { 73 | messageType: this.messageType, 74 | msg: { 75 | originalUrl: this.originalUrl, 76 | baseUrl: this.baseUrl, 77 | body: this.body, 78 | path: this.path, 79 | method: this.method, 80 | headers: this.msg.headers, 81 | connection: { 82 | remoteAddress: this.msg.connection.remoteAddress, 83 | }, 84 | }, 85 | }; 86 | } 87 | 88 | static fromJSON(json) { 89 | return new HTTPMessage(json.msg, undefined); 90 | } 91 | } 92 | export default HTTPMessage; 93 | -------------------------------------------------------------------------------- /lib/src/messages/index.ts: -------------------------------------------------------------------------------- 1 | import HTTPMessage from './http-message.js'; 2 | import MIDIMessage from './midi-message.js'; 3 | import MQTTMessage from './mqtt-message.js'; 4 | import OSCMessage from './osc-message.js'; 5 | import TCPMessage from './tcp-message.js'; 6 | import UDPMessage from './udp-message.js'; 7 | import WebSocketMessage from './websocket-message.js'; 8 | 9 | export { HTTPMessage, MIDIMessage, MQTTMessage, OSCMessage, TCPMessage, UDPMessage, WebSocketMessage }; 10 | 11 | export const MessageTypeClassMap = { 12 | http: HTTPMessage, 13 | midi: MIDIMessage, 14 | mqtt: MQTTMessage, 15 | osc: OSCMessage, 16 | tcp: TCPMessage, 17 | udp: UDPMessage, 18 | ws: WebSocketMessage, 19 | }; 20 | 21 | export type Message = HTTPMessage | MIDIMessage | MQTTMessage | OSCMessage | TCPMessage | UDPMessage | WebSocketMessage; 22 | export type ByteMessage = MIDIMessage | MQTTMessage | OSCMessage | TCPMessage | UDPMessage | WebSocketMessage; 23 | -------------------------------------------------------------------------------- /lib/src/messages/mqtt-message.ts: -------------------------------------------------------------------------------- 1 | class MQTTMessage { 2 | private msg: string | object; 3 | topic: string; 4 | processedPayload: string | object; 5 | 6 | constructor(msg: Buffer, topic: string) { 7 | this.payload = msg.toString(); 8 | this.topic = topic; 9 | } 10 | 11 | processPayload() { 12 | try { 13 | this.processedPayload = JSON.parse(this.msg.toString()); 14 | } catch (error) { 15 | this.processedPayload = this.msg; 16 | } 17 | } 18 | 19 | get messageType() { 20 | return 'mqtt'; 21 | } 22 | 23 | get payload() { 24 | return this.processedPayload; 25 | } 26 | 27 | set payload(value) { 28 | this.msg = value; 29 | this.processPayload(); 30 | } 31 | 32 | get bytes() { 33 | return Buffer.from(this.msg.toString()); 34 | } 35 | 36 | toString() { 37 | return `${this.topic} - ${this.msg}`; 38 | } 39 | 40 | toJSON() { 41 | return { 42 | messageType: this.messageType, 43 | msg: this.msg, 44 | topic: this.topic, 45 | }; 46 | } 47 | 48 | static fromJSON(json) { 49 | return new MQTTMessage(json.msg, json.topic); 50 | } 51 | } 52 | export default MQTTMessage; 53 | -------------------------------------------------------------------------------- /lib/src/messages/osc-message.ts: -------------------------------------------------------------------------------- 1 | import { OSCSender } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import { OscMessageOrBundle, toBuffer } from 'osc-min'; 4 | 5 | class OSCMessage { 6 | private msg: any; 7 | sender: OSCSender; 8 | 9 | constructor(msg: OscMessageOrBundle, sender: OSCSender) { 10 | this.msg = msg; 11 | this.msg.args = this.msg.args.map((arg) => { 12 | if (has(arg, 'value')) { 13 | return arg.value; 14 | } 15 | return arg; 16 | }); 17 | this.sender = sender; 18 | if (this.sender?.address?.substr(0, 7) === '::ffff:') { 19 | this.sender.address = this.sender.address.substr(7); 20 | } 21 | } 22 | 23 | get messageType() { 24 | return 'osc'; 25 | } 26 | 27 | get address() { 28 | return this.msg.address; 29 | } 30 | 31 | set address(address) { 32 | this.msg.address = address; 33 | } 34 | 35 | get addressParts() { 36 | return this.address.split('/').splice(1); 37 | } 38 | 39 | get args() { 40 | return this.msg.args; 41 | } 42 | 43 | set args(args) { 44 | this.msg.args = args; 45 | } 46 | 47 | get bytes() { 48 | return Uint8Array.from(toBuffer(this.msg)); 49 | } 50 | 51 | toString() { 52 | return `${this.address} ${this.args.join(' ')}`; 53 | } 54 | 55 | toJSON() { 56 | return { 57 | messageType: this.messageType, 58 | msg: this.msg, 59 | sender: this.sender, 60 | }; 61 | } 62 | 63 | static fromJSON(json) { 64 | return new OSCMessage(json.msg, json.sender); 65 | } 66 | } 67 | export default OSCMessage; 68 | -------------------------------------------------------------------------------- /lib/src/messages/tcp-message.ts: -------------------------------------------------------------------------------- 1 | import { TCPSender } from '@showbridge/types'; 2 | 3 | class TCPMessage { 4 | private msg: Buffer; 5 | sender: TCPSender; 6 | 7 | constructor(msg: Buffer, sender: TCPSender) { 8 | this.msg = msg; 9 | this.sender = sender; 10 | if (this.sender?.address?.substr(0, 7) === '::ffff:') { 11 | this.sender.address = this.sender.address.substr(7); 12 | } 13 | } 14 | 15 | get messageType() { 16 | return 'tcp'; 17 | } 18 | 19 | get bytes() { 20 | return Uint8Array.from(this.msg); 21 | } 22 | 23 | set bytes(bytes) { 24 | this.msg = Buffer.from(bytes); 25 | } 26 | 27 | get string() { 28 | return this.msg.toString(); 29 | } 30 | 31 | set string(string) { 32 | this.msg = Buffer.from(string); 33 | } 34 | 35 | toString() { 36 | return this.string; 37 | } 38 | 39 | toJSON() { 40 | return { 41 | messageType: this.messageType, 42 | msg: this.msg, 43 | sender: this.sender, 44 | }; 45 | } 46 | 47 | static fromJSON(json) { 48 | return new TCPMessage(json.msg, json.sender); 49 | } 50 | } 51 | export default TCPMessage; 52 | -------------------------------------------------------------------------------- /lib/src/messages/udp-message.ts: -------------------------------------------------------------------------------- 1 | import { UDPSender } from '@showbridge/types'; 2 | 3 | class UDPMessage { 4 | private msg: Buffer; 5 | sender: UDPSender; 6 | 7 | constructor(msg: Buffer, sender: UDPSender) { 8 | this.msg = msg; 9 | 10 | this.sender = sender; 11 | if (this.sender?.address.substr(0, 7) === '::ffff:') { 12 | this.sender.address = this.sender.address.substr(7); 13 | } 14 | } 15 | 16 | get messageType() { 17 | return 'udp'; 18 | } 19 | 20 | get bytes() { 21 | return Uint8Array.from(this.msg); 22 | } 23 | 24 | set bytes(bytes) { 25 | this.msg = Buffer.from(bytes); 26 | } 27 | 28 | get string() { 29 | return this.msg.toString(); 30 | } 31 | 32 | set string(string) { 33 | this.msg = Buffer.from(string); 34 | } 35 | 36 | toString() { 37 | return this.string; 38 | } 39 | 40 | toJSON() { 41 | return { 42 | messageType: this.messageType, 43 | msg: this.msg, 44 | sender: this.sender, 45 | }; 46 | } 47 | 48 | static fromJSON(json) { 49 | return new UDPMessage(json.msg, json.sender); 50 | } 51 | } 52 | export default UDPMessage; 53 | -------------------------------------------------------------------------------- /lib/src/messages/websocket-message.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketSender } from '@showbridge/types'; 2 | import { RawData } from 'ws'; 3 | 4 | export type WebUIPayload = { 5 | eventName: string; 6 | data: { 7 | [key: string]: any; 8 | }; 9 | }; 10 | 11 | class WebSocketMessage { 12 | private msg: string; 13 | sender: WebSocketSender; 14 | processedPayload: string | WebUIPayload | any; 15 | 16 | constructor(msg: RawData, sender: WebSocketSender) { 17 | this.payload = msg.toString(); 18 | this.sender = sender; 19 | 20 | if (this.sender?.address?.substr(0, 7) === '::ffff:') { 21 | this.sender.address = this.sender.address.substr(7); 22 | } 23 | } 24 | 25 | processPayload(): string | any { 26 | try { 27 | this.processedPayload = JSON.parse(this.msg.toString()); 28 | } catch (error) { 29 | this.processedPayload = this.msg.toString(); 30 | } 31 | } 32 | 33 | get messageType() { 34 | return 'ws'; 35 | } 36 | 37 | get payload() { 38 | return this.processedPayload; 39 | } 40 | 41 | set payload(payload: string) { 42 | this.msg = payload; 43 | this.processPayload(); 44 | } 45 | 46 | get bytes(): Buffer { 47 | return Buffer.from(this.msg.toString()); 48 | } 49 | 50 | toString(): string { 51 | return this.msg.toString(); 52 | } 53 | 54 | toJSON() { 55 | return { 56 | messageType: this.messageType, 57 | msg: this.msg, 58 | sender: this.sender, 59 | }; 60 | } 61 | 62 | static fromJSON(json) { 63 | return new WebSocketMessage(json.msg, json.sender); 64 | } 65 | } 66 | export default WebSocketMessage; 67 | -------------------------------------------------------------------------------- /lib/src/protocols/index.ts: -------------------------------------------------------------------------------- 1 | import CloudProtocol from './cloud-protocol.js'; 2 | import HTTPProtocol from './http-protocol.js'; 3 | import MIDIProtocol from './midi-protocol.js'; 4 | import MQTTProtocol from './mqtt-protocol.js'; 5 | import TCPProtocol from './tcp-protocol.js'; 6 | import UDPProtocol from './udp-protocol.js'; 7 | 8 | export { CloudProtocol, HTTPProtocol, MIDIProtocol, MQTTProtocol, TCPProtocol, UDPProtocol }; 9 | 10 | export const ProtocolTypeClassMap = { 11 | cloud: CloudProtocol, 12 | http: HTTPProtocol, 13 | midi: MIDIProtocol, 14 | mqtt: MQTTProtocol, 15 | tcp: TCPProtocol, 16 | udp: UDPProtocol, 17 | }; 18 | -------------------------------------------------------------------------------- /lib/src/protocols/protocol.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolObj } from '@showbridge/types/dist/models/protocol.js'; 2 | import EventEmitter from 'node:events'; 3 | import Router from '../router.js'; 4 | import { Templating } from '../utils/index.js'; 5 | 6 | class Protocol extends EventEmitter { 7 | router: Router; 8 | private obj: ProtocolObj; 9 | 10 | constructor(protocolObj: ProtocolObj, router: Router) { 11 | super(); 12 | this.router = router; 13 | this.obj = protocolObj; 14 | } 15 | 16 | get params() { 17 | return this.obj.params; 18 | } 19 | 20 | resolveTemplatedParams(data) { 21 | return Templating.resolveAllKeys(this.params, data); 22 | } 23 | 24 | toJSON() { 25 | return { 26 | params: this.params, 27 | }; 28 | } 29 | } 30 | export default Protocol; 31 | -------------------------------------------------------------------------------- /lib/src/transforms/floor-transform.ts: -------------------------------------------------------------------------------- 1 | import { FloorTransformParams } from '@showbridge/types'; 2 | import { get, set } from 'lodash-es'; 3 | import { Message } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Transform from './transform.js'; 6 | 7 | class FloorTransform extends Transform { 8 | _transform(msg: Message, vars) { 9 | logger.trace(`transform: before ${this.type} = ${msg}`); 10 | try { 11 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 12 | const propertyValue = get(msg, resolvedParams.property); 13 | if (propertyValue === undefined) { 14 | logger.error(`transform: floor transform could not find msg property = ${resolvedParams.property}`); 15 | return; 16 | } 17 | 18 | if (typeof propertyValue !== 'number') { 19 | logger.error('transform: floor only works on numbers'); 20 | return; 21 | } 22 | 23 | set(msg, resolvedParams.property, Math.floor(propertyValue)); 24 | logger.trace(`transform: after ${this.type} = ${msg}`); 25 | } catch (error) { 26 | logger.error(`transform: problem executing floor transform - ${error}`); 27 | } 28 | } 29 | } 30 | 31 | export default FloorTransform; 32 | -------------------------------------------------------------------------------- /lib/src/transforms/index.ts: -------------------------------------------------------------------------------- 1 | import FloorTransform from './floor-transform.js'; 2 | import LogTransform from './log-transform.js'; 3 | import MapTransform from './map-transform.js'; 4 | import PowerTransform from './power-transform.js'; 5 | import RoundTransform from './round-transform.js'; 6 | import ScaleTransform from './scale-transform.js'; 7 | import TemplateTransform from './template-transform.js'; 8 | 9 | export { 10 | FloorTransform, 11 | LogTransform, 12 | MapTransform, 13 | PowerTransform, 14 | RoundTransform, 15 | ScaleTransform, 16 | TemplateTransform, 17 | }; 18 | 19 | export const TransformTypeClassMap = { 20 | floor: FloorTransform, 21 | log: LogTransform, 22 | map: MapTransform, 23 | power: PowerTransform, 24 | round: RoundTransform, 25 | scale: ScaleTransform, 26 | template: TemplateTransform, 27 | }; 28 | -------------------------------------------------------------------------------- /lib/src/transforms/log-transform.ts: -------------------------------------------------------------------------------- 1 | import { LogTransformParams } from '@showbridge/types'; 2 | import { get, set } from 'lodash-es'; 3 | import { Message } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Transform from './transform.js'; 6 | 7 | class LogTransform extends Transform { 8 | _transform(msg: Message, vars) { 9 | logger.trace(`transform: before ${this.type} = ${msg}`); 10 | try { 11 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 12 | const propertyValue = get(msg, resolvedParams.property); 13 | if (propertyValue === undefined) { 14 | logger.error(`transform: log transform could not find msg property = ${resolvedParams.property}`); 15 | return; 16 | } 17 | 18 | if (typeof propertyValue !== 'number') { 19 | logger.error('transform: log can only operate on numbers'); 20 | return; 21 | } 22 | 23 | const newValue = Math.log(propertyValue) / Math.log(resolvedParams.base); 24 | set(msg, resolvedParams.property, newValue); 25 | 26 | logger.trace(`transform: after ${this.type} = ${msg}`); 27 | } catch (error) { 28 | logger.error(`transform: problem executing map transform - ${error}`); 29 | } 30 | } 31 | } 32 | 33 | export default LogTransform; 34 | -------------------------------------------------------------------------------- /lib/src/transforms/map-transform.ts: -------------------------------------------------------------------------------- 1 | import { MapTransformParams } from '@showbridge/types'; 2 | import { get, has, set } from 'lodash-es'; 3 | import { Message } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Transform from './transform.js'; 6 | 7 | class MapTransform extends Transform { 8 | _transform(msg: Message, vars) { 9 | logger.trace(`transform: before ${this.type} = ${msg}`); 10 | try { 11 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 12 | const propertyValue = get(msg, resolvedParams.property); 13 | if (has(resolvedParams.map, propertyValue)) { 14 | set(msg, resolvedParams.property, resolvedParams.map[propertyValue]); 15 | } 16 | logger.trace(`transform: after ${this.type} = ${msg}`); 17 | } catch (error) { 18 | logger.error(`transform: problem executing map transform - ${error}`); 19 | } 20 | } 21 | } 22 | 23 | export default MapTransform; 24 | -------------------------------------------------------------------------------- /lib/src/transforms/power-transform.ts: -------------------------------------------------------------------------------- 1 | import { PowerTransformParams } from '@showbridge/types'; 2 | import { get, set } from 'lodash-es'; 3 | import { Message } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Transform from './transform.js'; 6 | 7 | class PowerTransform extends Transform { 8 | _transform(msg: Message, vars) { 9 | logger.trace(`transform: before ${this.type} = ${msg}`); 10 | try { 11 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 12 | const propertyValue = get(msg, resolvedParams.property); 13 | if (propertyValue === undefined) { 14 | logger.error(`transform: power transform could not find msg property = ${resolvedParams.property}`); 15 | return; 16 | } 17 | 18 | if (typeof propertyValue !== 'number') { 19 | logger.error('transform: power can only operate on numbers'); 20 | return; 21 | } 22 | 23 | const newValue = propertyValue ** resolvedParams.exponent; 24 | set(msg, resolvedParams.property, newValue); 25 | 26 | logger.trace(`transform: after ${this.type} = ${msg}`); 27 | } catch (error) { 28 | logger.error(`transform: problem executing power transform - ${error}`); 29 | } 30 | } 31 | } 32 | 33 | export default PowerTransform; 34 | -------------------------------------------------------------------------------- /lib/src/transforms/round-transform.ts: -------------------------------------------------------------------------------- 1 | import { RoundTransformParams } from '@showbridge/types'; 2 | import { get, set } from 'lodash-es'; 3 | import { Message } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Transform from './transform.js'; 6 | 7 | class RoundTransform extends Transform { 8 | _transform(msg: Message, vars) { 9 | logger.trace(`transform: before ${this.type} = ${msg}`); 10 | try { 11 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 12 | const propertyValue = get(msg, resolvedParams.property); 13 | if (propertyValue === undefined) { 14 | logger.error(`transform: round transform could not find msg property = ${resolvedParams.property}`); 15 | return; 16 | } 17 | if (typeof propertyValue !== 'number') { 18 | logger.error('transform: round only works on numbers'); 19 | return; 20 | } 21 | 22 | set(msg, resolvedParams.property, Math.round(propertyValue)); 23 | logger.trace(`transform: after ${this.type} = ${msg}`); 24 | } catch (error) { 25 | logger.error(`transform: problem executing round transform - ${error}`); 26 | } 27 | } 28 | } 29 | 30 | export default RoundTransform; 31 | -------------------------------------------------------------------------------- /lib/src/transforms/scale-transform.ts: -------------------------------------------------------------------------------- 1 | import { ScaleTransformParams } from '@showbridge/types'; 2 | import { get, has, set } from 'lodash-es'; 3 | import { Message } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Transform from './transform.js'; 6 | 7 | class ScaleTransform extends Transform { 8 | _transform(msg: Message, vars) { 9 | logger.trace(`transform: before ${this.type} = ${msg}`); 10 | try { 11 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 12 | const propertyValue = get(msg, resolvedParams.property); 13 | if (propertyValue === undefined) { 14 | logger.error(`transform: scale transform could not find msg property = ${resolvedParams.property}`); 15 | return; 16 | } 17 | 18 | if (typeof propertyValue !== 'number') { 19 | logger.error('transform: scale only works on number values'); 20 | return; 21 | } 22 | 23 | if (!has(resolvedParams, 'inRange') || !has(resolvedParams, 'outRange')) { 24 | logger.error('transform: scale must have both an inRange and an outRange property'); 25 | return; 26 | } 27 | 28 | const { inRange, outRange } = resolvedParams; 29 | 30 | const scaledValue = 31 | ((propertyValue - inRange[0]) * (outRange[1] - outRange[0])) / (inRange[1] - inRange[0]) + outRange[0]; 32 | set(msg, resolvedParams.property, scaledValue); 33 | 34 | logger.trace(`transform: after ${this.type} = ${msg}`); 35 | } catch (error) { 36 | logger.error(`transform: problem executing scale transform - ${error}`); 37 | } 38 | } 39 | } 40 | 41 | export default ScaleTransform; 42 | -------------------------------------------------------------------------------- /lib/src/transforms/template-transform.ts: -------------------------------------------------------------------------------- 1 | import { TemplateTransformParams } from '@showbridge/types'; 2 | import { set } from 'lodash-es'; 3 | import { Message } from '../messages/index.js'; 4 | import { Templating, logger } from '../utils/index.js'; 5 | import Transform from './transform.js'; 6 | 7 | class TemplateTransform extends Transform { 8 | _transform(msg: Message, vars) { 9 | try { 10 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 11 | 12 | let newValue: string = Templating.getTemplateResult(resolvedParams.template, { msg, vars }); 13 | // NOTE(jwetzell): try to convert it to a number if it is one 14 | if (!Number.isNaN(parseFloat(newValue))) { 15 | if (newValue.includes('.')) { 16 | set(msg, resolvedParams.property, parseFloat(newValue)); 17 | } else { 18 | set(msg, resolvedParams.property, parseInt(newValue)); 19 | } 20 | } else { 21 | set(msg, resolvedParams.property, newValue); 22 | } 23 | 24 | logger.trace(`transform: after ${this.type} = ${msg}`); 25 | } catch (error) { 26 | logger.error(`transform: problem executing template transform - ${error}`); 27 | logger.error(error); 28 | } 29 | } 30 | } 31 | 32 | export default TemplateTransform; 33 | -------------------------------------------------------------------------------- /lib/src/transforms/transform.ts: -------------------------------------------------------------------------------- 1 | import { RouterVars, TransformObj } from '@showbridge/types'; 2 | import { Message } from '../messages/index.js'; 3 | import { disabled, Templating } from '../utils/index.js'; 4 | 5 | class Transform { 6 | private obj: TransformObj; 7 | 8 | constructor(transformObj: TransformObj) { 9 | this.obj = transformObj; 10 | } 11 | 12 | // eslint-disable-next-line no-underscore-dangle, no-unused-vars 13 | _transform(msg: Message, vars: RouterVars) {} 14 | 15 | transform(msg: Message, vars: RouterVars) { 16 | if (!this.enabled) { 17 | return; 18 | } 19 | 20 | this._transform(msg, vars); 21 | } 22 | 23 | get type() { 24 | return this.obj.type; 25 | } 26 | 27 | get params() { 28 | return this.obj.params; 29 | } 30 | 31 | get enabled() { 32 | return this.obj.enabled && !disabled.transforms.has(this.type); 33 | } 34 | 35 | get comment() { 36 | return this.obj.comment; 37 | } 38 | 39 | resolveTemplatedParams(data) { 40 | return Templating.resolveAllKeys(this.params, data); 41 | } 42 | 43 | toJSON() { 44 | return { 45 | type: this.type, 46 | params: this.params, 47 | enabled: this.enabled, 48 | comment: this.comment, 49 | }; 50 | } 51 | } 52 | export default Transform; 53 | -------------------------------------------------------------------------------- /lib/src/triggers/any-trigger.ts: -------------------------------------------------------------------------------- 1 | import { AnyTriggerParams } from '@showbridge/types'; 2 | import { Message } from '../messages/index.js'; 3 | import Trigger from './trigger.js'; 4 | 5 | class AnyTrigger extends Trigger { 6 | test(msg: Message) { 7 | return msg !== undefined; 8 | } 9 | } 10 | 11 | export default AnyTrigger; 12 | -------------------------------------------------------------------------------- /lib/src/triggers/bytes-equal-trigger.ts: -------------------------------------------------------------------------------- 1 | import { BytesEqualTriggerParams } from '@showbridge/types'; 2 | import { isEqual } from 'lodash-es'; 3 | import { MIDIMessage, MQTTMessage, OSCMessage, TCPMessage, UDPMessage, WebSocketMessage } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class BytesEqualTrigger extends Trigger { 8 | test(msg: MIDIMessage | UDPMessage | OSCMessage | TCPMessage | MQTTMessage | WebSocketMessage) { 9 | if (msg.bytes === undefined) { 10 | logger.error('trigger: bytes equality check attempted on msg that does not have bytes'); 11 | return false; 12 | } 13 | 14 | // NOTE(jwetzell): good we are looking at a message that has bytes 15 | const bytesToMatch = Uint8Array.from(this.params.bytes); 16 | return isEqual(msg.bytes, bytesToMatch); 17 | } 18 | } 19 | 20 | export default BytesEqualTrigger; 21 | -------------------------------------------------------------------------------- /lib/src/triggers/http-request-trigger.ts: -------------------------------------------------------------------------------- 1 | import { HTTPRequestTriggerParams } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import HTTPMessage from '../messages/http-message.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class HTTPRequestTrigger extends Trigger { 8 | test(msg: HTTPMessage) { 9 | let matched = true; 10 | 11 | if (msg.messageType !== 'http') { 12 | logger.error('trigger: http-request only works with http messages'); 13 | return false; 14 | } 15 | 16 | if (has(this.params, 'path')) { 17 | matched = matched && this.params.path === msg.path; 18 | } 19 | 20 | if (has(this.params, 'method')) { 21 | matched = matched && this.params.method.toUpperCase() === msg.method.toUpperCase(); 22 | } 23 | 24 | return matched; 25 | } 26 | } 27 | 28 | export default HTTPRequestTrigger; 29 | -------------------------------------------------------------------------------- /lib/src/triggers/index.ts: -------------------------------------------------------------------------------- 1 | import AnyTrigger from './any-trigger.js'; 2 | import BytesEqualTrigger from './bytes-equal-trigger.js'; 3 | import HTTPRequestTrigger from './http-request-trigger.js'; 4 | import MIDIControlChangeTrigger from './midi-control-change-trigger.js'; 5 | import MIDINoteOffTrigger from './midi-note-off-trigger.js'; 6 | import MIDINoteOnTrigger from './midi-note-on-trigger.js'; 7 | import MIDIPitchBendTrigger from './midi-pitch-bend-trigger.js'; 8 | import MIDIProgramChangeTrigger from './midi-program-change-trigger.js'; 9 | import MIDITrigger from './midi-trigger.js'; 10 | import MQTTTopicTrigger from './mqtt-topic-trigger.js'; 11 | import OSCAddressTrigger from './osc-address-trigger.js'; 12 | import RegexTrigger from './regex-trigger.js'; 13 | import SenderTrigger from './sender-trigger.js'; 14 | import Trigger from './trigger.js'; 15 | 16 | export { 17 | AnyTrigger, 18 | BytesEqualTrigger, 19 | HTTPRequestTrigger, 20 | MIDIControlChangeTrigger, 21 | MIDINoteOffTrigger, 22 | MIDINoteOnTrigger, 23 | MIDIPitchBendTrigger, 24 | MIDIProgramChangeTrigger, 25 | MIDITrigger, 26 | MQTTTopicTrigger, 27 | OSCAddressTrigger, 28 | RegexTrigger, 29 | SenderTrigger, 30 | Trigger, 31 | }; 32 | 33 | export const TriggerTypeClassMap = { 34 | any: AnyTrigger, 35 | 'bytes-equal': BytesEqualTrigger, 36 | 'http-request': HTTPRequestTrigger, 37 | 'midi-control-change': MIDIControlChangeTrigger, 38 | 'midi-note-off': MIDINoteOffTrigger, 39 | 'midi-note-on': MIDINoteOnTrigger, 40 | midi: MIDITrigger, 41 | 'midi-pitch-bend': MIDIPitchBendTrigger, 42 | 'midi-program-change': MIDIProgramChangeTrigger, 43 | 'osc-address': OSCAddressTrigger, 44 | 'mqtt-topic': MQTTTopicTrigger, 45 | regex: RegexTrigger, 46 | sender: SenderTrigger, 47 | }; 48 | -------------------------------------------------------------------------------- /lib/src/triggers/midi-control-change-trigger.ts: -------------------------------------------------------------------------------- 1 | import { MIDIControlChangeTriggerParams } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import { MIDIMessage } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class MIDIControlChangeTrigger extends Trigger { 8 | test(msg: MIDIMessage) { 9 | if (msg.messageType !== 'midi') { 10 | logger.error('trigger: midi-control-change trigger only works on midi messages'); 11 | return false; 12 | } 13 | 14 | if (msg.status !== 'control_change') { 15 | return false; 16 | } 17 | 18 | if (has(this.params, 'port') && this.params.port !== msg.port) { 19 | return false; 20 | } 21 | 22 | if (has(this.params, 'channel') && this.params.channel !== msg.channel) { 23 | return false; 24 | } 25 | 26 | if (has(this.params, 'control') && this.params.control !== msg.control) { 27 | return false; 28 | } 29 | 30 | if (has(this.params, 'value') && this.params.value !== msg.value) { 31 | return false; 32 | } 33 | 34 | // NOTE(jwetzell): if msg has passed all the above it is a match; 35 | return true; 36 | } 37 | } 38 | 39 | export default MIDIControlChangeTrigger; 40 | -------------------------------------------------------------------------------- /lib/src/triggers/midi-note-off-trigger.ts: -------------------------------------------------------------------------------- 1 | import { MIDINoteOffTriggerParams } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import { MIDIMessage } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class MIDINoteOffTrigger extends Trigger { 8 | test(msg: MIDIMessage) { 9 | if (msg.messageType !== 'midi') { 10 | logger.error('trigger: midi-note-off trigger only works on midi messages'); 11 | return false; 12 | } 13 | 14 | if (msg.status !== 'note_off') { 15 | return false; 16 | } 17 | 18 | if (has(this.params, 'port') && this.params.port !== msg.port) { 19 | return false; 20 | } 21 | 22 | if (has(this.params, 'channel') && this.params.channel !== msg.channel) { 23 | return false; 24 | } 25 | 26 | if (has(this.params, 'note') && this.params.note !== msg.note) { 27 | return false; 28 | } 29 | 30 | if (has(this.params, 'velocity') && this.params.velocity !== msg.velocity) { 31 | return false; 32 | } 33 | 34 | // NOTE(jwetzell): if msg has passed all the above it is a match; 35 | return true; 36 | } 37 | } 38 | 39 | export default MIDINoteOffTrigger; 40 | -------------------------------------------------------------------------------- /lib/src/triggers/midi-note-on-trigger.ts: -------------------------------------------------------------------------------- 1 | import { MIDINoteOnTriggerParams } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import { MIDIMessage } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class MIDINoteOnTrigger extends Trigger { 8 | test(msg: MIDIMessage) { 9 | if (msg.messageType !== 'midi') { 10 | logger.error('trigger: midi-note-on trigger only works on midi messages'); 11 | return false; 12 | } 13 | 14 | if (msg.status !== 'note_on') { 15 | return false; 16 | } 17 | 18 | if (has(this.params, 'port') && this.params.port !== msg.port) { 19 | return false; 20 | } 21 | 22 | if (has(this.params, 'channel') && this.params.channel !== msg.channel) { 23 | return false; 24 | } 25 | 26 | if (has(this.params, 'note') && this.params.note !== msg.note) { 27 | return false; 28 | } 29 | 30 | if (has(this.params, 'velocity') && this.params.velocity !== msg.velocity) { 31 | return false; 32 | } 33 | 34 | // NOTE(jwetzell): if msg has passed all the above it is a match; 35 | return true; 36 | } 37 | } 38 | 39 | export default MIDINoteOnTrigger; 40 | -------------------------------------------------------------------------------- /lib/src/triggers/midi-pitch-bend-trigger.ts: -------------------------------------------------------------------------------- 1 | import { MIDIPitchBendTriggerParams } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import { MIDIMessage } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class MIDIPitchBendTrigger extends Trigger { 8 | test(msg: MIDIMessage) { 9 | if (msg.messageType !== 'midi') { 10 | logger.error('trigger: midi-pitch-bend trigger only works on midi messages'); 11 | return false; 12 | } 13 | 14 | if (msg.status !== 'pitch_bend') { 15 | return false; 16 | } 17 | 18 | if (has(this.params, 'port') && this.params.port !== msg.port) { 19 | return false; 20 | } 21 | 22 | if (has(this.params, 'channel') && this.params.channel !== msg.channel) { 23 | return false; 24 | } 25 | 26 | if (has(this.params, 'value') && this.params.value !== msg.value) { 27 | return false; 28 | } 29 | 30 | // NOTE(jwetzell): if msg has passed all the above it is a match; 31 | return true; 32 | } 33 | } 34 | 35 | export default MIDIPitchBendTrigger; 36 | -------------------------------------------------------------------------------- /lib/src/triggers/midi-program-change-trigger.ts: -------------------------------------------------------------------------------- 1 | import { MIDIProgramChangeTriggerParams } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import { MIDIMessage } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class MIDIProgramChangeTrigger extends Trigger { 8 | test(msg: MIDIMessage) { 9 | if (msg.messageType !== 'midi') { 10 | logger.error('trigger: midi-program-change trigger only works on midi messages'); 11 | return false; 12 | } 13 | 14 | if (msg.status !== 'program_change') { 15 | return false; 16 | } 17 | 18 | if (has(this.params, 'port') && this.params.port !== msg.port) { 19 | return false; 20 | } 21 | 22 | if (has(this.params, 'channel') && this.params.channel !== msg.channel) { 23 | return false; 24 | } 25 | 26 | if (has(this.params, 'program') && this.params.program !== msg.program) { 27 | return false; 28 | } 29 | 30 | // NOTE(jwetzell): if msg has passed all the above it is a match; 31 | return true; 32 | } 33 | } 34 | 35 | export default MIDIProgramChangeTrigger; 36 | -------------------------------------------------------------------------------- /lib/src/triggers/midi-trigger.ts: -------------------------------------------------------------------------------- 1 | import { MIDITriggerParams, RouterVars } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import { MIDIMessage } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class MIDITrigger extends Trigger { 8 | test(msg: MIDIMessage, vars: RouterVars) { 9 | if (msg.messageType !== 'midi') { 10 | logger.error('trigger: midi trigger only works on midi messages'); 11 | return false; 12 | } 13 | 14 | const resolvedParams = this.resolveTemplatedParams({ msg, vars }); 15 | 16 | if (has(resolvedParams, 'port') && resolvedParams.port !== msg.port) { 17 | return false; 18 | } 19 | 20 | // NOTE(jwetzell): if msg has passed all the above it is a match; 21 | return true; 22 | } 23 | } 24 | 25 | export default MIDITrigger; 26 | -------------------------------------------------------------------------------- /lib/src/triggers/mqtt-topic-trigger.ts: -------------------------------------------------------------------------------- 1 | import { MQTTTopicTriggerParams } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import MQTTMessage from '../messages/mqtt-message.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class MQTTTopicTrigger extends Trigger { 8 | test(msg: MQTTMessage) { 9 | if (msg.messageType !== 'mqtt') { 10 | logger.error('trigger: mqtt-topic only works with mqtt messages'); 11 | return false; 12 | } 13 | 14 | if (!has(this.params, 'topic')) { 15 | logger.error('trigger: mqtt-topic has no topic configured'); 16 | return false; 17 | } 18 | 19 | // NOTE(jwetzell) convert osc wildcard into regex 20 | const regexString = this.params.topic.replaceAll('+', '[^/]+').replaceAll('#', '.+'); 21 | const topicRegex = new RegExp(`^${regexString}$`); 22 | return topicRegex.test(msg.topic); 23 | } 24 | } 25 | 26 | export default MQTTTopicTrigger; 27 | -------------------------------------------------------------------------------- /lib/src/triggers/osc-address-trigger.ts: -------------------------------------------------------------------------------- 1 | import { OSCAddressTriggerParams } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import { OSCMessage } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class OSCAddressTrigger extends Trigger { 8 | test(msg: OSCMessage) { 9 | if (msg.messageType !== 'osc') { 10 | logger.error('trigger: osc-address only works with osc messages'); 11 | return false; 12 | } 13 | 14 | if (!has(this.params, 'address')) { 15 | logger.error('trigger: osc-address has no address configured'); 16 | return false; 17 | } 18 | 19 | // NOTE(jwetzell) convert osc wildcard into regex 20 | const regexString = this.params.address 21 | .replaceAll('{', '(') 22 | .replaceAll('}', ')') 23 | .replaceAll(',', '|') 24 | .replaceAll('[!', '[^') 25 | .replaceAll('*', '[^/]+') 26 | .replaceAll('?', '.'); 27 | const addressRegex = new RegExp(`^${regexString}$`); 28 | return addressRegex.test(msg.address); 29 | } 30 | } 31 | 32 | export default OSCAddressTrigger; 33 | -------------------------------------------------------------------------------- /lib/src/triggers/regex-trigger.ts: -------------------------------------------------------------------------------- 1 | import { RegexTriggerParams } from '@showbridge/types'; 2 | import { get, has } from 'lodash-es'; 3 | import { Message } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class RegexTrigger extends Trigger { 8 | test(msg: Message) { 9 | if (!has(this.params, 'patterns') || !has(this.params, 'properties')) { 10 | logger.error('regex: must have both patterns and properties params'); 11 | return false; 12 | } 13 | 14 | if (this.params.patterns.length !== this.params.properties.length) { 15 | logger.error('trigger: regex trigger requires patterns and properties to be the same length'); 16 | return false; 17 | } 18 | 19 | let allRegexTestsPassed = true; 20 | 21 | for (let i = 0; i < this.params.patterns.length; i += 1) { 22 | const pattern = this.params.patterns[i]; 23 | const property = this.params.properties[i]; 24 | 25 | const regex = new RegExp(pattern, 'g'); 26 | const matchPropertyValue = get(msg, property); 27 | if (matchPropertyValue === undefined) { 28 | logger.error('trigger: regex is configured to look at a property that does not exist on this message.'); 29 | // NOTE(jwetzell): bad property config = no fire and since all must match we can stop here 30 | allRegexTestsPassed = false; 31 | } 32 | if (!regex.test(matchPropertyValue)) { 33 | // NOTE(jwetzell): property value doesn't fit regex = no fire and since all must match we can stop here 34 | allRegexTestsPassed = false; 35 | } 36 | } 37 | return allRegexTestsPassed; 38 | } 39 | } 40 | 41 | export default RegexTrigger; 42 | -------------------------------------------------------------------------------- /lib/src/triggers/sender-trigger.ts: -------------------------------------------------------------------------------- 1 | import { SenderTriggerParams } from '@showbridge/types'; 2 | import { has } from 'lodash-es'; 3 | import { HTTPMessage, TCPMessage, UDPMessage, WebSocketMessage } from '../messages/index.js'; 4 | import { logger } from '../utils/index.js'; 5 | import Trigger from './trigger.js'; 6 | 7 | class SenderTrigger extends Trigger { 8 | test(msg: HTTPMessage | TCPMessage | UDPMessage | WebSocketMessage) { 9 | if (!has(msg, 'sender')) { 10 | logger.error('trigger: host trigger attempted on message type that does not have host information'); 11 | return false; 12 | } 13 | 14 | if (has(this.params, 'address')) { 15 | return msg.sender.address === this.params.address; 16 | } 17 | 18 | // NOTE(jwetzell): default to a no match 19 | return false; 20 | } 21 | } 22 | 23 | export default SenderTrigger; 24 | -------------------------------------------------------------------------------- /lib/src/utils/disabling.ts: -------------------------------------------------------------------------------- 1 | const disabled = { 2 | actions: new Set(), 3 | protocols: new Set(), 4 | triggers: new Set(), 5 | transforms: new Set(), 6 | }; 7 | 8 | export default disabled; 9 | -------------------------------------------------------------------------------- /lib/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | const hexRegex = /^[0-9A-Fa-f\sx,]+$/; 2 | 3 | export function hexToBytes(hex: string): number[] { 4 | if (!hexRegex.test(hex)) { 5 | throw new Error('hex string contains invalid characters'); 6 | } 7 | const cleanHex = hex.replaceAll(' ', '').replaceAll('0x', '').replaceAll(',', ''); 8 | const bytes = []; 9 | for (let c = 0; c < cleanHex.length; c += 2) { 10 | bytes.push(parseInt(cleanHex.substr(c, 2), 16)); 11 | } 12 | return bytes; 13 | } 14 | 15 | export { default as disabled } from './disabling.js'; 16 | export { default as logger } from './logging.js'; 17 | export * as Templating from './templating.js'; 18 | -------------------------------------------------------------------------------- /lib/src/utils/logging.ts: -------------------------------------------------------------------------------- 1 | import { pino } from 'pino'; 2 | 3 | const logger = pino(); 4 | 5 | export default logger; 6 | -------------------------------------------------------------------------------- /lib/tests/config/bad_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "http": { 3 | "params": { 4 | "port": 3000 5 | }, 6 | "triggers": [ 7 | { 8 | "enabled": true, 9 | "actions": [{ "type": "log", "enabled": true }] 10 | } 11 | ] 12 | }, 13 | "ws": { 14 | "triggers": [ 15 | { 16 | "type": "any", 17 | "enabled": true, 18 | "actions": [{ "type": "log", "enabled": true }] 19 | } 20 | ] 21 | }, 22 | "osc": { 23 | "triggers": [ 24 | { 25 | "type": "any", 26 | "enabled": true, 27 | "actions": [{ "type": "log", "enabled": true }] 28 | } 29 | ] 30 | }, 31 | "midi": { 32 | "params": { 33 | "virtualInputName": "showbridge Input", 34 | "virtualOutputName": "showbridge Output" 35 | }, 36 | "triggers": [ 37 | { 38 | "type": "any", 39 | "enabled": true, 40 | "actions": [{ "type": "log", "enabled": true }] 41 | } 42 | ] 43 | }, 44 | "udp": { 45 | "params": { 46 | "port": 8000 47 | }, 48 | "triggers": [ 49 | { 50 | "type": "any", 51 | "enabled": true, 52 | "actions": [{ "type": "log", "enabled": true }] 53 | } 54 | ] 55 | }, 56 | "tcp": { 57 | "params": { 58 | "port": 8000 59 | }, 60 | "triggers": [ 61 | { 62 | "type": "any", 63 | "enabled": true, 64 | "actions": [{ "type": "log", "enabled": true }] 65 | } 66 | ] 67 | }, 68 | "mqtt": { 69 | "params": { 70 | "broker": "", 71 | "topics": [] 72 | }, 73 | "triggers": [ 74 | { 75 | "type": "any", 76 | "enabled": true, 77 | "actions": [{ "type": "log", "enabled": true }] 78 | } 79 | ] 80 | }, 81 | "cloud": { 82 | "params": { 83 | "url": "", 84 | "rooms": [] 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/tests/config/cloud_single_room_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "http": { 3 | "params": { 4 | "port": 3000 5 | }, 6 | "triggers": [ 7 | { 8 | "type": "any", 9 | "enabled": true, 10 | "actions": [{ "type": "log", "enabled": true }] 11 | } 12 | ] 13 | }, 14 | "ws": { 15 | "triggers": [ 16 | { 17 | "type": "any", 18 | "enabled": true, 19 | "actions": [{ "type": "log", "enabled": true }] 20 | } 21 | ] 22 | }, 23 | "osc": { 24 | "triggers": [ 25 | { 26 | "type": "any", 27 | "enabled": true, 28 | "actions": [{ "type": "log", "enabled": true }] 29 | } 30 | ] 31 | }, 32 | "midi": { 33 | "params": { 34 | "virtualInputName": "showbridge Input", 35 | "virtualOutputName": "showbridge Output" 36 | }, 37 | "triggers": [ 38 | { 39 | "type": "any", 40 | "enabled": true, 41 | "actions": [{ "type": "log", "enabled": true }] 42 | } 43 | ] 44 | }, 45 | "udp": { 46 | "params": { 47 | "port": 8000 48 | }, 49 | "triggers": [ 50 | { 51 | "type": "any", 52 | "enabled": true, 53 | "actions": [{ "type": "log", "enabled": true }] 54 | } 55 | ] 56 | }, 57 | "tcp": { 58 | "params": { 59 | "port": 8000 60 | }, 61 | "triggers": [ 62 | { 63 | "type": "any", 64 | "enabled": true, 65 | "actions": [{ "type": "log", "enabled": true }] 66 | } 67 | ] 68 | }, 69 | "mqtt": { 70 | "params": { 71 | "broker": "", 72 | "topics": [] 73 | }, 74 | "triggers": [ 75 | { 76 | "type": "any", 77 | "enabled": true, 78 | "actions": [{ "type": "log", "enabled": true }] 79 | } 80 | ] 81 | }, 82 | "cloud": { 83 | "params": { 84 | "url": "", 85 | "room": "test" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/tests/config/good_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./config.schema.json", 3 | "http": { 4 | "params": { 5 | "port": 3000 6 | }, 7 | "triggers": [ 8 | { 9 | "type": "any", 10 | "enabled": true, 11 | "actions": [{ "type": "log", "enabled": true }] 12 | } 13 | ] 14 | }, 15 | "ws": { 16 | "triggers": [ 17 | { 18 | "type": "any", 19 | "enabled": true, 20 | "actions": [{ "type": "log", "enabled": true }] 21 | } 22 | ] 23 | }, 24 | "osc": { 25 | "triggers": [] 26 | }, 27 | "midi": { 28 | "params": { 29 | "virtualInputName": "showbridge Input", 30 | "virtualOutputName": "showbridge Output" 31 | }, 32 | "triggers": [ 33 | { 34 | "type": "any", 35 | "enabled": true, 36 | "actions": [{ "type": "log", "enabled": true }] 37 | } 38 | ] 39 | }, 40 | "udp": { 41 | "params": { 42 | "port": 8000 43 | }, 44 | "triggers": [ 45 | { 46 | "type": "any", 47 | "enabled": true, 48 | "actions": [{ "type": "log", "enabled": true }] 49 | } 50 | ] 51 | }, 52 | "tcp": { 53 | "params": { 54 | "port": 8000 55 | }, 56 | "triggers": [ 57 | { 58 | "type": "any", 59 | "enabled": true, 60 | "actions": [{ "type": "log", "enabled": true }] 61 | } 62 | ] 63 | }, 64 | "mqtt": { 65 | "params": { 66 | "broker": "", 67 | "topics": [] 68 | }, 69 | "triggers": [ 70 | { 71 | "type": "any", 72 | "enabled": true, 73 | "actions": [{ "type": "log", "enabled": true }] 74 | } 75 | ] 76 | }, 77 | "cloud": { 78 | "params": { 79 | "url": "", 80 | "rooms": [] 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/tests/messages/mqtt-message.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import MQTTMessage from '../../dist/lib/messages/mqtt-message.js'; 4 | 5 | describe('MQTTMessage', () => { 6 | test('create MQTTMessage', () => { 7 | const message = new MQTTMessage('hello', 'test'); 8 | assert.equal(message.messageType, 'mqtt'); 9 | }); 10 | 11 | test('string MQTTMessage payload', () => { 12 | const payload = 'hello'; 13 | const topic = 'test'; 14 | const message = new MQTTMessage(payload, topic); 15 | assert.equal(typeof message.payload, 'string'); 16 | assert.equal(message.payload, payload); 17 | assert.deepEqual(message.bytes, Buffer.from(payload)); 18 | assert.deepEqual(message.toJSON(), { 19 | messageType: 'mqtt', 20 | msg: payload, 21 | topic, 22 | }); 23 | }); 24 | 25 | test('JSON MQTTMessage payload', () => { 26 | const payload = JSON.stringify({ key: 'hello' }); 27 | const topic = 'test'; 28 | 29 | const message = new MQTTMessage(payload, topic); 30 | assert.equal(typeof message.payload, 'object'); 31 | assert.deepEqual(message.payload, JSON.parse(payload)); 32 | assert.deepEqual(message.bytes, Buffer.from(payload)); 33 | assert.deepEqual(message.toJSON(), { 34 | messageType: 'mqtt', 35 | msg: payload, 36 | topic, 37 | }); 38 | }); 39 | 40 | test('MQTTMessage JSON conversion', () => { 41 | const message = new MQTTMessage('hello', 'test'); 42 | const toJSON = message.toJSON(); 43 | const fromJSON = MQTTMessage.fromJSON(toJSON); 44 | assert.deepEqual(fromJSON, message); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /lib/tests/messages/tcp-message.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import TCPMessage from '../../dist/lib/messages/tcp-message.js'; 4 | 5 | describe('TCPMessage', () => { 6 | test('create TCPMessage', () => { 7 | const message = new TCPMessage(Buffer.from('test'), { address: '127.0.0.1', port: 0 }); 8 | assert.notEqual(message, undefined); 9 | assert.equal(message.messageType, 'tcp'); 10 | }); 11 | 12 | test('string TCPMessage', () => { 13 | const message = new TCPMessage(Buffer.from('test'), { address: '127.0.0.1', port: 0 }); 14 | assert.notEqual(message, undefined); 15 | assert.equal(message.string, 'test'); 16 | assert.equal(message.toString(), 'test'); 17 | assert.deepEqual(message.bytes, Buffer.from('test')); 18 | }); 19 | 20 | test('TCPMessage JSON conversion', () => { 21 | const message = new TCPMessage(Buffer.from('test'), { address: '127.0.0.1', port: 0 }); 22 | 23 | const toJSON = message.toJSON(); 24 | 25 | const fromJSON = TCPMessage.fromJSON(toJSON); 26 | 27 | assert.deepEqual(fromJSON, message); 28 | }); 29 | 30 | test('TCPMessage IPv6 sender', () => { 31 | const message = new TCPMessage(Buffer.from('test'), { address: '::ffff:127.0.0.1', port: 0 }); 32 | assert.equal(message.sender.address, '127.0.0.1'); 33 | }); 34 | 35 | test('TCPMessage string setter', () => { 36 | const message = new TCPMessage(Buffer.from('test'), { address: '127.0.0.1', port: 0 }); 37 | assert.equal(message.string, 'test'); 38 | message.string = 'changed'; 39 | assert.equal(message.string, 'changed'); 40 | }); 41 | 42 | test('TCPMessage bytes setter', () => { 43 | const message = new TCPMessage(Buffer.from('test'), { address: '127.0.0.1', port: 0 }); 44 | assert.deepEqual(message.bytes, Buffer.from('test')); 45 | message.bytes = Buffer.from('changed'); 46 | assert.deepEqual(message.bytes, Buffer.from('changed')); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /lib/tests/messages/websocket-message.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import WebSocketMessage from '../../dist/lib/messages/websocket-message.js'; 4 | 5 | describe('WebSocketMessage', () => { 6 | test('create WebSocketMessage', () => { 7 | const message = new WebSocketMessage('hello', { address: '127.0.0.1', port: 0 }); 8 | assert.equal(message.messageType, 'ws'); 9 | }); 10 | 11 | test('string WebSocketMessage payload', () => { 12 | const payload = 'hello'; 13 | const sender = { address: '127.0.0.1', port: 0 }; 14 | const message = new WebSocketMessage(payload, sender); 15 | assert.equal(typeof message.payload, 'string'); 16 | assert.equal(message.payload, payload); 17 | assert.equal(message.toString(), payload); 18 | assert.deepEqual(message.bytes, Buffer.from(payload)); 19 | assert.deepEqual(message.toJSON(), { 20 | messageType: 'ws', 21 | msg: payload, 22 | sender, 23 | }); 24 | }); 25 | 26 | test('JSON WebSocketMessage payload', () => { 27 | const payload = JSON.stringify({ key: 'hello' }); 28 | const sender = { address: '127.0.0.1', port: 0 }; 29 | 30 | const message = new WebSocketMessage(payload, sender); 31 | assert.equal(typeof message.payload, 'object'); 32 | assert.deepEqual(message.payload, JSON.parse(payload)); 33 | assert.deepEqual(message.bytes, Buffer.from(payload)); 34 | assert.deepEqual(message.toJSON(), { 35 | messageType: 'ws', 36 | msg: payload, 37 | sender, 38 | }); 39 | }); 40 | 41 | test('WebSocketMessage JSON conversion', () => { 42 | const message = new WebSocketMessage('hello', { address: '127.0.0.1', port: 0 }); 43 | const toJSON = message.toJSON(); 44 | const fromJSON = WebSocketMessage.fromJSON(toJSON); 45 | assert.deepEqual(fromJSON, message); 46 | }); 47 | 48 | test('WebSocketMessage IPv6 sender', () => { 49 | const message = new WebSocketMessage('hello', { address: '::ffff:127.0.0.1', port: 0 }); 50 | assert.equal(message.sender.address, '127.0.0.1'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/tests/transforms/floor-transform.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import FloorTransform from '../../dist/lib/transforms/floor-transform.js'; 4 | 5 | describe('FloorTransform', () => { 6 | test('create', () => { 7 | const transform = new FloorTransform({ type: 'floor', params: { property: 'test' }, enabled: true }); 8 | assert.notEqual(transform, undefined); 9 | }); 10 | 11 | test('positive value', () => { 12 | const transform = new FloorTransform({ type: 'floor', params: { property: 'test' }, enabled: true }); 13 | const msg = { test: 100.6565 }; 14 | transform.transform(msg, {}); 15 | assert.strictEqual(msg.test, 100); 16 | }); 17 | 18 | test('negative value', () => { 19 | const transform = new FloorTransform({ type: 'floor', params: { property: 'test' }, enabled: true }); 20 | const msg = { test: -100.6565 }; 21 | transform.transform(msg, {}); 22 | assert.strictEqual(msg.test, -101); 23 | }); 24 | 25 | test('missing property', () => { 26 | const transform = new FloorTransform({ type: 'floor', params: {}, enabled: true }); 27 | const msg = { test: 100 }; 28 | transform.transform(msg, {}); 29 | assert.strictEqual(msg.test, 100); 30 | }); 31 | 32 | test("property value isn't a number", () => { 33 | const transform = new FloorTransform({ type: 'floor', params: { property: 'test' }, enabled: true }); 34 | const msg = { test: 'string' }; 35 | transform.transform(msg, {}); 36 | assert.strictEqual(msg.test, 'string'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /lib/tests/transforms/log-transform.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import LogTransform from '../../dist/lib/transforms/log-transform.js'; 4 | 5 | describe('LogTransform', () => { 6 | test('create', () => { 7 | const transform = new LogTransform({ type: 'log', params: { property: 'test', base: 10 }, enabled: true }); 8 | assert.notEqual(transform, undefined); 9 | }); 10 | 11 | test('complete params', () => { 12 | const transform = new LogTransform({ type: 'log', params: { property: 'test', base: 10 }, enabled: true }); 13 | const msg = { test: 100 }; 14 | transform.transform(msg, {}); 15 | assert.strictEqual(msg.test, 2); 16 | }); 17 | 18 | test('missing base', () => { 19 | const transform = new LogTransform({ type: 'log', params: { property: 'test' }, enabled: true }); 20 | const msg = { test: 100 }; 21 | transform.transform(msg, {}); 22 | assert.strictEqual(msg.test, NaN); 23 | }); 24 | 25 | test('missing property', () => { 26 | const transform = new LogTransform({ type: 'log', params: { base: 10 }, enabled: true }); 27 | const msg = { test: 100 }; 28 | transform.transform(msg, {}); 29 | assert.strictEqual(msg.test, 100); 30 | }); 31 | 32 | test("property value isn't a number", () => { 33 | const transform = new LogTransform({ type: 'log', params: { property: 'test', base: 10 }, enabled: true }); 34 | const msg = { test: 'string' }; 35 | transform.transform(msg, {}); 36 | assert.strictEqual(msg.test, 'string'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /lib/tests/transforms/map-transform.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import MapTransform from '../../dist/lib/transforms/map-transform.js'; 4 | 5 | describe('MapTransform', () => { 6 | test('create', () => { 7 | const transform = new MapTransform({ 8 | type: 'map', 9 | params: { property: 'test', map: { M: 'Monday', W: 'Wednesday' } }, 10 | enabled: true, 11 | }); 12 | assert.notEqual(transform, undefined); 13 | }); 14 | 15 | test('complete params', () => { 16 | const transform = new MapTransform({ 17 | type: 'map', 18 | params: { property: 'test', map: { M: 'Monday', W: 'Wednesday' } }, 19 | enabled: true, 20 | }); 21 | const msg = { test: 'M' }; 22 | transform.transform(msg, {}); 23 | assert.strictEqual(msg.test, 'Monday'); 24 | }); 25 | 26 | test('missing map', () => { 27 | const transform = new MapTransform({ type: 'map', params: { property: 'test' }, enabled: true }); 28 | const msg = { data: 'templated' }; 29 | transform.transform(msg, {}); 30 | assert.strictEqual(msg.test, undefined); 31 | }); 32 | 33 | test('missing property', () => { 34 | const transform = new MapTransform({ 35 | type: 'map', 36 | params: { map: { M: 'Monday', W: 'Wednesday' } }, 37 | enabled: true, 38 | }); 39 | const msg = { data: 'templated' }; 40 | transform.transform(msg, {}); 41 | assert.strictEqual(msg.test, undefined); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /lib/tests/transforms/power-transform.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import PowerTransform from '../../dist/lib/transforms/power-transform.js'; 4 | 5 | describe('PowerTransform', () => { 6 | test('create', () => { 7 | const transform = new PowerTransform({ type: 'power', params: { property: 'test', exponent: 4 }, enabled: true }); 8 | assert.notEqual(transform, undefined); 9 | }); 10 | 11 | test('complete params', () => { 12 | const transform = new PowerTransform({ type: 'power', params: { property: 'test', exponent: 4 }, enabled: true }); 13 | const msg = { test: 2 }; 14 | transform.transform(msg, {}); 15 | assert.strictEqual(msg.test, 16); 16 | }); 17 | 18 | test('missing power', () => { 19 | const transform = new PowerTransform({ type: 'power', params: { property: 'test' }, enabled: true }); 20 | const msg = { test: 100 }; 21 | transform.transform(msg, {}); 22 | assert.strictEqual(msg.test, NaN); 23 | }); 24 | 25 | test('missing property', () => { 26 | const transform = new PowerTransform({ type: 'power', params: { exponent: 4 }, enabled: true }); 27 | const msg = { test: 2 }; 28 | transform.transform(msg, {}); 29 | assert.strictEqual(msg.test, 2); 30 | }); 31 | 32 | test("property value isn't a number", () => { 33 | const transform = new PowerTransform({ type: 'power', params: { property: 'test', exponent: 4 }, enabled: true }); 34 | const msg = { test: 'string' }; 35 | transform.transform(msg, {}); 36 | assert.strictEqual(msg.test, 'string'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /lib/tests/transforms/round-transform.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import RoundTransform from '../../dist/lib/transforms/round-transform.js'; 4 | 5 | describe('RoundTransform', () => { 6 | test('create', () => { 7 | const transform = new RoundTransform({ type: 'round', params: { property: 'test' }, enabled: true }); 8 | assert.notEqual(transform, undefined); 9 | }); 10 | 11 | test('positive value', () => { 12 | const transform = new RoundTransform({ type: 'round', params: { property: 'test' }, enabled: true }); 13 | const msg = { test: 100.6565 }; 14 | transform.transform(msg, {}); 15 | assert.strictEqual(msg.test, 101); 16 | }); 17 | 18 | test('negative value', () => { 19 | const transform = new RoundTransform({ type: 'round', params: { property: 'test' }, enabled: true }); 20 | const msg = { test: -100.6565 }; 21 | transform.transform(msg, {}); 22 | assert.strictEqual(msg.test, -101); 23 | }); 24 | 25 | test('missing property', () => { 26 | const transform = new RoundTransform({ type: 'round', params: {}, enabled: true }); 27 | const msg = { test: 100 }; 28 | transform.transform(msg, {}); 29 | assert.strictEqual(msg.test, 100); 30 | }); 31 | 32 | test("property value isn't a number", () => { 33 | const transform = new RoundTransform({ type: 'round', params: { property: 'test' }, enabled: true }); 34 | const msg = { test: 'string' }; 35 | transform.transform(msg, {}); 36 | assert.strictEqual(msg.test, 'string'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /lib/tests/transforms/template-transform.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-template-curly-in-string */ 2 | import assert from 'node:assert'; 3 | import { describe, test } from 'node:test'; 4 | import TemplateTransform from '../../dist/lib/transforms/template-transform.js'; 5 | 6 | describe('TemplateTransform', () => { 7 | test('create', () => { 8 | const transform = new TemplateTransform({ 9 | type: 'template', 10 | params: { property: 'test', template: '${msg.data}' }, 11 | enabled: true, 12 | }); 13 | assert.notEqual(transform, undefined); 14 | }); 15 | 16 | test('complete params', () => { 17 | const transform = new TemplateTransform({ 18 | type: 'template', 19 | params: { property: 'test', template: '${msg.data}' }, 20 | enabled: true, 21 | }); 22 | const msg = { data: 'templated' }; 23 | transform.transform(msg, {}); 24 | assert.strictEqual(msg.test, 'templated'); 25 | }); 26 | 27 | test('missing template', () => { 28 | const transform = new TemplateTransform({ type: 'template', params: { property: 'test' }, enabled: true }); 29 | const msg = { data: 'templated' }; 30 | transform.transform(msg, {}); 31 | assert.strictEqual(msg.test, ''); 32 | }); 33 | 34 | test('missing property', () => { 35 | const transform = new TemplateTransform({ type: 'template', params: { template: '${msg.data}' }, enabled: true }); 36 | const msg = { data: 'templated' }; 37 | transform.transform(msg, {}); 38 | assert.strictEqual(msg.test, undefined); 39 | }); 40 | 41 | test('template result is a number', () => { 42 | const transform = new TemplateTransform({ 43 | type: 'template', 44 | params: { property: 'test', template: '${msg.data}' }, 45 | enabled: true, 46 | }); 47 | const msg = { data: '3' }; 48 | transform.transform(msg, {}); 49 | assert.strictEqual(msg.test, 3); 50 | }); 51 | 52 | test('template is malformed', () => { 53 | const transform = new TemplateTransform({ 54 | type: 'template', 55 | params: { property: 'test', template: '${test.test}' }, 56 | enabled: true, 57 | }); 58 | const msg = { data: '3' }; 59 | transform.transform(msg, {}); 60 | assert.strictEqual(msg.test, undefined); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /lib/tests/triggers/any-trigger.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import UDPMessage from '../../dist/lib/messages/udp-message.js'; 4 | import { AnyTrigger } from '../../dist/lib/triggers/index.js'; 5 | 6 | describe('AnyTrigger', () => { 7 | test('create', () => { 8 | const trigger = new AnyTrigger({ 9 | type: 'any', 10 | params: { 11 | address: '127.0.0.1', 12 | }, 13 | actions: [ 14 | { 15 | type: 'log', 16 | enabled: true, 17 | }, 18 | ], 19 | enabled: true, 20 | }); 21 | 22 | assert.notEqual(trigger, undefined); 23 | trigger.shouldFire({}); 24 | }); 25 | 26 | test('anything fires', () => { 27 | const trigger = new AnyTrigger({ 28 | type: 'any', 29 | params: { 30 | address: '127.0.0.1', 31 | }, 32 | actions: [ 33 | { 34 | type: 'log', 35 | enabled: true, 36 | }, 37 | ], 38 | enabled: true, 39 | }); 40 | 41 | const fired = trigger.shouldFire(new UDPMessage(Buffer.from('test'), { address: '127.0.0.1', port: 0 })); 42 | assert.strictEqual(fired, true); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /lib/tests/triggers/bytes-equal-trigger.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import UDPMessage from '../../dist/lib/messages/udp-message.js'; 4 | import { BytesEqualTrigger } from '../../dist/lib/triggers/index.js'; 5 | 6 | describe('BytesEqualTrigger', () => { 7 | test('create', () => { 8 | const trigger = new BytesEqualTrigger({ 9 | type: 'any', 10 | params: { 11 | address: '127.0.0.1', 12 | }, 13 | actions: [ 14 | { 15 | type: 'log', 16 | enabled: true, 17 | }, 18 | ], 19 | enabled: true, 20 | }); 21 | 22 | assert.notEqual(trigger, undefined); 23 | trigger.shouldFire({}); 24 | }); 25 | 26 | // TODO(jwetzell): test other message types 27 | 28 | test('bytes match', () => { 29 | const trigger = new BytesEqualTrigger({ 30 | type: 'any', 31 | params: { 32 | bytes: [0x01, 0x02, 0x03], 33 | }, 34 | actions: [ 35 | { 36 | type: 'log', 37 | enabled: true, 38 | }, 39 | ], 40 | enabled: true, 41 | }); 42 | 43 | const fired = trigger.shouldFire( 44 | new UDPMessage(Buffer.from([0x01, 0x02, 0x03]), { address: '127.0.0.1', port: 0 }) 45 | ); 46 | assert.strictEqual(fired, true); 47 | }); 48 | 49 | test('bytes mismatch', () => { 50 | const trigger = new BytesEqualTrigger({ 51 | type: 'any', 52 | params: { 53 | bytes: [0x01, 0x02, 0x03], 54 | }, 55 | actions: [ 56 | { 57 | type: 'log', 58 | enabled: true, 59 | }, 60 | ], 61 | enabled: true, 62 | }); 63 | 64 | const fired = trigger.shouldFire( 65 | new UDPMessage(Buffer.from([0x01, 0x02, 0x01]), { address: '127.0.0.1', port: 0 }) 66 | ); 67 | assert.strictEqual(fired, false); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /lib/tests/triggers/sender-trigger.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import UDPMessage from '../../dist/lib/messages/udp-message.js'; 4 | import { SenderTrigger } from '../../dist/lib/triggers/index.js'; 5 | 6 | describe('SenderTrigger', () => { 7 | test('create', () => { 8 | const trigger = new SenderTrigger({ 9 | type: 'sender', 10 | params: { 11 | address: '127.0.0.1', 12 | }, 13 | actions: [ 14 | { 15 | type: 'log', 16 | enabled: true, 17 | }, 18 | ], 19 | enabled: true, 20 | }); 21 | 22 | assert.notEqual(trigger, undefined); 23 | trigger.shouldFire({}); 24 | }); 25 | 26 | test('sender match', () => { 27 | const trigger = new SenderTrigger({ 28 | type: 'sender', 29 | params: { 30 | address: '127.0.0.1', 31 | }, 32 | actions: [ 33 | { 34 | type: 'log', 35 | enabled: true, 36 | }, 37 | ], 38 | enabled: true, 39 | }); 40 | 41 | const fired = trigger.shouldFire(new UDPMessage(Buffer.from('test'), { address: '127.0.0.1', port: 0 })); 42 | assert.strictEqual(fired, true); 43 | }); 44 | 45 | test('sender mismatch', () => { 46 | const trigger = new SenderTrigger({ 47 | type: 'sender', 48 | params: { 49 | address: '10.0.0.1', 50 | }, 51 | actions: [ 52 | { 53 | type: 'log', 54 | enabled: true, 55 | }, 56 | ], 57 | enabled: true, 58 | }); 59 | 60 | const fired = trigger.shouldFire(new UDPMessage(Buffer.from('test'), { address: '127.0.0.1', port: 0 })); 61 | assert.strictEqual(fired, false); 62 | }); 63 | 64 | test('no sender', () => { 65 | const trigger = new SenderTrigger({ 66 | type: 'sender', 67 | params: {}, 68 | actions: [ 69 | { 70 | type: 'log', 71 | enabled: true, 72 | }, 73 | ], 74 | enabled: true, 75 | }); 76 | 77 | const fired = trigger.shouldFire(new UDPMessage(Buffer.from('test'), { address: '127.0.0.1', port: 0 })); 78 | assert.strictEqual(fired, false); 79 | }); 80 | 81 | // TODO(jwetzell): add tests for other message types 82 | }); 83 | -------------------------------------------------------------------------------- /lib/tests/utils/index.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { describe, test } from 'node:test'; 3 | import { hexToBytes } from '../../dist/lib/utils/index.js'; 4 | 5 | describe('hexToBytes', () => { 6 | test('0x12 0x34 0x56', () => { 7 | const bytes = hexToBytes('0x12 0x34 0x56'); 8 | assert.deepEqual(bytes, [0x12, 0x34, 0x56]); 9 | }); 10 | 11 | test('0x12, 0x34, 0x56', () => { 12 | const bytes = hexToBytes('0x12, 0x34, 0x56'); 13 | assert.deepEqual(bytes, [0x12, 0x34, 0x56]); 14 | }); 15 | 16 | test('123456', () => { 17 | const bytes = hexToBytes('123456'); 18 | assert.deepEqual(bytes, [0x12, 0x34, 0x56]); 19 | }); 20 | 21 | test('12,34,56', () => { 22 | const bytes = hexToBytes('12,34,56'); 23 | assert.deepEqual(bytes, [0x12, 0x34, 0x56]); 24 | }); 25 | 26 | test('12 34 56', () => { 27 | const bytes = hexToBytes('12 34 56'); 28 | assert.deepEqual(bytes, [0x12, 0x34, 0x56]); 29 | }); 30 | 31 | test('invalid string', () => { 32 | assert.throws(() => { 33 | hexToBytes('asdfghjkl'); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/lib", 4 | "allowJs": true, 5 | "target": "ES2021", 6 | "module": "Node16", 7 | "moduleResolution": "Node16", 8 | "declaration": true 9 | }, 10 | "include": ["./src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "showbridge", 3 | "version": "0.0.0", 4 | "description": "Simple Protocol Router /s", 5 | "type": "module", 6 | "private": true, 7 | "scripts": { 8 | "install:all": "node scripts/install_all.js", 9 | "format:write": "prettier ./ --write" 10 | }, 11 | "author": { 12 | "name": "Joel Wetzell", 13 | "email": "me@jwetzell.com", 14 | "url": "https://jwetzell.com" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/jwetzell/showbridge.git" 19 | }, 20 | "keywords": [ 21 | "show", 22 | "control", 23 | "protocol", 24 | "router", 25 | "theatre" 26 | ], 27 | "license": "MIT", 28 | "devDependencies": { 29 | "@types/lodash-es": "^4.17.12", 30 | "eslint": "8.57.0", 31 | "eslint-config-airbnb": "19.0.4", 32 | "eslint-config-prettier": "10.0.2", 33 | "prettier": "3.5.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sample/config/midi-to-cue.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../schema/config.schema.json", 3 | "protocols": { 4 | "http": { 5 | "params": { 6 | "port": 3000 7 | } 8 | }, 9 | "midi": { 10 | "params": {} 11 | }, 12 | "tcp": { 13 | "params": { 14 | "address": "0.0.0.0", 15 | "port": 8000 16 | } 17 | }, 18 | "udp": { 19 | "params": { 20 | "address": "0.0.0.0", 21 | "port": 8000 22 | } 23 | }, 24 | "mqtt": { 25 | "params": { 26 | "broker": "", 27 | "topics": [] 28 | } 29 | } 30 | }, 31 | "handlers": { 32 | "http": { 33 | "triggers": [] 34 | }, 35 | "ws": { 36 | "triggers": [] 37 | }, 38 | "osc": { 39 | "triggers": [] 40 | }, 41 | "midi": { 42 | "triggers": [ 43 | { 44 | "type": "midi-note-on", 45 | "params": { 46 | "velocity": 127 47 | }, 48 | "actions": [ 49 | { 50 | "type": "log", 51 | "enabled": true 52 | }, 53 | { 54 | "type": "osc-output", 55 | "params": { 56 | "host": "127.0.0.1", 57 | "protocol": "udp", 58 | "_address": "/cue/${msg.note}/go", 59 | "_args": [], 60 | "port": 53000 61 | }, 62 | "enabled": true 63 | } 64 | ], 65 | "enabled": true 66 | } 67 | ] 68 | }, 69 | "tcp": { 70 | "triggers": [] 71 | }, 72 | "udp": { 73 | "triggers": [] 74 | }, 75 | "mqtt": { 76 | "triggers": [] 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /sample/vars/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "patches": { 3 | "midi": [], 4 | "network": [] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /sample/vars/dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "value", 3 | "key": "anothervalue", 4 | "patches": { 5 | "midi": [ 6 | { 7 | "name": "Test 2", 8 | "port": "showbridge dev Input" 9 | }, 10 | { 11 | "name": "Test", 12 | "port": "IAC Driver Bus 1" 13 | } 14 | ], 15 | "network": [ 16 | { 17 | "name": "localhost", 18 | "host": "127.0.0.1", 19 | "port": 53000 20 | }, 21 | { 22 | "name": "Protokol", 23 | "host": "127.0.0.1", 24 | "port": 9000 25 | } 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /scripts/check_format_lint.js: -------------------------------------------------------------------------------- 1 | import cp from 'node:child_process'; 2 | 3 | const eslintProcess = cp.spawnSync('eslint', ['./'], { 4 | stdio: 'inherit', 5 | }); 6 | 7 | if (eslintProcess.error) { 8 | console.error(eslintProcess.error); 9 | } 10 | 11 | const lintError = eslintProcess.error || eslintProcess.status > 0; 12 | 13 | const prettierProcess = cp.spawnSync('prettier', ['./', '--check'], { 14 | stdio: 'inherit', 15 | }); 16 | 17 | if (prettierProcess.error) { 18 | console.error(prettierProcess.error); 19 | } 20 | 21 | const formatError = prettierProcess.error || prettierProcess.status > 0; 22 | 23 | if (lintError || formatError) { 24 | console.error(`Formatting or linting error found.`); 25 | process.exit(1); 26 | } else { 27 | console.log('No formatting or linting errors found.'); 28 | } 29 | -------------------------------------------------------------------------------- /scripts/install_all.js: -------------------------------------------------------------------------------- 1 | import cp from 'node:child_process'; 2 | import os from 'node:os'; 3 | import path from 'node:path'; 4 | 5 | const projectFolders = [ 6 | path.resolve(import.meta.dirname, '../cli'), 7 | path.resolve(import.meta.dirname, '../cloud'), 8 | path.resolve(import.meta.dirname, '../docs'), 9 | path.resolve(import.meta.dirname, '../launcher'), 10 | path.resolve(import.meta.dirname, '../lib'), 11 | path.resolve(import.meta.dirname, '../site'), 12 | path.resolve(import.meta.dirname, '../types'), 13 | path.resolve(import.meta.dirname, '../webui'), 14 | ]; 15 | 16 | const processes = []; 17 | 18 | projectFolders.forEach((projectFolder) => { 19 | console.log(`spawning npm ci process for ${projectFolder}`); 20 | const childProcess = cp.spawn(os.platform() === 'win32' ? 'npm.cmd' : 'npm', ['ci'], { 21 | env: process.env, 22 | cwd: projectFolder, 23 | stdio: 'inherit', 24 | }); 25 | processes.push(childProcess); 26 | }); 27 | 28 | process.on('SIGINT', () => { 29 | console.log(); 30 | console.log('killing processes'); 31 | console.log(); 32 | processes.forEach((process) => { 33 | process.kill('SIGINT'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /scripts/setup_dev.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { existsSync, readFileSync } from 'fs'; 3 | import path from 'node:path'; 4 | 5 | const projectRoot = path.join(import.meta.dirname, '../'); 6 | let folder = ''; 7 | 8 | if (process.argv.length === 3) { 9 | folder = path.join(projectRoot, process.argv[2]); 10 | } else { 11 | console.error('must pass folder to setup as argument'); 12 | process.exit(1); 13 | } 14 | if (folder === '') { 15 | console.error('must pass folder to setup as argument'); 16 | process.exit(1); 17 | } 18 | 19 | const libraryInfo = JSON.parse(readFileSync(path.join(folder, 'package.json'))); 20 | 21 | console.log(`setting up dev for ${libraryInfo.name}`); 22 | 23 | const showbridgeDependencies = Object.keys(libraryInfo.dependencies).filter((pkg) => pkg.startsWith('@showbridge')); 24 | 25 | if (showbridgeDependencies.length === 0) { 26 | console.error('no showbridge dependencies found in requested folder'); 27 | process.exit(1); 28 | } 29 | 30 | const libraries = showbridgeDependencies.map((pkg) => pkg.replace('@showbridge/', '')); 31 | 32 | libraries.forEach((library) => { 33 | const libraryPath = path.join(projectRoot, library); 34 | if (existsSync(path.join(libraryPath, 'setup_dev.js'))) { 35 | execSync(`cd ${libraryPath} && npm run build && node setup_dev.js`); 36 | } 37 | console.log(`building and linking ${library}`); 38 | execSync(`cd ${libraryPath} && npm run build:dev && npm link`, { 39 | stdio: 'inherit', 40 | }); 41 | }); 42 | 43 | execSync(`cd ${folder} && npm link ${showbridgeDependencies.join(' ')}`, { 44 | stdio: 'inherit', 45 | }); 46 | 47 | if (existsSync(path.join(folder, 'setup_dev.js'))) { 48 | execSync(`cd ${folder} && node setup_dev.js`); 49 | } 50 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@showbridge/site", 3 | "version": "1.0.0", 4 | "description": "www.showbrigde.io", 5 | "main": "index.js", 6 | "scripts": { 7 | "tailwind": "tailwindcss -i tailwind.css -o src/assets/css/tailwind.css", 8 | "dev": "http-server src/" 9 | }, 10 | "author": { 11 | "name": "Joel Wetzell", 12 | "email": "me@jwetzell.com", 13 | "url": "https://jwetzell.com" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jwetzell/showbridge.git" 18 | }, 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/jwetzell/showbridge/issues" 22 | }, 23 | "homepage": "https://github.com/jwetzell/showbridge#readme", 24 | "devDependencies": { 25 | "http-server": "14.1.1", 26 | "tailwindcss": "4.1.7", 27 | "@tailwindcss/cli": "4.1.7" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /site/src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/site/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /site/src/assets/images/icon512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/site/src/assets/images/icon512x512.png -------------------------------------------------------------------------------- /site/src/assets/js/tracking.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/site/src/assets/js/tracking.js -------------------------------------------------------------------------------- /site/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['src/**/*.html'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /site/tailwind.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | -------------------------------------------------------------------------------- /types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@showbridge/types", 3 | "version": "0.5.0", 4 | "description": "type library for showbridge", 5 | "main": "", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "prebuild": "rimraf dist", 12 | "build": "tsc", 13 | "prepack": "npm run build" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jwetzell/showbridge.git" 18 | }, 19 | "author": { 20 | "name": "Joel Wetzell", 21 | "email": "me@jwetzell.com", 22 | "url": "https://jwetzell.com" 23 | }, 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@types/node": "22.15.18", 27 | "rimraf": "6.0.1", 28 | "typescript": "5.8.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /types/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models/action'; 2 | export * from './models/config'; 3 | export * from './models/params'; 4 | export * from './models/router'; 5 | export * from './models/sender'; 6 | export * from './models/transform'; 7 | export * from './models/trigger'; 8 | -------------------------------------------------------------------------------- /types/src/models/action.ts: -------------------------------------------------------------------------------- 1 | import { TransformParams } from './params'; 2 | import { TransformObj } from './transform'; 3 | 4 | export type ActionObj = { 5 | type: string; 6 | params?: T; 7 | transforms: TransformObj[]; 8 | enabled: boolean; 9 | comment?: string; 10 | }; 11 | -------------------------------------------------------------------------------- /types/src/models/config.ts: -------------------------------------------------------------------------------- 1 | import { TriggerParams } from './params'; 2 | import { 3 | CloudProtocolParams, 4 | HTTPProtocolParams, 5 | MIDIProtocolParams, 6 | MQTTProtocolParams, 7 | TCPProtocolParams, 8 | UDPProtocolParams, 9 | } from './params/protocols'; 10 | import { ProtocolObj } from './protocol'; 11 | import { TriggerObj } from './trigger'; 12 | 13 | export type HandlerObj = { 14 | triggers: TriggerObj[]; 15 | }; 16 | 17 | export type ConfigObj = { 18 | $schema?: string; 19 | protocols: { 20 | cloud: ProtocolObj; 21 | http: ProtocolObj; 22 | midi: ProtocolObj; 23 | mqtt: ProtocolObj; 24 | tcp: ProtocolObj; 25 | udp: ProtocolObj; 26 | }; 27 | handlers: { 28 | http: HandlerObj; 29 | midi: HandlerObj; 30 | mqtt: HandlerObj; 31 | osc: HandlerObj; 32 | tcp: HandlerObj; 33 | udp: HandlerObj; 34 | ws: HandlerObj; 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /types/src/models/params/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './transforms'; 3 | export * from './triggers'; 4 | -------------------------------------------------------------------------------- /types/src/models/params/protocols.ts: -------------------------------------------------------------------------------- 1 | export type CloudProtocolParams = { 2 | url?: string; 3 | rooms?: string[]; 4 | }; 5 | 6 | export type HTTPProtocolParams = { 7 | address?: string; 8 | port: number; 9 | }; 10 | 11 | export type MIDIProtocolParams = { 12 | virtualInputName?: string; 13 | virtualOutputName?: string; 14 | }; 15 | 16 | export type MQTTProtocolParams = { 17 | broker: string; 18 | username?: string; 19 | password?: string; 20 | topics?: string[]; 21 | }; 22 | 23 | export type TCPProtocolParams = { 24 | address?: string; 25 | port: number; 26 | }; 27 | 28 | export type UDPProtocolParams = { 29 | address?: string; 30 | port: number; 31 | }; 32 | 33 | export type ProtocolParams = 34 | | CloudProtocolParams 35 | | HTTPProtocolParams 36 | | MIDIProtocolParams 37 | | MQTTProtocolParams 38 | | TCPProtocolParams 39 | | UDPProtocolParams; 40 | -------------------------------------------------------------------------------- /types/src/models/params/transforms.ts: -------------------------------------------------------------------------------- 1 | export type FloorTransformParams = { 2 | property: string; 3 | }; 4 | 5 | export type LogTransformParams = { 6 | property: string; 7 | base: number; 8 | }; 9 | 10 | export type MapTransformParams = { 11 | property: string; 12 | map: { 13 | [k: string]: unknown; 14 | }; 15 | }; 16 | 17 | export type PowerTransformParams = { 18 | property: string; 19 | exponent: number; 20 | }; 21 | 22 | export type RoundTransformParams = { 23 | property: string; 24 | }; 25 | 26 | export type ScaleTransformParams = { 27 | property: string; 28 | inRange: [number, number]; 29 | outRange: [number, number]; 30 | }; 31 | 32 | export type TemplateTransformParams = { 33 | property: string; 34 | template: string; 35 | }; 36 | 37 | export type TransformParams = 38 | | FloorTransformParams 39 | | LogTransformParams 40 | | MapTransformParams 41 | | PowerTransformParams 42 | | RoundTransformParams 43 | | ScaleTransformParams 44 | | TemplateTransformParams; 45 | -------------------------------------------------------------------------------- /types/src/models/params/triggers.ts: -------------------------------------------------------------------------------- 1 | export type AnyTriggerParams = undefined; 2 | 3 | export type BytesEqualTriggerParams = { 4 | bytes: number[]; 5 | }; 6 | 7 | export type HTTPRequestTriggerParams = { 8 | path?: string; 9 | method?: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; 10 | }; 11 | 12 | export type MIDITriggerParams = { 13 | port?: string; 14 | }; 15 | 16 | export type MIDIControlChangeTriggerParams = { 17 | port?: string; 18 | channel?: number; 19 | control?: number; 20 | value?: number; 21 | }; 22 | 23 | export type MIDINoteOffTriggerParams = { 24 | port?: string; 25 | channel?: number; 26 | note?: number; 27 | velocity?: number; 28 | }; 29 | 30 | export type MIDINoteOnTriggerParams = { 31 | port?: string; 32 | channel?: number; 33 | note?: number; 34 | velocity?: number; 35 | }; 36 | 37 | export type MIDIPitchBendTriggerParams = { 38 | port?: string; 39 | channel?: number; 40 | value?: number; 41 | }; 42 | 43 | export type MIDIProgramChangeTriggerParams = { 44 | port?: string; 45 | channel?: number; 46 | program?: number; 47 | }; 48 | 49 | export type MQTTTopicTriggerParams = { 50 | topic: string; 51 | }; 52 | 53 | export type OSCAddressTriggerParams = { 54 | address: string; 55 | }; 56 | 57 | export type RegexTriggerParams = { 58 | patterns: string[]; 59 | properties: string[]; 60 | }; 61 | 62 | export type SenderTriggerParams = { 63 | address: string; 64 | }; 65 | 66 | export type TriggerParams = 67 | | AnyTriggerParams 68 | | BytesEqualTriggerParams 69 | | HTTPRequestTriggerParams 70 | | MIDITriggerParams 71 | | MIDIControlChangeTriggerParams 72 | | MIDINoteOffTriggerParams 73 | | MIDINoteOnTriggerParams 74 | | MIDIPitchBendTriggerParams 75 | | MIDIProgramChangeTriggerParams 76 | | MQTTTopicTriggerParams 77 | | OSCAddressTriggerParams 78 | | RegexTriggerParams 79 | | SenderTriggerParams; 80 | -------------------------------------------------------------------------------- /types/src/models/protocol.ts: -------------------------------------------------------------------------------- 1 | export type ProtocolObj = { 2 | params?: T; 3 | }; 4 | -------------------------------------------------------------------------------- /types/src/models/router.ts: -------------------------------------------------------------------------------- 1 | export type MIDIPatch = Patch & { 2 | port: string; 3 | }; 4 | 5 | export type NetworkPatch = Patch & { 6 | host: string; 7 | port: number; 8 | }; 9 | 10 | type Patch = { 11 | name: string; 12 | }; 13 | 14 | export type RouterVars = { 15 | patches?: { 16 | midi?: MIDIPatch[]; 17 | network?: NetworkPatch[]; 18 | }; 19 | [k: string]: any; 20 | }; 21 | -------------------------------------------------------------------------------- /types/src/models/sender.ts: -------------------------------------------------------------------------------- 1 | export type TCPSender = { 2 | protocol: 'tcp'; 3 | address: string; 4 | port: number; 5 | }; 6 | 7 | export type UDPSender = { 8 | protocol: 'udp'; 9 | address: string; 10 | port: number; 11 | }; 12 | 13 | export type OSCSender = UDPSender | TCPSender; 14 | 15 | export type HTTPSender = { 16 | protocol: 'tcp'; 17 | address: string; 18 | }; 19 | 20 | export type WebSocketSender = { 21 | protocol: 'tcp'; 22 | address: string; 23 | port: number; 24 | }; 25 | -------------------------------------------------------------------------------- /types/src/models/transform.ts: -------------------------------------------------------------------------------- 1 | export type TransformObj = { 2 | type: string; 3 | params?: T; 4 | enabled: boolean; 5 | comment?: string; 6 | }; 7 | -------------------------------------------------------------------------------- /types/src/models/trigger.ts: -------------------------------------------------------------------------------- 1 | import { ActionObj } from './action'; 2 | import { ActionParams, TriggerParams } from './params'; 3 | 4 | export type TriggerObj = { 5 | type: string; 6 | params?: T; 7 | enabled: boolean; 8 | comment?: string; 9 | actions: ActionObj[]; 10 | subTriggers: TriggerObj[]; 11 | }; 12 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "target": "ES2021", 5 | "module": "Node16", 6 | "moduleResolution": "Node16", 7 | "declaration": true, 8 | "emitDeclarationOnly": true 9 | }, 10 | "include": ["./src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /webui/.postcssrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@tailwindcss/postcss": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@showbridge/webui", 3 | "version": "0.10.0", 4 | "author": { 5 | "name": "Joel Wetzell", 6 | "email": "me@jwetzell.com", 7 | "url": "https://jwetzell.com/" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jwetzell/showbridge.git" 12 | }, 13 | "scripts": { 14 | "build": "ng build --configuration production", 15 | "build:demo": "ng build --configuration demo", 16 | "build:dev": "ng build --configuration development --stats-json", 17 | "dev": "ng build --watch --configuration development --stats-json", 18 | "prepack": "npm run build" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "dependencies": { 24 | "@angular/animations": "19.2.11", 25 | "@angular/cdk": "19.2.16", 26 | "@angular/common": "19.2.11", 27 | "@angular/compiler": "19.2.11", 28 | "@angular/core": "19.2.11", 29 | "@angular/forms": "19.2.11", 30 | "@angular/material": "19.2.16", 31 | "@angular/platform-browser": "19.2.11", 32 | "@angular/platform-browser-dynamic": "19.2.11", 33 | "@angular/router": "19.2.11", 34 | "ajv": "8.17.1", 35 | "lodash-es": "4.17.21", 36 | "ngx-timeago": "3.0.0", 37 | "rxjs": "7.8.2", 38 | "tslib": "2.8.1", 39 | "zone.js": "0.15.0" 40 | }, 41 | "devDependencies": { 42 | "@angular/build": "19.2.12", 43 | "@angular/cli": "19.2.12", 44 | "@angular/compiler-cli": "19.2.11", 45 | "@showbridge/types": "0.5.0", 46 | "@types/lodash-es": "4.17.12", 47 | "autoprefixer": "10.4.21", 48 | "postcss": "8.5.3", 49 | "tailwindcss": "4.1.6", 50 | "@tailwindcss/postcss": "4.1.6", 51 | "typescript": "5.8.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /webui/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/app/app.component.css -------------------------------------------------------------------------------- /webui/src/app/components/action/action.component.css: -------------------------------------------------------------------------------- 1 | .cdk-drag-placeholder { 2 | opacity: 0; 3 | } 4 | -------------------------------------------------------------------------------- /webui/src/app/components/array-form/array-form.component.css: -------------------------------------------------------------------------------- 1 | .cdk-drag-placeholder { 2 | opacity: 0; 3 | } 4 | -------------------------------------------------------------------------------- /webui/src/app/components/array-form/array-form.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | {{ i }}: 10 | 11 | 15 | 16 | 17 | 18 | 25 | 26 | = minItems" 29 | matTooltip="Remove"> 30 | remove 31 | 32 | 38 | 39 | 40 | 41 | 46 | add 47 | 48 | 49 | -------------------------------------------------------------------------------- /webui/src/app/components/clipboard-dialog/clipboard-dialog.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/app/components/clipboard-dialog/clipboard-dialog.component.css -------------------------------------------------------------------------------- /webui/src/app/components/clipboard-dialog/clipboard-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Import to clipboard 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | Cancel 16 | 17 | Import 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /webui/src/app/components/clipboard-dialog/clipboard-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AbstractControl, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms'; 3 | import { MatDialogRef } from '@angular/material/dialog'; 4 | import { CopyService } from 'src/app/services/copy.service'; 5 | 6 | @Component({ 7 | selector: 'app-clipboard-dialog', 8 | templateUrl: './clipboard-dialog.component.html', 9 | styleUrl: './clipboard-dialog.component.css', 10 | standalone: false, 11 | }) 12 | export class ClipboardDialogComponent { 13 | formGroup?: FormGroup; 14 | 15 | constructor( 16 | private copyService: CopyService, 17 | public dialogRef: MatDialogRef 18 | ) { 19 | this.formGroup = new FormGroup({ 20 | clipboard: new FormControl(null, [Validators.required, this.jsonValidator()]), 21 | }); 22 | } 23 | 24 | jsonValidator() { 25 | return (control: AbstractControl): ValidationErrors | null => { 26 | try { 27 | const value = JSON.parse(control.value); 28 | return null; 29 | } catch (error) { 30 | console.log(error); 31 | return { json: true }; 32 | } 33 | }; 34 | } 35 | 36 | importClipboard() { 37 | if (this.formGroup?.valid) { 38 | const jsonSnippet = JSON.parse(this.formGroup.value.clipboard); 39 | this.copyService.setSnippet(jsonSnippet); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /webui/src/app/components/config/config.component.css: -------------------------------------------------------------------------------- 1 | ::ng-deep .mdc-tab { 2 | border: 2px solid black !important; 3 | } 4 | 5 | .mat-mdc-tab-group { 6 | height: 100%; 7 | } 8 | 9 | ::ng-deep .mat-mdc-tab-body-wrapper { 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /webui/src/app/components/config/config.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | {{ messageType.name }} 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | {{ messageType.name }} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /webui/src/app/components/config/config.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { ConfigFile } from 'src/app/models/config.models'; 3 | import { ObjectInfo } from 'src/app/models/form.model'; 4 | import { EventService } from 'src/app/services/event.service'; 5 | import { SchemaService } from 'src/app/services/schema.service'; 6 | import { SettingsService } from 'src/app/services/settings.service'; 7 | 8 | @Component({ 9 | selector: 'app-config', 10 | templateUrl: './config.component.html', 11 | styleUrls: ['./config.component.css'], 12 | standalone: false, 13 | }) 14 | export class ConfigComponent { 15 | @Input() config!: ConfigFile; 16 | @Output() updated: EventEmitter = new EventEmitter(); 17 | pendingUpdate?: ConfigFile; 18 | selectedMessageType: ObjectInfo = this.schemaService.handlerTypes[0]; 19 | 20 | messageAndProtocolTypes: ObjectInfo[]; 21 | 22 | constructor( 23 | public schemaService: SchemaService, 24 | public eventService: EventService, 25 | public settingsService: SettingsService 26 | ) { 27 | this.messageAndProtocolTypes = []; 28 | 29 | this.schemaService.handlerTypes.forEach((handlerInfo) => { 30 | if (this.messageAndProtocolTypes.find((objectInfo) => objectInfo.type === handlerInfo.type) === undefined) { 31 | this.messageAndProtocolTypes.push(handlerInfo); 32 | } 33 | }); 34 | this.schemaService.protocolTypes.forEach((protocolInfo) => { 35 | if (this.messageAndProtocolTypes.find((objectInfo) => objectInfo.type === protocolInfo.type) === undefined) { 36 | this.messageAndProtocolTypes.push(protocolInfo); 37 | } 38 | }); 39 | } 40 | 41 | handlerUpdate(type: string) { 42 | if (this.config.handlers[type] === undefined) { 43 | this.config.handlers[type] = { 44 | triggers: [], 45 | }; 46 | } 47 | this.updated.emit(true); 48 | } 49 | 50 | protocolUpdate(type: string) { 51 | if (this.config.protocols[type] === undefined) { 52 | this.config.protocols[type] = { 53 | params: {}, 54 | }; 55 | } 56 | this.updated.emit(true); 57 | } 58 | 59 | selectMessageType(messageType: ObjectInfo) { 60 | this.selectedMessageType = messageType; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /webui/src/app/components/import-json/import-json.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/app/components/import-json/import-json.component.css -------------------------------------------------------------------------------- /webui/src/app/components/import-json/import-json.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ this.data.title }} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Cancel 11 | 12 | Import 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /webui/src/app/components/import-json/import-json.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { AbstractControl, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms'; 3 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 4 | import { SomeJSONSchema } from 'ajv/dist/types/json-schema'; 5 | import { SchemaService } from 'src/app/services/schema.service'; 6 | 7 | type ImportJSONComponentData = { 8 | schema?: SomeJSONSchema; 9 | title: string; 10 | }; 11 | 12 | @Component({ 13 | selector: 'app-import-json', 14 | templateUrl: './import-json.component.html', 15 | styleUrls: ['./import-json.component.css'], 16 | standalone: false, 17 | }) 18 | export class ImportJSONComponent { 19 | readonly data = inject(MAT_DIALOG_DATA); 20 | 21 | formGroup?: FormGroup; 22 | 23 | constructor( 24 | private schemaService: SchemaService, 25 | public dialogRef: MatDialogRef 26 | ) { 27 | if (this.data.schema !== undefined) { 28 | console.log(this.data.schema); 29 | this.formGroup = new FormGroup({ 30 | json: new FormControl(null, [ 31 | Validators.required, 32 | this.isJSON, 33 | this.schemaService.jsonValidator(this.data.schema), 34 | ]), 35 | }); 36 | } else { 37 | this.formGroup = new FormGroup({ 38 | json: new FormControl(null, [Validators.required, this.isJSON]), 39 | }); 40 | } 41 | 42 | this.formGroup.valueChanges.subscribe(console.log); 43 | } 44 | 45 | isJSON(control: AbstractControl): ValidationErrors | null { 46 | console.log(control); 47 | try { 48 | JSON.parse(control.value); 49 | return null; 50 | } catch (error) { 51 | return { json: error }; 52 | } 53 | } 54 | 55 | importJson() { 56 | if (this.formGroup?.valid) { 57 | this.dialogRef.close(JSON.parse(this.formGroup.controls['json'].value)); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /webui/src/app/components/message-type/message-type.component.css: -------------------------------------------------------------------------------- 1 | .cdk-drag-placeholder { 2 | opacity: 0; 3 | } 4 | -------------------------------------------------------------------------------- /webui/src/app/components/midi-info-dialog/midi-info-dialog.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/app/components/midi-info-dialog/midi-info-dialog.component.css -------------------------------------------------------------------------------- /webui/src/app/components/midi-info-dialog/midi-info-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Device Type 9 | {{ midiDevice.type }} 10 | 11 | 12 | 13 | Port Name 14 | 19 | {{ midiDevice.name }} 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /webui/src/app/components/midi-info-dialog/midi-info-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Clipboard } from '@angular/cdk/clipboard'; 2 | import { Component, Inject } from '@angular/core'; 3 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { MIDIStatus } from 'src/app/models/events.model'; 6 | 7 | @Component({ 8 | selector: 'app-midi-info-dialog', 9 | templateUrl: './midi-info-dialog.component.html', 10 | styleUrls: ['./midi-info-dialog.component.css'], 11 | standalone: false, 12 | }) 13 | export class MIDIInfoDialogComponent { 14 | constructor( 15 | public dialogRef: MatDialogRef, 16 | @Inject(MAT_DIALOG_DATA) public data: MIDIStatus, 17 | private clipboard: Clipboard, 18 | private snackbar: MatSnackBar 19 | ) {} 20 | 21 | getTableData() { 22 | return this.data.devices; 23 | } 24 | 25 | copyName(name: string) { 26 | this.clipboard.copy(name); 27 | this.snackbar.open('Name copied!', 'Dismiss', { duration: 2000 }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webui/src/app/components/params-form/params-form.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/app/components/params-form/params-form.component.css -------------------------------------------------------------------------------- /webui/src/app/components/patch-editor/patch-editor.component.css: -------------------------------------------------------------------------------- 1 | @reference "tailwindcss"; 2 | 3 | input { 4 | @apply w-full pl-2 text-gray-200 bg-gray-900; 5 | } 6 | 7 | select { 8 | @apply w-full text-gray-200 bg-gray-500; 9 | } 10 | 11 | .cdk-drag-placeholder { 12 | opacity: 0; 13 | } 14 | -------------------------------------------------------------------------------- /webui/src/app/components/transform/transform.component.css: -------------------------------------------------------------------------------- 1 | .cdk-drag-placeholder { 2 | opacity: 0; 3 | } 4 | -------------------------------------------------------------------------------- /webui/src/app/components/trigger/trigger.component.css: -------------------------------------------------------------------------------- 1 | .cdk-drag-placeholder { 2 | opacity: 0; 3 | } 4 | -------------------------------------------------------------------------------- /webui/src/app/models/config.models.ts: -------------------------------------------------------------------------------- 1 | import { TriggerObj, TriggerParams } from '@showbridge/types'; 2 | 3 | export type HandlersConfiguration = { [key: string]: HandlerConfiguration }; 4 | export type ProtocolsConfiguration = { [key: string]: ProtocolConfiguration }; 5 | 6 | export type ConfigFile = { 7 | protocols: ProtocolsConfiguration; 8 | handlers: HandlersConfiguration; 9 | }; 10 | 11 | export type HandlerConfiguration = { 12 | params?: { 13 | [k: string]: string | number; 14 | }; 15 | triggers?: TriggerObj[]; 16 | }; 17 | export type ProtocolConfiguration = { 18 | params?: { 19 | [k: string]: string | number; 20 | }; 21 | triggers?: TriggerObj[]; 22 | }; 23 | 24 | export type CloudConfiguration = { 25 | params: 26 | | { 27 | url: string; 28 | room: string; 29 | } 30 | | { 31 | url: string; 32 | rooms: string[]; 33 | }; 34 | }; 35 | 36 | export type ConfigState = { 37 | config: ConfigFile; 38 | timestamp: number; 39 | isLive: boolean; 40 | isCurrent: boolean; 41 | }; 42 | -------------------------------------------------------------------------------- /webui/src/app/models/copy-object.model.ts: -------------------------------------------------------------------------------- 1 | import { ActionObj, ActionParams, TransformObj, TransformParams, TriggerObj, TriggerParams } from '@showbridge/types'; 2 | 3 | export type TriggerCopyObject = { 4 | type: 'Trigger'; 5 | object: TriggerObj | TriggerObj[]; 6 | }; 7 | 8 | export type ActionCopyObject = { 9 | type: 'Action'; 10 | object: ActionObj | ActionObj[]; 11 | }; 12 | 13 | export type TransformCopyObject = { 14 | type: 'Transform'; 15 | object: TransformObj | TransformObj[]; 16 | }; 17 | 18 | export type CopyObject = TriggerCopyObject | TransformCopyObject | ActionCopyObject; 19 | -------------------------------------------------------------------------------- /webui/src/app/models/events.model.ts: -------------------------------------------------------------------------------- 1 | export type MessageEventData = { 2 | eventName: 'messageIn'; 3 | data: { 4 | type: string; 5 | }; 6 | }; 7 | 8 | export type TriggerEventData = { 9 | eventName: 'trigger'; 10 | data: { 11 | path: string; 12 | fired: boolean; 13 | }; 14 | }; 15 | 16 | export type ActionEventData = { 17 | eventName: 'action'; 18 | data: { 19 | path: string; 20 | fired: boolean; 21 | }; 22 | }; 23 | 24 | export type TransformEventData = { 25 | eventName: 'transform'; 26 | data: { 27 | path: string; 28 | fired: boolean; 29 | }; 30 | }; 31 | 32 | export type ProtocolStatusEventData = { 33 | eventName?: 'protocolStatus'; 34 | data: { 35 | cloud?: CloudStatus; 36 | http?: HTTPStatus; 37 | midi?: MIDIStatus; 38 | mqtt?: MQTTStatus; 39 | tcp?: TCPStatus; 40 | udp?: UDPStatus; 41 | }; 42 | }; 43 | 44 | export type CloudStatus = { 45 | enabled: boolean; 46 | connected: boolean; 47 | id?: string; 48 | roundtripMs?: number; 49 | }; 50 | 51 | export type HTTPStatus = { 52 | enabled: boolean; 53 | listening: boolean; 54 | address: { 55 | port: number; 56 | family: string; 57 | address: string; 58 | }; 59 | }; 60 | 61 | export type MIDIStatus = { 62 | enabled: boolean; 63 | devices: MIDIDeviceInfo[]; 64 | }; 65 | 66 | export type MIDIDeviceInfo = { 67 | type: string; 68 | name: string; 69 | }; 70 | 71 | export type MQTTStatus = { 72 | enabled: boolean; 73 | connected: boolean; 74 | broker: string; 75 | }; 76 | 77 | export type TCPStatus = { 78 | enabled: boolean; 79 | listening: boolean; 80 | address: { 81 | port: number; 82 | family: string; 83 | address: string; 84 | }; 85 | }; 86 | 87 | export type UDPStatus = { 88 | enabled: boolean; 89 | listening: boolean; 90 | address: { 91 | port: number; 92 | family: string; 93 | address: string; 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /webui/src/app/models/form.model.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup } from '@angular/forms'; 2 | 3 | export type ObjectInfo = { 4 | name: string; 5 | type: string; 6 | schema: any; 7 | }; 8 | 9 | export type ParamsFormInfo = { 10 | formGroup: FormGroup; 11 | paramsInfo: ParamsInfo; 12 | }; 13 | 14 | export type ParamsInfo = { 15 | [key: string]: ParamInfo; 16 | }; 17 | 18 | export type ParamInfo = { 19 | key: string; 20 | display: string; 21 | hint: string; 22 | type: string; 23 | placeholder?: string; 24 | options?: string[]; 25 | isTemplated: boolean; 26 | canTemplate: boolean; 27 | isConst: boolean; 28 | schema: any; 29 | }; 30 | -------------------------------------------------------------------------------- /webui/src/app/models/template.model.ts: -------------------------------------------------------------------------------- 1 | import { CopyObject } from './copy-object.model'; 2 | 3 | export type GenericTemplateObject = { 4 | id?: number; 5 | object: T; 6 | description?: string; 7 | tags?: string; 8 | }; 9 | 10 | export type TemplateObject = CopyObject & { 11 | id?: number; 12 | description?: string; 13 | tags?: string; 14 | }; 15 | -------------------------------------------------------------------------------- /webui/src/app/services/lists.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class ListsService { 7 | public actionListIds: string[] = []; 8 | public transformListIds: string[] = []; 9 | public triggerListIds: string[] = []; 10 | 11 | registerActionList(path: string | undefined) { 12 | if (path === undefined) { 13 | return ''; 14 | } 15 | 16 | const id = this.pathToId(path); 17 | if (!this.actionListIds.includes(id)) { 18 | this.actionListIds.push(id); 19 | } 20 | return id; 21 | } 22 | 23 | registerTransformList(path: string | undefined) { 24 | if (path === undefined) { 25 | return ''; 26 | } 27 | 28 | const id = this.pathToId(path); 29 | if (!this.transformListIds.includes(id)) { 30 | this.transformListIds.push(id); 31 | } 32 | return id; 33 | } 34 | 35 | registerTriggerList(path: string | undefined) { 36 | if (path === undefined) { 37 | return ''; 38 | } 39 | 40 | const id = this.pathToId(path); 41 | if (!this.triggerListIds.includes(id)) { 42 | this.triggerListIds.push(id); 43 | } 44 | return id; 45 | } 46 | 47 | pathToId(path: string) { 48 | return path.replaceAll('/', '.'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /webui/src/app/services/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class SettingsService { 8 | public isDummySite: boolean = false; 9 | public configPath: string = '/config'; 10 | public schemaPath: string = '/config/schema'; 11 | public varsPath: string = '/vars'; 12 | public baseUrl: string = window.location.href; 13 | 14 | public configUrl: BehaviorSubject = new BehaviorSubject(new URL(this.configPath, this.baseUrl)); 15 | public schemaUrl: BehaviorSubject = new BehaviorSubject(new URL(this.schemaPath, this.baseUrl)); 16 | public websocketUrl: BehaviorSubject = new BehaviorSubject(new URL(this.baseUrl.replace('http', 'ws'))); 17 | public varsUrl: BehaviorSubject = new BehaviorSubject(new URL(this.varsPath, this.baseUrl)); 18 | constructor() {} 19 | 20 | setupForDummySite() { 21 | this.isDummySite = true; 22 | this.configPath = '/config.json'; 23 | this.schemaPath = '/config.schema.json'; 24 | this.updateBaseUrl(window.location.href); 25 | } 26 | 27 | updateBaseUrl(baseUrl: string) { 28 | this.baseUrl = baseUrl; 29 | this.configUrl.next(new URL(this.configPath, baseUrl)); 30 | this.schemaUrl.next(new URL(this.schemaPath, baseUrl)); 31 | this.websocketUrl.next(new URL(this.baseUrl.replace('http', 'ws'))); 32 | this.varsUrl.next(new URL(this.varsPath, baseUrl)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /webui/src/app/services/vars.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { MIDIPatch, NetworkPatch, RouterVars } from '@showbridge/types'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | import { SettingsService } from './settings.service'; 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class VarsService { 10 | varsUrl?: URL; 11 | 12 | public currentVars: BehaviorSubject = new BehaviorSubject({}); 13 | 14 | constructor( 15 | private http: HttpClient, 16 | private settingsService: SettingsService 17 | ) { 18 | settingsService.varsUrl.subscribe((url) => { 19 | console.log(`vars url: ${url.toString()}`); 20 | this.varsUrl = url; 21 | this.loadVars(); 22 | }); 23 | } 24 | 25 | loadVars() { 26 | if (this.varsUrl) { 27 | this.http.get(this.varsUrl.toString()).subscribe((vars) => { 28 | this.currentVars.next(vars); 29 | }); 30 | } else { 31 | throw new Error('vars: no vars url set'); 32 | } 33 | } 34 | 35 | uploadVars(vars: RouterVars) { 36 | if (this.varsUrl) { 37 | return this.http.post(this.varsUrl.toString(), vars, { 38 | headers: { 39 | 'Content-Type': 'application/json', 40 | }, 41 | }); 42 | } else { 43 | throw new Error('vars: no vars url set'); 44 | } 45 | } 46 | 47 | updateMIDIPatches(patches: MIDIPatch[]) { 48 | if (this.varsUrl) { 49 | return this.http.post(`${this.varsUrl?.toString()}/patches/midi`, patches, { 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | }, 53 | }); 54 | } else { 55 | throw new Error('vars: no vars url set'); 56 | } 57 | } 58 | 59 | updateNetworkPatches(patches: NetworkPatch[]) { 60 | if (this.varsUrl) { 61 | return this.http.post(`${this.varsUrl?.toString()}/patches/network`, patches, { 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | }, 65 | }); 66 | } else { 67 | throw new Error('vars: no vars url set'); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /webui/src/app/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export function downloadJSON(data: object, filename: string) { 2 | const content = JSON.stringify(data, null, 2); 3 | const dataUri = URL.createObjectURL( 4 | new Blob([content], { 5 | type: 'text/json;charset=utf-8', 6 | }) 7 | ); 8 | const dummyLink = document.createElement('a'); 9 | dummyLink.href = dataUri; 10 | dummyLink.download = filename; 11 | 12 | document.body.appendChild(dummyLink); 13 | dummyLink.click(); 14 | document.body.removeChild(dummyLink); 15 | } 16 | -------------------------------------------------------------------------------- /webui/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/.gitkeep -------------------------------------------------------------------------------- /webui/src/assets/css/styles.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | background-color: rgb(30, 30, 30); 5 | } 6 | body { 7 | margin: 0; 8 | font-family: Roboto, 'Helvetica Neue', sans-serif; 9 | } 10 | 11 | .cdk-drop-list-dragging .cdk-drag { 12 | transition: transform 200ms cubic-bezier(0, 0, 0.2, 1); 13 | } 14 | 15 | .cdk-drag-animating { 16 | transition: transform 200ms cubic-bezier(0, 0, 0.2, 1); 17 | } 18 | -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fBBc4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fBBc4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fBBc4.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOmCnqEu92Fr1Mu4WxKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOmCnqEu92Fr1Mu4WxKOzY.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOmCnqEu92Fr1Mu4mxK.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOmCnqEu92Fr1Mu4mxK.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOmCnqEu92Fr1Mu5mxKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOmCnqEu92Fr1Mu5mxKOzY.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOmCnqEu92Fr1Mu72xKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOmCnqEu92Fr1Mu72xKOzY.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOmCnqEu92Fr1Mu7GxKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOmCnqEu92Fr1Mu7GxKOzY.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOmCnqEu92Fr1Mu7WxKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOmCnqEu92Fr1Mu7WxKOzY.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/KFOmCnqEu92Fr1Mu7mxKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/KFOmCnqEu92Fr1Mu7mxKOzY.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2 -------------------------------------------------------------------------------- /webui/src/assets/fonts/gok-H7zzDkdnRel8-DQ6KAXJ69wP1tGnf4ZGhUce.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/fonts/gok-H7zzDkdnRel8-DQ6KAXJ69wP1tGnf4ZGhUce.woff2 -------------------------------------------------------------------------------- /webui/src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwetzell/showbridge/0575ca5822af75b0f4a60d5973dbaa794a30b21f/webui/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /webui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Showbridge 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /webui/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | platformBrowserDynamic() 6 | .bootstrapModule(AppModule) 7 | .catch((err) => console.error(err)); 8 | -------------------------------------------------------------------------------- /webui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{html,ts}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /webui/tailwind.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @layer components { 4 | .context-menu-item { 5 | @apply p-2 py-0.5 text-left border border-gray-700 hover:bg-gray-500; 6 | } 7 | 8 | .context-menu { 9 | @apply flex flex-col bg-gray-600 border border-black border-solid; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webui/theme.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | 3 | @include mat.elevation-classes(); 4 | @include mat.app-background(); 5 | 6 | // NOTE(jwetzell): Color Palettes 7 | $showbridge-primary-palette: ( 8 | 50: #e4f2ff, 9 | 100: #bdddff, 10 | 200: #93c8ff, 11 | 300: #68b2ff, 12 | 400: #4ba1ff, 13 | 500: #3991ff, 14 | 600: #3b83f6, 15 | 700: #3a70e2, 16 | 800: #395ecf, 17 | 900: #353eaf, 18 | contrast: ( 19 | 50: rgba(black, 0.87), 20 | 100: rgba(black, 0.87), 21 | 200: rgba(black, 0.87), 22 | 300: rgba(black, 0.87), 23 | 400: rgba(black, 0.87), 24 | 500: white, 25 | 600: white, 26 | 700: white, 27 | 800: white, 28 | 900: white, 29 | ), 30 | ); 31 | 32 | $showbridge-warn-palette: ( 33 | 50: #fcedef, 34 | 100: #f9d3d4, 35 | 200: #e6a5a0, 36 | 300: #d9857c, 37 | 400: #e26e5c, 38 | 500: #e46546, 39 | 600: #d75d44, 40 | 700: #c5543e, 41 | 800: #b84e39, 42 | 900: #a9442e, 43 | contrast: ( 44 | 50: rgba(black, 0.87), 45 | 100: rgba(black, 0.87), 46 | 200: rgba(black, 0.87), 47 | 300: rgba(black, 0.87), 48 | 400: rgba(black, 0.87), 49 | 500: white, 50 | 600: white, 51 | 700: white, 52 | 800: white, 53 | 900: white, 54 | ), 55 | ); 56 | 57 | $showbridge-accent-palette: ( 58 | 50: #eceff1, 59 | 100: #cfd8dc, 60 | 200: #b0bec5, 61 | 300: #90a4ae, 62 | 400: #78909c, 63 | 500: #607d8b, 64 | 600: #546e7a, 65 | 700: #455a64, 66 | 800: #37474f, 67 | 900: #263238, 68 | contrast: ( 69 | 50: rgba(black, 0.87), 70 | 100: rgba(black, 0.87), 71 | 200: rgba(black, 0.87), 72 | 300: rgba(black, 0.87), 73 | 400: rgba(black, 0.87), 74 | 500: white, 75 | 600: white, 76 | 700: white, 77 | 800: white, 78 | 900: white, 79 | ), 80 | ); 81 | 82 | $my-primary: mat.m2-define-palette($showbridge-primary-palette, 600); 83 | $my-accent: mat.m2-define-palette($showbridge-accent-palette, 200); 84 | $my-warn: mat.m2-define-palette($showbridge-warn-palette, 400); 85 | 86 | $my-theme: mat.m2-define-dark-theme( 87 | ( 88 | color: ( 89 | primary: $my-primary, 90 | accent: $my-accent, 91 | warn: $my-warn, 92 | ), 93 | typography: mat.m2-define-typography-config(), 94 | density: 0, 95 | ) 96 | ); 97 | 98 | @include mat.all-component-themes($my-theme); 99 | -------------------------------------------------------------------------------- /webui/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "node", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022", 20 | "useDefineForClassFields": false, 21 | "lib": ["ES2022", "dom"] 22 | }, 23 | "angularCompilerOptions": { 24 | "enableI18nLegacyMessageIdFormat": false, 25 | "strictInjectionParameters": true, 26 | "strictInputAccessModifiers": true, 27 | "strictTemplates": true 28 | } 29 | } 30 | --------------------------------------------------------------------------------