Compare commits

..

285 Commits

Author SHA1 Message Date
sparks-alec d377df5803 styles for tiny viewports 2023-05-24 01:19:01 -04:00
sparks-alec 1eea4bf342 handle objects better 2023-05-24 01:18:36 -04:00
sparks-alec d3bd8cc88e confirm OCA device is d&b 2023-05-24 00:23:33 -04:00
sparks-alec 76e0a6a87a add basic hostname validation for bonjour devices 2023-05-24 00:22:00 -04:00
sparks-alec 50ccd0bb5a initial d&b commit 2023-05-23 23:37:25 -04:00
Joel Wetzell d5cec8799a fix for mac notarization 2023-05-10 16:30:05 -05:00
jwetzell 39d95bb87f Merge pull request #97 from stagehacks/dependabot/npm_and_yarn/electron-builder-24.4.0
Bump electron-builder from 24.2.0 to 24.4.0
2023-05-10 15:21:55 -05:00
dependabot[bot] 3e9b433056 Bump electron-builder from 24.2.0 to 24.4.0
Bumps [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) from 24.2.0 to 24.4.0.
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/v24.4.0/packages/electron-builder)

---
updated-dependencies:
- dependency-name: electron-builder
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-10 20:13:01 +00:00
jwetzell dbadb6d1ad Merge pull request #96 from stagehacks/dependabot/npm_and_yarn/eslint-8.40.0
Bump eslint from 8.38.0 to 8.40.0
2023-05-10 15:11:26 -05:00
jwetzell de7c2674bd Merge pull request #92 from stagehacks/dependabot/npm_and_yarn/prettier-2.8.8
Bump prettier from 2.8.7 to 2.8.8
2023-05-10 15:09:36 -05:00
dependabot[bot] 5f2967afae Bump eslint from 8.38.0 to 8.40.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.38.0 to 8.40.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.38.0...v8.40.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-08 10:03:22 +00:00
dependabot[bot] 711eaaf69e Bump prettier from 2.8.7 to 2.8.8
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.7 to 2.8.8.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.7...2.8.8)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-24 10:03:33 +00:00
jwetzell b6173695ce Merge pull request #88 from stagehacks/dependabot/npm_and_yarn/minimatch-and-electron-builder-3.1.2
Bump minimatch and electron-builder
2023-04-10 11:19:13 -05:00
dependabot[bot] a91c67a323 Bump minimatch and electron-builder
Bumps [minimatch](https://github.com/isaacs/minimatch) to 3.1.2 and updates ancestor dependency [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder). These dependencies need to be updated together.


Updates `minimatch` from 3.0.4 to 3.1.2
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

Updates `electron-builder` from 23.6.0 to 24.2.0
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/v24.2.0/packages/electron-builder)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
- dependency-name: electron-builder
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-10 16:10:51 +00:00
jwetzell 41384cb66c Merge pull request #87 from stagehacks/dependabot/npm_and_yarn/eslint-8.38.0
Bump eslint from 8.37.0 to 8.38.0
2023-04-10 11:10:05 -05:00
dependabot[bot] 7dc37a7fcb Bump eslint from 8.37.0 to 8.38.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.37.0 to 8.38.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.37.0...v8.38.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-10 10:03:19 +00:00
jwetzell 7769ad9059 Merge pull request #85 from stagehacks/dependabot/npm_and_yarn/eslint-8.37.0
Bump eslint from 8.36.0 to 8.37.0
2023-04-04 11:46:35 -05:00
jwetzell 159da195d3 Merge pull request #78 from stagehacks/plugin-atem
This has passed tests and has been cleaned up, send it!
2023-04-04 10:51:36 -05:00
dependabot[bot] a8d8092e65 Bump eslint from 8.36.0 to 8.37.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.36.0 to 8.37.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.36.0...v8.37.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 10:04:27 +00:00
jwetzell 88ad9207e2 Merge branch 'plugin-atem' of github.com:stagehacks/Cue-View into plugin-atem 2023-03-29 06:55:00 -05:00
sparks-alec 37a3fde2e0 remove old comments
remove old comments

remove comments
2023-03-29 05:38:33 -04:00
sparks-alec c2fcdfd6c9 add basic config help 2023-03-29 05:38:33 -04:00
Joel Wetzell bd2b190fac Merge branch 'main' into plugin-atem 2023-03-28 13:50:13 -05:00
Joel Wetzell 50d8363a4a regenerate package-lock using node v18.13.0 2023-03-28 13:41:04 -05:00
sparks-alec 68e7df1308 add button images 2023-03-28 10:46:39 -04:00
sparks-alec 209dc2d07d gray out disabled buttons, change element tree a bit 2023-03-28 10:46:18 -04:00
sparks-alec e742f129a1 update rate displays to use seconds:frames 2023-03-28 10:43:28 -04:00
sparks-alec 09f827a374 add images 2023-03-28 10:42:55 -04:00
sparks-alec 3e19954ae9 Update package-lock.json 2023-03-28 10:41:51 -04:00
jwetzell 90e06cab99 Merge pull request #79 from stagehacks/dependabot/npm_and_yarn/osc-2.4.4
Bump osc from 2.4.3 to 2.4.4
2023-03-28 07:18:53 -05:00
jwetzell 98a0c9c31b Merge pull request #81 from stagehacks/dependabot/npm_and_yarn/eslint-config-prettier-8.8.0
Bump eslint-config-prettier from 8.7.0 to 8.8.0
2023-03-28 07:18:41 -05:00
jwetzell 5c28a0c6b3 Merge pull request #83 from stagehacks/dependabot/npm_and_yarn/prettier-2.8.7
Bump prettier from 2.8.4 to 2.8.7
2023-03-28 07:18:31 -05:00
dependabot[bot] 095d7fb89f Bump prettier from 2.8.4 to 2.8.7
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.4 to 2.8.7.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.4...2.8.7)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 10:05:23 +00:00
dependabot[bot] f5e8e729e8 Bump eslint-config-prettier from 8.7.0 to 8.8.0
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.7.0 to 8.8.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.7.0...v8.8.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 10:04:15 +00:00
dependabot[bot] 3b24dcb063 Bump osc from 2.4.3 to 2.4.4
Bumps [osc](https://github.com/colinbdclark/osc.js) from 2.4.3 to 2.4.4.
- [Release notes](https://github.com/colinbdclark/osc.js/releases)
- [Commits](https://github.com/colinbdclark/osc.js/compare/2.4.3...2.4.4)

---
updated-dependencies:
- dependency-name: osc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-20 10:08:41 +00:00
jwetzell 9f8e0e260d Merge branch 'main' into plugin-atem 2023-03-17 09:47:26 -05:00
jwetzell b0790d50aa fix package-lock.json 2023-03-17 09:43:03 -05:00
jwetzell b314735604 Merge pull request #76 from stagehacks/dependabot/npm_and_yarn/eslint-8.36.0
Bump eslint from 8.35.0 to 8.36.0
2023-03-16 13:31:54 -05:00
dependabot[bot] f584e63940 Bump eslint from 8.35.0 to 8.36.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.35.0 to 8.36.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.35.0...v8.36.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 10:08:07 +00:00
jwetzell 84079906b5 Merge pull request #74 from stagehacks/dependabot/npm_and_yarn/http-cache-semantics-4.1.1
Bump http-cache-semantics from 4.1.0 to 4.1.1
2023-03-11 10:32:19 -06:00
dependabot[bot] 56a0ae1846 Bump http-cache-semantics from 4.1.0 to 4.1.1
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-11 16:22:52 +00:00
jwetzell ffe9920a32 Merge pull request #73 from stagehacks/dependabot/npm_and_yarn/eslint-config-prettier-8.7.0
Bump eslint-config-prettier from 8.6.0 to 8.7.0
2023-03-11 10:22:27 -06:00
dependabot[bot] cadcf89a3a Bump eslint-config-prettier from 8.6.0 to 8.7.0
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.6.0 to 8.7.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.6.0...v8.7.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-06 10:13:09 +00:00
jwetzell a4c2c4a3b2 Merge pull request #70 from stagehacks/dependabot/npm_and_yarn/eslint-8.35.0
Bump eslint from 8.34.0 to 8.35.0
2023-02-27 09:56:29 -06:00
dependabot[bot] ec45f6fbf5 Bump eslint from 8.34.0 to 8.35.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.34.0 to 8.35.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.34.0...v8.35.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-27 09:30:08 +00:00
jwetzell 6d335da4d7 add cache to release.yaml 2023-02-20 15:56:54 -06:00
jwetzell 00ce8b32bc flashing ftb, wire up missing button and styling 2023-02-18 11:54:41 -06:00
jwetzell 96b00def56 Merge pull request #65 from stagehacks/dependabot/npm_and_yarn/prettier-2.8.4
Bump prettier from 2.8.3 to 2.8.4
2023-02-13 05:57:50 -06:00
jwetzell c051c271c4 Merge pull request #66 from stagehacks/dependabot/npm_and_yarn/eslint-8.34.0
Bump eslint from 8.33.0 to 8.34.0
2023-02-13 05:57:23 -06:00
dependabot[bot] 6437099ff5 Bump eslint from 8.33.0 to 8.34.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.33.0 to 8.34.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.33.0...v8.34.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-13 10:15:24 +00:00
dependabot[bot] 6b6665a23c Bump prettier from 2.8.3 to 2.8.4
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.3 to 2.8.4.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.3...2.8.4)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-13 10:14:54 +00:00
jwetzell 5d551e9ed9 make buttons more buttony 2023-02-10 12:44:58 -06:00
jwetzell d2958966be make atem more responsive and more gradients 2023-02-09 19:40:07 -06:00
jwetzell 6e4133ae8f call atem update on interval 2023-02-08 17:03:15 -06:00
jwetzell 97a03b9dbb atem element size adjustments 2023-02-08 17:03:02 -06:00
jwetzell be657f2341 remove rogue semi-colon
Man this was driving me insane trying to find the source of this semi-colon
2023-02-08 16:46:54 -06:00
jwetzell c8d1b7b76b space out transition settings portion 2023-02-08 16:21:08 -06:00
Joel Wetzell 7709792fb5 update transition section layout with dsk 2023-02-08 16:15:45 -06:00
Joel Wetzell 5ea17755d1 update transition layout 2023-02-08 15:58:02 -06:00
jwetzell 56cc1b0cf8 transition and style wired up and prune inputs 2023-02-08 15:10:56 -06:00
Joel Wetzell f605125f74 populate next transition and transition style 2023-02-08 12:27:38 -06:00
jwetzell 1ef470e115 atem styling updates 2023-02-07 22:08:16 -06:00
jwetzell be3899a8f6 add search for sending udp direct to IPs for ATEM 2023-02-07 12:54:10 -06:00
jwetzell bd6757c99f convert to different atem library. Add FTB and DSK 2023-02-07 00:35:12 -06:00
jwetzell 5f10edfae8 Merge pull request #64 from stagehacks/dependabot/npm_and_yarn/electron-22.2.0
Bump electron from 22.1.0 to 22.2.0
2023-02-06 07:03:48 -06:00
dependabot[bot] eeab8eebde Bump electron from 22.1.0 to 22.2.0
Bumps [electron](https://github.com/electron/electron) from 22.1.0 to 22.2.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v22.1.0...v22.2.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-06 09:18:28 +00:00
jwetzell 042777a541 Merge pull request #62 from stagehacks/dependabot/npm_and_yarn/electron-22.1.0
Bump electron from 22.0.3 to 22.1.0
2023-01-30 08:20:12 -06:00
jwetzell 8edacd2511 Merge pull request #63 from stagehacks/dependabot/npm_and_yarn/eslint-8.33.0
Bump eslint from 8.32.0 to 8.33.0
2023-01-30 08:19:59 -06:00
dependabot[bot] be141280a4 Bump eslint from 8.32.0 to 8.33.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.32.0 to 8.33.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.32.0...v8.33.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-30 09:21:21 +00:00
dependabot[bot] a21dd719f7 Bump electron from 22.0.3 to 22.1.0
Bumps [electron](https://github.com/electron/electron) from 22.0.3 to 22.1.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v22.0.3...v22.1.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-30 09:21:05 +00:00
jwetzell 8d70d9d246 add atem transition bar 2023-01-26 02:02:33 -06:00
jwetzell 6f0e528164 basic atem connection/display working 2023-01-26 00:48:06 -06:00
jwetzell 65996e0e7a fix closing div tag 2023-01-26 00:18:59 -06:00
jwetzell 61e6d24e0f chain release jobs 2023-01-24 22:40:00 -06:00
jwetzell 6c2e1f204e Merge pull request #58 from stagehacks/dependabot/npm_and_yarn/eslint-plugin-import-2.27.5
Bump eslint-plugin-import from 2.27.4 to 2.27.5
2023-01-24 22:34:01 -06:00
dependabot[bot] dcc9caeb61 Bump eslint-plugin-import from 2.27.4 to 2.27.5
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.27.4 to 2.27.5.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.27.4...v2.27.5)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-25 04:33:26 +00:00
jwetzell e5635b05b3 Merge pull request #59 from stagehacks/dependabot/npm_and_yarn/eslint-8.32.0
Bump eslint from 8.31.0 to 8.32.0
2023-01-24 22:32:56 -06:00
jwetzell af82caddf9 Merge pull request #60 from stagehacks/dependabot/npm_and_yarn/prettier-2.8.3
Bump prettier from 2.8.2 to 2.8.3
2023-01-24 22:32:44 -06:00
jwetzell ee2d5ab533 Merge pull request #61 from stagehacks/dependabot/npm_and_yarn/electron-22.0.3
Bump electron from 22.0.2 to 22.0.3
2023-01-24 22:32:12 -06:00
dependabot[bot] 37594515e9 Bump electron from 22.0.2 to 22.0.3
Bumps [electron](https://github.com/electron/electron) from 22.0.2 to 22.0.3.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v22.0.2...v22.0.3)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-25 04:26:48 +00:00
dependabot[bot] 35c2326ec9 Bump prettier from 2.8.2 to 2.8.3
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.2 to 2.8.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.2...2.8.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-25 04:26:19 +00:00
dependabot[bot] 4b3c679842 Bump eslint from 8.31.0 to 8.32.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.31.0 to 8.32.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.31.0...v8.32.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-25 04:26:10 +00:00
jwetzell 51b6f47c81 Create dependabot.yml 2023-01-24 22:25:25 -06:00
jwetzell e7b0e557f6 Add issue templates 2023-01-23 14:48:28 -06:00
jwetzell aa4397ba56 cleanup templating 2023-01-22 13:01:35 -06:00
jwetzell 2c02b4751d default to port 3037 for eos third party osc 2023-01-18 22:03:43 -06:00
jwetzell 12d357081d fix eos updating when new cue lists added 2023-01-18 22:02:59 -06:00
jwetzell ebc17e599a adjust styling for eos closes #56 2023-01-18 16:17:56 -06:00
jwetzell cbde42a2e7 X32 cleanup 2023-01-18 15:42:01 -06:00
jwetzell cf9797c3af fix variable reference in template 2023-01-14 10:54:08 -06:00
jwetzell bbc2ae7159 fix mistake made when replacing indexOf 2023-01-14 10:48:36 -06:00
jwetzell 0275381255 bump Cue-View version 2023-01-13 16:26:04 -06:00
jwetzell 0aa25d8e70 add link to release page to update download prompt 2023-01-13 16:25:47 -06:00
jwetzell 53cd180b72 update electron 2023-01-13 15:42:24 -06:00
jwetzell 88c7401f64 update node version 2023-01-13 15:37:25 -06:00
sparks-alec 3c842d3bee Update main.js 2023-01-09 19:27:13 -05:00
sparks-alec f31ba8ae73 remove old debug 2023-01-09 19:23:28 -05:00
jwetzell 0d235b5c61 remove logging and commented code 2023-01-09 09:11:52 -06:00
jwetzell 5ccb3c0828 remove menu from network info window 2023-01-09 08:46:17 -06:00
sparks-alec 9b951452a5 fix netinfo window styles on windows 2023-01-09 01:18:03 -05:00
sparks-alec 6ccfd92db3 make device active when its view is clicked 2023-01-09 00:58:29 -05:00
sparks-alec 7d180c889c add per-address priority information for Net3 sACN 2023-01-09 00:57:50 -05:00
sparks-alec 1d689ef6c5 Revert "clicking on a device column makes it the active the device"
This reverts commit 205f407266.
2023-01-09 00:12:52 -05:00
sparks-alec 205f407266 clicking on a device column makes it the active the device 2023-01-08 23:51:55 -05:00
sparks-alec ee3e79bef0 remove alec's terrible old code 2023-01-08 23:24:04 -05:00
sparks-alec ca37a98a7e thump faster 2023-01-08 23:23:49 -05:00
sparks-alec f73cd9dbdb Update main.js 2023-01-08 23:12:24 -05:00
sparks-alec f9e4130837 Update main.js 2023-01-08 23:11:52 -05:00
sparks-alec ababf93e2b bugfix basically everything about heartbeats 2023-01-08 23:11:07 -05:00
sparks-alec 185b360d8a update config format
addresses #50
2023-01-08 22:32:46 -05:00
sparks-alec ddcc177bec update config format 2023-01-08 22:32:17 -05:00
sparks-alec d810f3ed40 make a newly manually added device the active device 2023-01-08 22:17:14 -05:00
sparks-alec 1d7ff53c9f remove lodash! 2023-01-08 22:13:26 -05:00
sparks-alec c8a23ee2b3 close TCP sockets before deleting device
prevents a situation where active TCP traffic would prevent a device from being visually deleted
2023-01-08 22:12:31 -05:00
sparks-alec 2393e8b459 bugfix make sure cue exists before modifying wait data 2023-01-08 22:11:44 -05:00
sparks-alec 9d3980cda9 Merge branch 'main' of https://github.com/stagehacks/Cue-View 2023-01-08 21:47:17 -05:00
sparks-alec e5cba677db bugfix #46 dupe device loading 2023-01-08 21:47:15 -05:00
jwetzell 341d093645 organize assets like images and stylesheets 2023-01-06 09:42:01 -06:00
jwetzell 2a4ab214b8 performance tweaks for sacn/artnet drawing 2023-01-05 15:26:40 -06:00
jwetzell 778173b118 replace indexOf with includes where possible 2023-01-05 08:51:31 -06:00
jwetzell a7256ba894 Change to manual workflow 2023-01-05 08:06:38 -06:00
sparks-alec a54298dc88 faster heartbeat for more frequent full draws
most of the device's UI isn't drawn until a full draw comes around, should do that more frequently
2023-01-05 02:47:11 -05:00
sparks-alec c3f64cd842 clearer feedback when too many hosts
Instead of preventing search when the subnet is too large, restrict the search area to part of the subnet and provide feedback about which hosts are to be searched.
2023-01-05 02:30:47 -05:00
sparks-alec 2d9d2fea07 menu item for Network Information window 2023-01-05 02:07:17 -05:00
sparks-alec 32c655fb43 prevent title text from overlapping the pin icon 2023-01-05 01:57:29 -05:00
sparks-alec 8e0fb1b474 correct system font 2023-01-05 01:29:41 -05:00
sparks-alec e91e5a1a43 add Network Info window 2023-01-05 01:29:31 -05:00
sparks-alec 369dcd5713 Merge branch 'main' of https://github.com/stagehacks/Cue-View 2023-01-05 01:06:15 -05:00
sparks-alec cdccffbb08 Merge pull request #44 from stagehacks/plugin-shure-wireless
Add Plugin: Shure Wireless
2023-01-05 01:05:53 -05:00
sparks-alec e978f86200 prettier 2023-01-04 22:57:42 -05:00
sparks-alec d59eb18de7 prettier 2023-01-04 22:56:32 -05:00
sparks-alec c5f79f5a50 add multicast on all NICs 2023-01-04 22:44:01 -05:00
sparks-alec e0eb2cfaaf Merge branch 'main' of https://github.com/stagehacks/Cue-View 2023-01-04 21:26:03 -05:00
sparks-alec da31f9b199 UDP use proper broadcast IPs
instead of generic 255.255.255.255 to search all interfaces, search all interfaces one by one using its appropriate broadcast IP
2023-01-04 21:25:59 -05:00
sparks-alec cd2fddea6e handle incorrect passwords better 2023-01-04 21:24:06 -05:00
jwetzell ce2d493216 move channel to class 2023-01-04 08:53:11 -06:00
jwetzell 5c02bc1fc4 package-lock.json shouldn't be ignored 2023-01-04 08:42:03 -06:00
sparks-alec fe57a2af52 check .send exists before running heartbeat 2023-01-04 05:15:46 -05:00
sparks-alec 77e0d59864 tcp search bugfix
when TCP device is discovered, wait to register it till socket closes. this way we don't have two sockets going at once!
2023-01-04 05:14:42 -05:00
sparks-alec 5a44a7d751 bunch o small changes 2023-01-04 05:06:03 -05:00
sparks-alec 7a4e2c96bb prettier width 2023-01-04 04:37:21 -05:00
sparks-alec 528f1cbef5 hardier time display 2023-01-04 04:36:47 -05:00
sparks-alec 2afb03d543 prettier width 2023-01-04 04:36:30 -05:00
sparks-alec e92a486088 bugfix variable names 2023-01-04 04:36:13 -05:00
sparks-alec 59257ff345 prettier wider 2023-01-04 04:35:36 -05:00
sparks-alec 845b7c3c9f make QLab more helpful when no data
when passcode is wrong or no workspaces are open
2023-01-04 01:36:05 -05:00
sparks-alec 190e771f4b add button style 2023-01-04 01:35:04 -05:00
sparks-alec de03d71893 prettier doesn't support ejs at all apparently 2023-01-04 01:34:47 -05:00
sparks-alec 354926c178 reformat new lint 2023-01-04 01:01:06 -05:00
sparks-alec 7eeec9bc50 add rf antenna A/B indicators 2023-01-04 00:52:34 -05:00
sparks-alec b5e93cb5ce smarter updates, fewer full draws 2023-01-04 00:52:17 -05:00
sparks-alec 9f9fc8cca2 add runtime for transmitters that support it 2023-01-03 23:51:49 -05:00
sparks-alec 1b6a3ce1b4 add runtime for transmitters that support it 2023-01-03 23:51:39 -05:00
sparks-alec af0e1eb8b8 make prettier 2023-01-03 23:51:23 -05:00
sparks-alec f8aad47858 accept templates inside HTML style attributes 2023-01-03 23:05:20 -05:00
jwetzell 0ed345b810 Merge pull request #43 from stagehacks/save-slot-cleanup
clean up slot/device saving. closes #40
2022-12-31 10:43:25 -06:00
jwetzell 6187af6f69 Merge pull request #42 from stagehacks/active-arrangement-indicator
Active arrangement indicator
2022-12-31 10:40:13 -06:00
jwetzell e166d09326 check qlab reply status before processing it 2022-12-31 10:37:42 -06:00
jwetzell a0e8789ca4 cleanup variable names 2022-12-30 15:17:28 -06:00
jwetzell ec2b477c9f add pretty script 2022-12-30 15:15:26 -06:00
jwetzell 0b459216ca update prettier settings 2022-12-30 15:15:09 -06:00
jwetzell 620e0f7c04 fix UDPSocket bind for new config layout 2022-12-30 15:02:48 -06:00
jwetzell f08f3b53df cleanup variable names 2022-12-30 15:02:32 -06:00
jwetzell b3ba6d2491 clean up slot/device saving 2022-12-30 14:33:17 -06:00
jwetzell a2cd653730 update artnet config layout 2022-12-29 16:30:02 -06:00
jwetzell 8728692d31 cleanup logging 2022-12-29 16:29:44 -06:00
jwetzell d0a8910f90 clean up save-slot click handling 2022-12-29 16:08:46 -06:00
jwetzell c4a7db2fb2 fix styling for active save slot 2022-12-29 16:08:15 -06:00
sparks-alec cb51bfaf72 improve clarity 2022-12-29 12:41:45 -05:00
sparks-alec b222617745 remove unavailable vars 2022-12-29 12:41:38 -05:00
sparks-alec a89d48106b delint 2022-12-29 12:18:10 -05:00
sparks-alec 075f6370d4 style tweaks 2022-12-29 12:17:56 -05:00
sparks-alec e48c982700 make Shure prettier 2022-12-29 10:48:55 -05:00
sparks-alec 83ca5aed73 add search by TCP port 2022-12-29 09:12:15 -05:00
sparks-alec 09f396c853 initial commit adding Shure Wireless 2022-12-29 08:01:09 -05:00
sparks-alec 93be649ce2 be smarter about changing the type of a device 2022-12-28 16:32:53 -05:00
sparks-alec c92960a87f add QLab 6 2022-12-17 02:19:01 -05:00
jwetzell 52e2cf7f4d Merge pull request #29 from stagehacks/lint-n-pretty
Lint n pretty
2022-12-15 13:14:16 -06:00
jwetzell 9784248865 Merge branch 'main' into lint-n-pretty 2022-12-15 13:14:04 -06:00
jwetzell cec2c14b3c remove broken badge 2022-12-15 13:11:39 -06:00
jwetzell 8e54d955e8 add vscode settings 2022-12-13 08:57:26 -06:00
jwetzell 8df019aee4 lint src files 2022-12-12 14:58:54 -06:00
jwetzell b754eece0d pretty loose files 2022-12-12 14:55:06 -06:00
jwetzell 29bdf40ffb pretty src directory 2022-12-12 14:54:24 -06:00
jwetzell c5cc0b4bea lint sacn plugin 2022-12-12 14:41:50 -06:00
jwetzell 958a37af0b lint qlab plugin 2022-12-12 14:41:37 -06:00
jwetzell 298e385867 lint pjlink plugin 2022-12-12 14:41:29 -06:00
jwetzell 6d2bdcae74 pretty xair plugin 2022-12-12 14:37:04 -06:00
jwetzell 7ecc379473 pretty x32 plugin 2022-12-12 14:36:54 -06:00
jwetzell 38b4274e85 pretty watchout plugin 2022-12-12 14:36:40 -06:00
jwetzell 3f5cefa4f4 pretty sacn plugin 2022-12-12 14:36:26 -06:00
jwetzell 2f0e2e387b pretty qlab plugin 2022-12-12 14:36:07 -06:00
jwetzell e84fa48a82 pretty pjlink plugin 2022-12-12 14:35:56 -06:00
jwetzell 1645480a08 pretty eos plugin 2022-12-12 14:35:24 -06:00
jwetzell ab32b67564 pretty artnet plugin 2022-12-12 14:32:31 -06:00
jwetzell b450d79d8e disable select on non-inputs closes #26 2022-12-12 11:49:27 -06:00
jwetzell 040b81dcbe add meyer processor as a future device 2022-12-11 20:32:37 -06:00
jwetzell ab09029d77 only show no update dialog when started from menu 2022-12-11 17:21:30 -06:00
sparks-alec 57c59da0b6 update QLab help text 2022-12-11 05:03:20 -05:00
sparks-alec 238b27bae6 bugfix number formatting 2022-12-11 04:58:09 -05:00
sparks-alec 72601b3c97 save field values with other save data 2022-12-11 04:45:12 -05:00
sparks-alec 419bf52ebb load default port on manually added devices 2022-12-11 04:30:02 -05:00
sparks-alec 6f690bb48c select a new device after deleting active 2022-12-11 04:25:21 -05:00
sparks-alec 401b599d33 load plugin info when device is created 2022-12-11 04:24:59 -05:00
sparks-alec 09ced85677 Update cue.ejs 2022-12-11 04:10:48 -05:00
sparks-alec 368f1700ee clarify when bad passcode 2022-12-11 04:08:07 -05:00
sparks-alec 97bd436785 bugfixfix #21 2022-12-11 03:55:41 -05:00
sparks-alec eb50088c06 Merge pull request #23 from stagehacks/new-plugin-format
New plugin format
2022-12-11 03:50:09 -05:00
sparks-alec 8d6a9614e8 bugfix enabling blank passcodes 2022-12-10 23:36:13 -05:00
sparks-alec fb9fc42b59 remove 169.x.x.x interfaces from search 2022-12-10 23:35:58 -05:00
sparks-alec 6f0a31b3bc increase search timeout 2022-12-10 23:35:30 -05:00
sparks-alec 936a42ae31 bugfix with loading plugin data on non-osc devices 2022-12-10 23:34:47 -05:00
sparks-alec a1877873b2 make default passcode blank 2022-12-10 23:34:16 -05:00
sparks-alec a5fa08ccfa support PJLink passwords 2022-12-10 23:34:01 -05:00
jwetzell 997c4f8ee5 Merge pull request #22 from stagehacks/macos-signing
macOS Signing
2022-12-10 14:22:47 -06:00
jwetzell 1963e40efb cleanup notarize script 2022-12-10 14:19:00 -06:00
jwetzell f18539c703 remove macOS auto update warning 2022-12-10 14:16:14 -06:00
jwetzell 934dd89b21 add script for notarizing macOS 2022-12-10 14:15:58 -06:00
jwetzell 4068199a03 Add env variables for signing and notarizing MacOS 2022-12-10 14:13:47 -06:00
jwetzell 13bcee1181 add environment variable for code signing 2022-12-10 13:32:42 -06:00
jwetzell 7089b1d3fa fix typo in eos templat. closes #20 2022-12-10 11:12:39 -06:00
sparks-alec 3daf347062 Update README.md 2022-12-10 06:58:46 -05:00
sparks-alec 5e21544a81 new universal plugin configuration object 2022-12-10 06:53:56 -05:00
sparks-alec 93225fc00e bigger inspector corner, nice mouseover cursors 2022-12-10 06:53:17 -05:00
sparks-alec 00bffef062 make inspector corner bigger 2022-12-10 06:51:50 -05:00
sparks-alec d88f0e2350 Merge pull request #19 from stagehacks/qlab-qlab5
QLab 5 Support
2022-12-10 04:18:36 -05:00
sparks-alec e781b783f3 fix error thrown by empty/unreachable workspaces 2022-12-10 04:15:23 -05:00
jwetzell 487123538a update build status badge 2022-12-09 14:13:40 -06:00
jwetzell 2df89da36d add build status badge to README 2022-12-09 14:09:07 -06:00
jwetzell 421fe8e3f3 don't run action if just readme is updated 2022-12-09 14:07:09 -06:00
jwetzell 1690661868 add name to workflow 2022-12-09 13:57:21 -06:00
jwetzell ba2b7aadd8 Merge pull request #11 from stagehacks/github-actions
Everything checks out with Alec. Merging
2022-11-30 14:15:38 -06:00
jwetzell 4b00c4f807 change github action name and branch 2022-11-30 08:02:43 -06:00
sparks-alec 54d04a7c44 bugfix for cues with prewaits
on small viewport sizes, cues with prewaits would be missing a column compared to their non-waited pals
2022-11-30 08:39:31 -05:00
sparks-alec c38f5c34a3 v5 support 2022-11-30 08:31:55 -05:00
sparks-alec 32ce970b05 display fancy mode-specific group cue icons 2022-11-30 08:31:33 -05:00
sparks-alec ffe6f00a83 v5 icons 2022-11-30 08:30:53 -05:00
sparks-alec 4e0d531658 add new styles for Playlist group cues 2022-11-30 07:51:51 -05:00
sparks-alec 413374f633 Merge pull request #17 from stagehacks/qlab-groupfix
Qlab groupfix
2022-11-30 03:33:48 -05:00
sparks-alec 6a01863ea6 new group cue icon 2022-11-30 03:14:18 -05:00
sparks-alec 26ed323bb4 fix group cue border drawing 2022-11-30 02:52:57 -05:00
jwetzell 5d69d05b85 Merge pull request #16 from stagehacks/eos-cleanup
Minor eos cleanup
2022-11-28 20:41:29 -06:00
jwetzell ee26671997 minor eos cleanup 2022-11-28 20:39:56 -06:00
jwetzell e69ea6ca6d Merge pull request #15 from stagehacks/pjlink-cleanup
Cleanup pjlink variables and logic
2022-11-28 20:04:33 -06:00
jwetzell f0d36ce2dd Merge pull request #14 from stagehacks/watchout-cleanup
Watchout cleanup
2022-11-28 20:03:10 -06:00
jwetzell 3c19746b7a Merge pull request #13 from stagehacks/sacn-cleanup
cleanup sacn variables/logic
2022-11-28 20:01:43 -06:00
jwetzell e9adf29897 cleanup watchout template 2022-11-28 20:00:44 -06:00
jwetzell 65844d25fe cleanup pjlink variables and logic 2022-11-28 19:59:55 -06:00
jwetzell e2ac66b564 cleanup watchout variable names 2022-11-28 19:42:00 -06:00
jwetzell 4f302bcdf5 cleanup sacn variables/logic 2022-11-28 19:36:58 -06:00
jwetzell 54046cc53a Merge pull request #12 from stagehacks/artnet-cleanup
artnet cleanup
2022-11-27 18:45:47 -06:00
jwetzell 95dc21a55c artnet cleanup 2022-11-27 18:44:41 -06:00
jwetzell c74e208144 cleanup logging 2022-11-27 14:04:14 -06:00
jwetzell 1aaf5857b6 create menu item for toggling auto update checks 2022-11-27 14:03:43 -06:00
jwetzell 63c501e71f change download prompt wording 2022-11-26 19:50:35 -06:00
jwetzell 9813ea0986 bump version to test autoUpdater 2022-11-26 19:39:34 -06:00
jwetzell f524d23dc7 auto update menu item and prompts 2022-11-26 19:33:05 -06:00
jwetzell daa7b7f177 update release to test autoupdate 2022-11-26 17:49:34 -06:00
jwetzell 940b366b29 add auto update 2022-11-26 17:37:20 -06:00
jwetzell 5c4938458d update appId 2022-11-26 17:37:01 -06:00
jwetzell 6732fc5495 update electron-builder config 2022-11-26 15:45:38 -06:00
jwetzell 0852479e08 add macos job 2022-11-26 15:04:59 -06:00
jwetzell 447f4d3b1f change branch to monitor to main 2022-11-26 14:52:47 -06:00
jwetzell fd3ae81e84 try adding windows 2022-11-26 14:17:17 -06:00
jwetzell af86f5cb82 try setting gh_token environment variable 2022-11-26 14:13:52 -06:00
jwetzell 8cc4c5cc53 change branch for testing purpose 2022-11-26 14:06:05 -06:00
jwetzell fc0af55d4e start work on github action 2022-11-26 14:05:44 -06:00
jwetzell 51ef8e01ba add nvmrc for node version 2022-11-26 14:04:51 -06:00
jwetzell eabc05bef3 Merge pull request #8 from stagehacks/qlab-cleanup
Qlab cleanup
2022-11-19 10:34:49 -06:00
jwetzell e4ba6f3548 move global variables to start of file 2022-11-19 10:25:52 -06:00
jwetzell 0f31a79105 cleanup qlab template 2022-11-19 10:25:34 -06:00
jwetzell bd08b718f1 start on QLab cleanup 2022-11-18 09:37:48 -06:00
jwetzell d3f3d96e6b update dependencies 2022-11-17 19:10:28 -06:00
jwetzell 457bca2396 add electron-builder as dev dependency 2022-11-17 19:05:48 -06:00
jwetzell d79ee8dea0 add npm scripts for individual build 2022-11-17 19:03:30 -06:00
jwetzell 1959169fb9 Merge pull request #7 from jwetzell/eos-cleanup
Eos cleanup
2022-11-17 18:37:46 -06:00
Joel Wetzell 0c04207a16 cleanup variable names 2022-10-25 15:17:10 -05:00
jwetzell a32cc32194 eos cleanup 2022-05-28 10:13:07 -05:00
jwetzell b27e836227 Update packages 2022-05-28 10:12:59 -05:00
sparks-alec 6cbb1c1f76 bad version num 2022-05-07 01:28:14 -04:00
sparks-alec a4c71e9313 version bump 2022-05-07 01:27:46 -04:00
sparks-alec d40f2ac7ee correct a ton of path syntax 2022-05-07 01:27:37 -04:00
sparks-alec 2b7db257ed Merge pull request #4 from jwetzell/x32-use-osc
convert x32 to use actual osc
2022-05-06 23:28:53 -04:00
jwetzell 5ae4f71a78 convert x32 to use actual osc 2022-03-01 23:27:54 -06:00
sparks-alec 85af9a796c add Art-Net and sACN to list of supported devices 2022-02-27 19:48:01 -05:00
102 changed files with 10609 additions and 3952 deletions
+1 -1
View File
@@ -20,6 +20,6 @@ module.exports = {
'no-use-before-define': 'off',
'space-infix-ops': 'warn',
'no-bitwise': 'off',
'no-restricted-globals': 'off'
'no-restricted-globals': 'off',
},
};
+31
View File
@@ -0,0 +1,31 @@
---
name: Bug report
about: Let us know what isn't working
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows 10]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
+45
View File
@@ -0,0 +1,45 @@
name: release
on:
workflow_dispatch:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run release
build-windows:
needs: build-linux
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run release
build-macos:
needs: build-windows
runs-on: macos-latest
env:
CSC_LINK: ${{ secrets.MACOS_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MACOS_CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run release
-4
View File
@@ -1,7 +1,3 @@
.DS_Store
node_modules
.vscode
/dist
package-lock.json
package-lock.json
+1
View File
@@ -0,0 +1 @@
v18.13.0
+2
View File
@@ -1,2 +1,4 @@
node_modules
out
dist
*.ejs
+1
View File
@@ -4,4 +4,5 @@ module.exports = {
semi: true,
singleQuote: true,
bracketSameLine: true,
printWidth: 120,
};
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}
+9
View File
@@ -0,0 +1,9 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": ["javascript"],
"html.validate.styles": false // accept templates inside HTML style attributes
}
+32 -30
View File
@@ -13,55 +13,57 @@ A dashboard for everything in your show.
## Supported Devices
- QLab 4
- QLab 4 & 5
- ETC Eos Consoles
- Watchout
- PJLink Projectors
- X32 Audio Consoles
- XAir Audio Consoles
- Art-Net Universes
- sACN Universes
#### Future Devices
- Spikemark (not possible until additional OSC methods are added)
- ATEM Video Mixers
- Art-Net, sACN Universes
- Epson Pro series Projectors
- DiGiCo Consoles
- d&b DS100, amps
- Hog
- Meyer Galileo Processors
# Plugins
A Cue View "plugin" is a system for communicating with a type of device, for example QLab or Watchout. It consists of a JS file that describes how to communicate with the device, an HTML template for displaying the device's data, and a CSS file to style the HTML.
### plugin.js
```js
exports.defaultName = 'Example Plugin';
exports.connectionType = 'osc' or 'TCPsocket' or 'UDPsocket'
exports.searchOptions = {
type: 'Bonjour',
bonjourNane: 'device'
exports.config = {
defaultName: 'Example Plugin',
connectionType: 'osc' or 'TCPsocket' or 'UDPsocket',
searchOptions: {
type: 'Bonjour',
bonjourNane: 'device'
},
searchOptions: {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x00, 0x01, 0x02]),
devicePort: 1234, // port the device receives messages on
listenPort: 2345, // port Cue View should listen for responses on
validateResponse: function(msg, info){
// if this function returns true, Cue View adds the responding IP address to the list
}
},
searchOptions: {
type: 'TCPport',
searchBuffer: Buffer.from('are you there'),
testPort: 1234, // port the device receives messages on
validateResponse: function(msg, info){
// if this function returns true, Cue View adds the responding IP address to the list
}
},
defaultPort: 1234, // only available for TCPsocket and UDPsocket devices
heartbeatInterval: 5000 // how frequently, in ms, to send the heartbeat message
}
exports.searchOptions = {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x00, 0x01, 0x02]),
devicePort: 1234, // port the device receives messages on
listenPort: 2345, // port Cue View should listen for responses on
validateResponse: function(msg, info){
// if this function returns true, Cue View adds the responding IP address to the list
}
}
exports.searchOptions = {
type: 'TCPport',
searchBuffer: Buffer.from('are you there'),
testPort: 1234, // port the device receives messages on
validateResponse: function(msg, info){
// if this function returns true, Cue View adds the responding IP address to the list
}
}
exports.defaultPort = 1234; // only available for TCPsocket and UDPsocket devices
exports.heartbeatInterval = 5000; // how frequently, in ms, to send the heartbeat message
exports.ready = function (device){
// runs when Cue View identifies a new device. send all data requests here
+50 -75
View File
@@ -3,93 +3,68 @@
<head>
<meta charset="UTF-8" />
<title>Cue View</title>
<link rel="stylesheet" href="src/index.css" />
<link rel="stylesheet" href="src/assets/css/index.css" />
</head>
<body>
<div id="main">
<div class="col" id="device-list-col">
<div id="view-buttons-bar">
<button
id="save-slot-1"
class="save-slot"
title="Window arrangement 1">
1
</button>
<button
id="save-slot-2"
class="save-slot"
title="Window arrangement 2">
2
</button>
<button
id="save-slot-3"
class="save-slot"
title="Window arrangement 3">
3
</button>
<button id="save-slot-1" class="save-slot" title="Window arrangement 1">1</button>
<button id="save-slot-2" class="save-slot" title="Window arrangement 2">2</button>
<button id="save-slot-3" class="save-slot" title="Window arrangement 3">3</button>
</div>
<div id="device-list">
<h3 class="init">
Click the Search button to find devices on the network.
</h3>
<h3 class="init">Click the Search button to find devices on the network.</h3>
</div>
<div id="device-tools">
<select
class="left button"
id="add-device-button"
title="Add Device..."></select>
<button
class="left"
id="search-button"
title="Search network for devices">
<img src="src/img/outline_search_white_18dp.png" />
</button>
<button
class="left"
id="refresh-device-button"
title="Refresh Device"
disabled>
<img src="src/img/outline_refresh_white_18dp.png" />
</button>
</div>
<div id="device-inspector">
<div id="device-tools">
<select class="left button" id="add-device-button" title="Add Device..."></select>
<button class="left" id="search-button" title="Search network for devices">
<img src="src/assets/img/outline_search_white_18dp.png" />
</button>
<button class="left" id="refresh-device-button" title="Refresh Device" disabled>
<img src="src/assets/img/outline_refresh_white_18dp.png" />
</button>
<button class="right" id="network-info-button">
<img src="src/assets/img/outline_info_white_18dp.png" />
</button>
</div>
<div id="device-settings">
<table id="device-settings-table">
<tr>
<th>Name:</th>
<td colspan="3">
<input id="device-settings-name" type="text" maxlength="30" />
</td>
</tr>
<tr>
<th>Type:</th>
<td colspan="3">
<select id="device-settings-plugin-dropdown"></select>
</td>
</tr>
<tr>
<th>Addr:</th>
<td colspan="3">
<input id="device-settings-ip" type="text" maxlength="30" />
</td>
</tr>
<tr>
<th>Port:</th>
<td>
<input
id="device-settings-port"
type="number"
value="53000"
maxlength="5" />
</td>
<th>Pin:</th>
<td><input id="device-settings-pin" type="checkbox" /></td>
</tr>
</table>
<div id="device-settings">
<table id="device-settings-table">
<tr>
<th>Name:</th>
<td colspan="3">
<input id="device-settings-name" type="text" maxlength="30" />
</td>
</tr>
<tr>
<th>Type:</th>
<td colspan="3">
<select id="device-settings-plugin-dropdown"></select>
</td>
</tr>
<tr>
<th>Addr:</th>
<td colspan="3">
<input id="device-settings-ip" type="text" maxlength="30" />
</td>
</tr>
<tr>
<th>Port:</th>
<td>
<input id="device-settings-port" type="number" value="53000" maxlength="5" />
</td>
<th>Pin:</th>
<td><input id="device-settings-pin" type="checkbox" /></td>
</tr>
<tbody id="device-settings-fields"></tbody>
</table>
<h3>No Device Selected</h3>
<h3>No Device Selected</h3>
</div>
</div>
</div>
+279 -58
View File
@@ -1,9 +1,17 @@
const { app, BrowserWindow, Menu, ipcMain, nativeTheme } = require('electron');
const { app, BrowserWindow, Menu, ipcMain, nativeTheme, dialog, shell } = require('electron');
const { autoUpdater } = require('electron-updater');
const path = require('path');
const packageInfo = require('./package.json');
const isMac = process.platform === 'darwin';
const isWin = process.platform === 'win32';
const isLinux = process.platform === 'linux';
let autoUpdate = false;
let manualUpdateCheck = false;
let menuObj;
let mainWindow;
let networkInfoWindow;
const menuTemplate = [
...(isMac ? [{ role: 'appMenu' }] : []),
@@ -12,18 +20,18 @@ const menuTemplate = [
submenu: [
{
label: 'Clear Saved Data',
id: 'resetViews',
click (menuItem, window, event) {
mainWindow.webContents.send('resetViews');
}
id: 'clearSavedData',
click(menuItem, window, event) {
mainWindow.webContents.send('clearSavedData');
},
},
{
label: 'Reload App',
role: 'reload'
role: 'reload',
},
{ type: 'separator' },
...(isMac ? [{ role: 'close' }] : [{ role: 'quit' }])
]
...(isMac ? [{ role: 'close' }] : [{ role: 'quit' }]),
],
},
{ role: 'editMenu' },
{
@@ -36,31 +44,41 @@ const menuTemplate = [
accelerator: 'CommandOrControl+1',
id: 'window1',
enabled: true,
click (menuItem, window, event) {
mainWindow.webContents.send('doSlots1');
}
click(menuItem, window, event) {
mainWindow.webContents.send('loadSlot', 1);
},
},
{
label: 'Arrangement 2',
accelerator: 'CommandOrControl+2',
id: 'window2',
enabled: true,
click (menuItem, window, event) {
mainWindow.webContents.send('doSlots2');
}
click(menuItem, window, event) {
mainWindow.webContents.send('loadSlot', 2);
},
},
{
label: 'Arrangement 3',
accelerator: 'CommandOrControl+3',
id: 'window3',
enabled: true,
click (menuItem, window, event) {
mainWindow.webContents.send('doSlots3');
}
click(menuItem, window, event) {
mainWindow.webContents.send('loadSlot', 3);
},
},
{ type: 'separator' },
{ role: 'toggleDevTools' }
]
{
label: 'Network Information...',
accelerator: 'CommandOrControl+I',
id: 'window4',
enabled: true,
click(menuItem, window, event) {
openNetworkInfoWindow();
},
},
{ type: 'separator' },
{ role: 'toggleDevTools' },
],
},
{
label: 'Device',
@@ -70,9 +88,9 @@ const menuTemplate = [
accelerator: 'CommandOrControl+F',
id: 'deviceSearch',
enabled: true,
click (menuItem, window, event) {
mainWindow.webContents.send('doSearch');
}
click(menuItem, window, event) {
mainWindow.webContents.send('searchAll');
},
},
{ type: 'separator' },
{
@@ -82,64 +100,118 @@ const menuTemplate = [
accelerator: 'CommandOrControl+P',
id: 'devicePin',
enabled: false,
click (menuItem, window, event) {
mainWindow.webContents.send(
'setActiveDevicePinned',
menuItem.checked
);
}
click(menuItem, window, event) {
mainWindow.webContents.send('setActiveDevicePinned', menuItem.checked);
},
},
{
label: 'Delete',
accelerator: 'CommandOrControl+Backspace',
id: 'deviceDelete',
enabled: false,
click (menuItem, window, event) {
mainWindow.webContents.send('doDelete');
}
}
]
}
click(menuItem, window, event) {
mainWindow.webContents.send('deleteActive');
},
},
],
},
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'About',
role: 'about',
},
{
label: 'Check for Updates',
click: () => {
// set manual update flag
manualUpdateCheck = true;
autoUpdater.checkForUpdates();
},
},
{
label: 'Enable Auto Update',
click: () => {
mainWindow.webContents.send('setAutoUpdate', !autoUpdate);
},
},
],
},
];
const windowMac = {
width: 1500,
height: 900,
titleBarStyle: 'hiddenInset',
transparent: true,
frame: false,
show: false,
// backgroundColor: "#333333",
vibrancy: 'window',
visualEffectState: 'followWindow',
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js'),
},
}
const windowWin = {
width: 1500,
height: 900,
backgroundColor: "#333333",
titleBarStyle: 'hiddenInset',
transparent: true,
frame: false,
show: false,
vibrancy: 'window',
visualEffectState: 'followWindow',
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js'),
},
};
const windowWin = {
width: 1500,
height: 900,
backgroundColor: '#333333',
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js'),
},
};
const networkInfoMac = {
width: 700,
height: 350,
// transparent: true,
frame: true,
show: false,
vibrancy: 'window',
visualEffectState: 'followWindow',
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload: path.join(__dirname, 'networkInterfaces.js'),
},
};
const networkInfoWin = {
width: 700,
height: 350,
backgroundColor: '#333333',
show: false,
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload: path.join(__dirname, 'networkInterfaces.js'),
},
};
if (isWin) {
app.setAppUserModelId(app.name);
}
const createWindow = () => {
nativeTheme.themeSource = 'dark';
if(isMac){
if (isMac) {
mainWindow = new BrowserWindow(windowMac);
}else{
} else {
mainWindow = new BrowserWindow(windowWin);
}
if (isLinux) {
mainWindow.setIcon(path.join(__dirname, 'src', 'assets', 'img', 'icon.png'));
}
mainWindow.loadFile('index.html');
mainWindow.on('ready-to-show', () => {
@@ -152,12 +224,11 @@ const createWindow = () => {
menuObj = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menuObj);
};
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
@@ -188,3 +259,153 @@ ipcMain.on('disableSearchAll', (event, arg) => {
ipcMain.on('setDevicePin', (event, arg) => {
menuObj.getMenuItemById('devicePin').checked = arg;
});
ipcMain.on('openNetworkInfoWindow', (event, arg) => {
openNetworkInfoWindow();
});
function openNetworkInfoWindow() {
if (!networkInfoWindow || (networkInfoWindow && networkInfoWindow.isDestroyed())) {
if (isMac) {
networkInfoWindow = new BrowserWindow(networkInfoMac);
} else {
networkInfoWindow = new BrowserWindow(networkInfoWin);
networkInfoWindow.removeMenu();
}
networkInfoWindow.loadFile('networkInterfaces.html');
}
networkInfoWindow.show();
}
// ONLY Autoupdate logic below
ipcMain.on('checkForUpdates', (event, arg) => {
autoUpdater.checkForUpdates();
});
ipcMain.on('setAutoUpdate', (event, _autoUpdate) => {
autoUpdate = _autoUpdate;
// update menu item for enabling/disabling auto update
menuTemplate[menuTemplate.length - 1].submenu[2].label = autoUpdate ? 'Disable Auto Update' : 'Enable Auto Update';
menuObj = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menuObj);
});
// this can be set to true to bypass the download update dialog and skip straight to install prompt
autoUpdater.autoDownload = false;
autoUpdater.on('update-available', (updateInfo) => {
// skip prompting to download if autoDownload is set
if (autoUpdater.autoDownload) {
return;
}
const msg = `Version v${updateInfo.version} is available. Would you like to download it?`;
const title = 'Update Available';
let dialogOpts = {};
if (isMac) {
dialogOpts = {
type: 'info',
buttons: ['Download', 'Cancel', 'View Release Notes'],
title: 'Update Available',
message: title,
detail: msg,
};
} else {
dialogOpts = {
type: 'info',
buttons: ['Download', 'Cancel', 'View Release Notes'],
title: 'Update Available',
message: msg,
};
}
dialog.showMessageBox(mainWindow, dialogOpts).then((returnValue) => {
if (returnValue.response === 0) {
// download was clicked
autoUpdater.downloadUpdate();
} else if (returnValue.response === 2) {
// view release notes clicked
shell.openExternal(`${packageInfo.repository}/releases/tag/v${updateInfo.version}`);
}
});
});
autoUpdater.on('update-downloaded', (event) => {
const title = 'Update Downloaded';
const msg = `Version v${event.version} has been downloaded. Would you like to install this update now?`;
let dialogOpts = {};
if (isMac) {
dialogOpts = {
type: 'info',
buttons: ['Install', 'Later'],
title: 'Update Downloaded',
message: title,
detail: msg,
};
} else {
dialogOpts = {
type: 'info',
buttons: ['Install', 'Later'],
title: 'Update Downloaded',
message: msg,
};
}
dialog.showMessageBox(mainWindow, dialogOpts).then((returnValue) => {
if (returnValue.response === 0) autoUpdater.quitAndInstall();
});
});
autoUpdater.on('update-not-available', (updateInfo) => {
if (manualUpdateCheck) {
let dialogOpts = {};
const msg = `There is no update available at this time. Latest version is v${updateInfo.version}`;
if (isMac) {
dialogOpts = {
type: 'info',
buttons: ['Ok'],
title: 'No Update Available',
message: 'No Update Available',
detail: msg,
};
} else {
dialogOpts = {
type: 'info',
buttons: ['Ok'],
title: 'No Update Available',
message: msg,
};
}
dialog.showMessageBox(mainWindow, dialogOpts);
// revert manual update flag
manualUpdateCheck = false;
}
});
autoUpdater.on('error', (error, message) => {
let dialogOpts = {};
if (isMac) {
dialogOpts = {
type: 'error',
buttons: ['Ok'],
title: 'Update Error',
message: 'Update Error',
detail: error.message,
};
} else {
dialogOpts = {
type: 'error',
buttons: ['Ok'],
title: 'Update Error',
message: error.message,
};
}
dialog.showMessageBox(mainWindow, dialogOpts);
});
+92
View File
@@ -0,0 +1,92 @@
<html>
<head>
<title>Network Information</title>
<style type="text/css">
body {
margin: 0px;
color: white;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont;
user-select: none;
background-color: transparent;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 12px;
padding-left: 10px;
}
th {
text-align: left;
background-color: rgba(100, 100, 100, 0.3);
border-top: #868686 1px solid;
border-bottom: #444 1px solid;
border-right: #868686 1px solid;
font-weight: normal;
height: 36px;
}
td {
border-bottom: dimgray 1px solid;
}
p {
color: #a0a0a0;
margin: 17px 13px;
font-size: 14px;
}
button {
box-sizing: content-box;
height: 25px;
border: none;
outline: none;
padding: 1px 10px;
position: absolute;
right: 10px;
top: 10px;
color: white;
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
font-size: 14px;
}
button:hover {
background-color: rgba(255, 255, 255, 0.2);
}
button:focus {
outline: none;
}
span {
border-radius: 4px;
padding: 2px 4px;
}
.if-en {
background-color: #1565c0;
}
.if-ut {
background-color: #bf360c;
}
.red {
color: #ca6f1e;
}
.green {
color: #28b463;
}
</style>
</head>
</html>
<p>Attached Network Interfaces:</p>
<button onclick="location.reload()">Refresh</button>
<table>
<tr>
<th>ID</th>
<th>IP Address</th>
<th>Subnet Mask</th>
<th>Searchable Address Ranges</th>
</tr>
<tbody id="network-interfaces"></tbody>
</table>
<script>
window.init();
</script>
+31
View File
@@ -0,0 +1,31 @@
const SEARCH = require('./src/search.js');
window.init = function init() {
const networkInterfaces = SEARCH.getNetworkInterfaces();
let html = '';
for (let i = 0; i < Object.keys(networkInterfaces).length; i++) {
const interfaceID = Object.keys(networkInterfaces)[i];
const interfaceObj = networkInterfaces[interfaceID];
html += `
<tr>
<td><span class="if-${interfaceID.substring(0, 2)}">${interfaceID}</span></td>
<td>${interfaceObj[0].address}</td>
<td>${interfaceObj[0].netmask}</td>
`;
if (interfaceObj[0].searchTruncated) {
html += `<td class='red'>`;
} else {
html += `<td class='green'>`;
}
html += `
${interfaceObj[0].firstSearchAddress} - ${interfaceObj[0].lastSearchAddress}</td>
</tr>
`;
document.getElementById('network-interfaces').innerHTML = html;
}
};
+44
View File
@@ -0,0 +1,44 @@
const notarize = require('@electron/notarize');
const fs = require('fs');
const path = require('path');
module.exports = async function (params) {
if (process.platform !== 'darwin') {
console.log('Only need to notarize MacOS, skipping');
return;
}
const appId = 'com.stagehacks.cueview';
const appPath = path.join(
params.appOutDir,
`${params.packager.appInfo.productFilename}.app`
);
if (!fs.existsSync(appPath)) {
console.error('App file not found skipping notarization.');
return;
}
if (
process.env.APPLE_ID === undefined ||
process.env.APPLE_ID_PASSWORD === undefined
) {
console.log('Apple ID and Password must be set in order to notarize.');
return;
}
try {
console.log(`Notarizing ${appId} found at ${appPath}`);
await notarize.notarize({
appBundleId: appId,
appPath,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
});
console.log(`Done notarizing ${appId}`);
} catch (error) {
console.log('There was an error notarizing.');
console.error(error);
}
};
+6588 -2229
View File
File diff suppressed because it is too large Load Diff
+43 -19
View File
@@ -1,52 +1,76 @@
{
"name": "cue-view",
"productName": "Cue View",
"version": "0.9.5-pre",
"version": "0.9.9-pre",
"description": "A dashboard for everything in your show",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"pretty": "prettier --write **/*",
"start": "electron .",
"build": "electron-builder -m -w -l",
"buildARM": "electron-builder -l --arm64"
"build:mac": "electron-builder -m",
"build:windows": "electron-builder -w",
"build:linux": "electron-builder -l",
"build:arm": "electron-builder -l --arm64",
"release": "electron-builder"
},
"author": "alec@stagehacks.com",
"license": "CC BY-SA 4.0",
"homepage": "https://github.com/stagehacks/Cue-View",
"repository": "https://github.com/stagehacks/Cue-View",
"devDependencies": {
"electron": "^15.2.0",
"eslint": "^8.9.0",
"@electron/notarize": "^1.2.3",
"electron": "^22.2.0",
"electron-builder": "^24.4.0",
"eslint": "^8.40.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.4",
"prettier": "^2.5.1"
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"prettier": "^2.8.8"
},
"dependencies": {
"atem-connection": "^3.1.3",
"bonjour": "^3.5.0",
"lodash": "^4.17.20",
"electron-updater": "^5.3.0",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"netmask": "^2.0.2",
"osc": "^2.4.1",
"uuid": "^8.3.2"
"osc": "^2.4.4",
"uuid": "^9.0.0"
},
"build": {
"appId": "com.electron.cueview",
"icon": "./src/img/",
"artifactName": "${productName} ${os} v${version}.${ext}",
"appId": "com.stagehacks.cueview",
"icon": "src/assets/img/",
"artifactName": "${name}.${os}.v${version}.${ext}",
"mac": {
"target": "zip",
"category": "Utilities",
"icon": "./src/img/icon.icns"
"icon": "src/assets/img/icon.icns",
"hardenedRuntime": true,
"electronLanguages": [
"en"
],
"publish": [
"github"
]
},
"win": {
"target": "portable",
"icon": "./src/img/icon.ico"
"target": "NSIS",
"icon": "src/assets/img/icon.ico",
"publish": [
"github"
]
},
"nsis": {
"oneClick": false
},
"linux": {
"target": "deb",
"target": "AppImage",
"maintainer": "alec@stagehacks.com",
"category": "Utility"
"category": "Utility",
"publish": [
"github"
]
}
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
<p><em>Art-Net</em> requires no configuration.</p>
<p>Art-Net™ Designed by and Copyright Artistic Licence Holdings Ltd</p>
<p>Art-Net™ Designed by and Copyright Artistic Licence Holdings Ltd</p>
+68 -92
View File
@@ -1,119 +1,95 @@
const _ = require('lodash');
exports.defaultName = "Art-Net";
exports.connectionType = 'UDPsocket';
exports.heartbeatInterval = 10000;
exports.searchOptions = {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x00]),
devicePort: 6454,
listenPort: 6454,
validateResponse (msg, info, devices) {
return msg.toString('utf8', 0, 7) === "Art-Net";
}
};
exports.defaultPort = 6454;
exports.ready = function ready(device) {
const d = device;
d.data.universes = {};
d.data.orderedUniverses = [];
exports.config = {
defaultName: 'Art-Net',
connectionType: 'UDPsocket',
defaultPort: 6454,
mayChangePort: false,
heartbeatInterval: 5000,
heartbeatTimeout: 15000,
searchOptions: {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x00]),
devicePort: 6454,
listenPort: 6454,
validateResponse(msg, info, devices) {
return msg.toString('utf8', 0, 7) === 'Art-Net';
},
},
};
exports.data = function data(device, buf) {
if(buf.length < 18){
return
}
const univ = buf.readUInt8(14);
const d = device;
let u = d.data.universes[univ];
exports.ready = function ready(_device) {
const device = _device;
device.data.universes = {};
device.data.orderedUniverses = [];
};
if(!u){
d.data.universes[univ] = {};
u = d.data.universes[univ];
exports.data = function data(_device, buf) {
if (buf.length < 18) {
return;
}
const universeIndex = buf.readUInt8(14);
const device = _device;
let universe = device.data.universes[universeIndex];
if (!universe) {
device.data.universes[universeIndex] = {};
universe = device.data.universes[universeIndex];
}
u.sequence = buf.readUInt8(12);
u.net = buf.readUInt8(15);
u.opCode = buf.readUInt8(9);
u.version = buf.readUInt16BE(10);
u.slots = Array.prototype.slice.call(buf, 18);
d.data.ip = d.addresses[0];
universe.sequence = buf.readUInt8(12);
universe.subnet = buf.readUInt8(15);
universe.opCode = buf.readUInt8(9);
universe.version = buf.readUInt16BE(10);
universe.slots = buf.slice(18);
device.data.ip = device.addresses[0];
if(!_.includes(d.data.orderedUniverses, univ)){
d.data.orderedUniverses.push(univ);
d.data.orderedUniverses.sort();
u.slotElems = [];
u.slotElemsSet = false;
d.draw();
updateElementsCache(d, d.data.universes, d.data.orderedUniverses);
if (!device.data.orderedUniverses.includes(universeIndex)) {
device.data.orderedUniverses.push(universeIndex);
device.data.orderedUniverses.sort();
universe.slotElems = [];
universe.slotElemsSet = false;
device.draw();
device.update('elementCache');
}
device.update("universeData", {
u: univ,
universe: u
device.update('universeData', {
universeIndex,
universe,
});
};
exports.heartbeat = function heartbeat(device) {
};
exports.heartbeat = function heartbeat(device) {};
let lastUpdate = Date.now();
exports.update = function update(device, document, updateType, updateData){
exports.update = function update(_device, _document, updateType, updateData) {
const device = _device;
const data = updateData;
const doc = document;
const document = _document;
if(updateType === "universeData" && data.universe){
if(Date.now() - lastUpdate > 1000){
if (updateType === 'universeData' && data.universe) {
if (Date.now() - lastUpdate > 1000) {
lastUpdate = Date.now();
updateElementsCache(device, device.data.universes, device.data.orderedUniverses);
device.update('elementCache');
}
const $elem = doc.getElementById(`universe-${data.u}`);
const $elem = document.getElementById(`universe-${data.universeIndex}`);
if($elem && data.universe.slotElemsSet){
for(let i = 0; i < 512; i++){
data.universe.slotElems[i].innerText = data.universe.slots[i];
if ($elem && data.universe.slotElemsSet) {
for (let i = 0; i < 512; i++) {
data.universe.slotElems[i].textContent = data.universe.slots[i];
}
doc.getElementById(`universe-${data.u}-sequence`).innerText = data.universe.sequence;
}else{
document.getElementById(`universe-${data.universeIndex}-sequence`).textContent = data.universe.sequence;
} else {
device.draw();
updateElementsCache(device, device.data.universes, device.data.orderedUniverses);
device.update('elementCache');
}
}else if(updateType === "elementCache"){
data.orderedUniverses.forEach(univ => {
for(let i = 0; i < 512; i++){
data.universes[univ].slotElems[i] = doc.getElementById(`${univ}-${i}`);
} else if (updateType === 'elementCache') {
device.data.orderedUniverses.forEach((universeIndex) => {
for (let i = 0; i < 512; i++) {
device.data.universes[universeIndex].slotElems[i] = document.getElementById(`${universeIndex}-${i}`);
}
data.universes[univ].slotElemsSet = true;
device.data.universes[universeIndex].slotElemsSet = true;
});
}
}
function updateElementsCache(device, univ, ordered){
device.update("elementCache", {
universes: univ,
orderedUniverses: ordered
});
}
};
+7 -12
View File
@@ -4,23 +4,23 @@
<div id="universes">
<% data.orderedUniverses.forEach(univ => {
let universe = data.universes[univ];
<% data.orderedUniverses.forEach(universeIndex => {
let universe = data.universes[universeIndex];
%>
<table id="universe-<%= univ %>">
<table id="universe-<%= universeIndex %>">
<tr>
<td class="data" colspan="2">
Universe
<br><em><%= univ %></em>
<br><em><%= universeIndex %></em>
</td>
<td class="data" colspan="2">
Sub-Net
<br><em><%= universe.net %></em>
<br><em><%= universe.subnet %></em>
</td>
<td class="data" colspan="2">
Sequence
<br><em id="universe-<%= univ %>-sequence"><%= universe.sequence %></em>
<br><em id="universe-<%= universeIndex %>-sequence"><%= universe.sequence %></em>
</td>
<td class="data" colspan="2">
OpCode
@@ -35,7 +35,6 @@
<br><em><%= data.ip %></em>
</td>
<td class="data" colspan="3"></td>
</tr>
<tr>
@@ -51,14 +50,10 @@
<th><%= row*16+1 %></th>
<% for(col=0; col<16; col++){ %>
<% slot++%>
<td id="<%= univ %>-<%= row*16+col %>" title="<%= univ %>/<%= slot %>"><%= universe.slots[slot-1] %></td>
<td id="<%= universeIndex %>-<%= row*16+col %>" title="<%= universeIndex %>/<%= slot %>"><%= universe.slots[slot-1] %></td>
<% } %>
</tr>
<% } %>
</table>
<% }); %>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+11
View File
@@ -0,0 +1,11 @@
<h3>ATEM Configuration</h3>
<ul>
<li>Set the IP Address, Subnet Mask, and Gateway of the ATEM using the <em>ATEM Setup utility</em></li>
<li>The ATEM must be connected to a computer using a USB cable to change its Network settings</li>
</ul>
<h3>ATEM Software Download</h3>
<button href="https://www.blackmagicdesign.com/support/family/atem-live-production-switchers">
Blackmagic Support
</button>
(Look for <em>ATEM Switchers Update</em>)
+240
View File
@@ -0,0 +1,240 @@
const { TransitionStyle, TransitionSelection } = require('atem-connection/dist/enums');
let timerFrameRate;
exports.config = {
defaultName: 'ATEM',
connectionType: 'atem',
defaultPort: 9910,
mayChangePort: false,
searchOptions: {
type: 'UDPScan',
searchBuffer: Buffer.from([
0x10, 0x14, 0x53, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
]),
listenPort: 0,
devicePort: 9910,
validateResponse(msg, info) {
// This is a tad bit hacky but works from what I can see.
return Buffer.compare(msg.slice(0, 4), Buffer.from([16, 20, 83, 171])) === 0;
},
},
};
exports.ready = function ready(_device) {
console.log('atem ready');
const device = _device;
device.data = device.connection.state;
device.draw();
// TODO: this is a little hacky but it works? gotta be a way to hook into device.draw() to trigger update
setInterval(() => {
device.update('inputs', device.data);
device.update('fadeToBlack', device.data);
device.update(`transitionPosition`, device.data);
device.update('downstreamKeyers', device.data);
device.update('upstreamKeyers', device.data);
device.update('transitionProperties', device.data);
device.update('deviceInfo', device.data);
}, 1000);
};
exports.update = function update(device, _document, updateType, data) {
const document = _document;
if (updateType.includes('transitionPosition')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const mixEffect = data.video.mixEffects[i];
const tbarId = `me-${i}-tbar-div`;
const tbarHandleId = `me-${i}-tbar-handle-div`;
if (document.getElementById(tbarId)) {
document.getElementById(tbarId).style.height = `${mixEffect.transitionPosition.handlePosition / 100}%`;
}
if (document.getElementById(tbarHandleId)) {
document.getElementById(tbarHandleId).style.bottom = `${mixEffect.transitionPosition.handlePosition / 100}%`;
}
document.getElementById(`me-${i}-transition-rate`).textContent = framesToTime(
mixEffect.transitionPosition.remainingFrames
);
if (mixEffect.transitionPosition.inTransition) {
document.getElementById(`me-${i}-auto`).classList.add('atem-red');
} else {
document.getElementById(`me-${i}-auto`).classList.remove('atem-red');
}
}
} else if (updateType.includes('downstreamKeyers')) {
for (let i = 0; i < data.video.downstreamKeyers.length; i++) {
const dsk = data.video.downstreamKeyers[i];
if (dsk.isAuto) {
document.getElementById(`dsk-${i}-auto`).classList.add('atem-red');
} else {
document.getElementById(`dsk-${i}-auto`).classList.remove('atem-red');
}
if (dsk.onAir) {
document.getElementById(`dsk-${i}-onair`).classList.add('atem-red');
} else {
document.getElementById(`dsk-${i}-onair`).classList.remove('atem-red');
}
if (dsk.properties.tie) {
document.getElementById(`dsk-${i}-tie`).classList.add('atem-yellow');
} else {
document.getElementById(`dsk-${i}-tie`).classList.remove('atem-yellow');
}
document.getElementById(`dsk-${i}-rate`).textContent = framesToTime(dsk.remainingFrames);
}
} else if (updateType.includes('fadeToBlack')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const fadeToBlack = data.video.mixEffects[i].fadeToBlack;
const ftbRateId = `me-${i}-ftb-rate`;
const ftbId = `me-${i}-ftb`;
document.getElementById(ftbRateId).textContent = framesToTime(fadeToBlack.remainingFrames);
if (fadeToBlack.isFullyBlack) {
if (document.getElementById(ftbId).classList.contains('atem-red')) {
document.getElementById(ftbId).classList.remove('atem-red');
} else {
document.getElementById(ftbId).classList.add('atem-red');
}
} else if (fadeToBlack.inTransition) {
document.getElementById(ftbId).classList.add('atem-red');
} else {
document.getElementById(ftbId).classList.remove('atem-red');
}
}
} else if (
updateType.includes('programInput') ||
updateType.includes('previewInput') ||
updateType.includes('inputs')
) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const mixEffect = data.video.mixEffects[0];
Object.keys(device.data.inputs)
.map((key) => Number(key))
.forEach((inputId) => {
if (mixEffect.programInput === inputId) {
document.getElementById(`me-${i}-program-input-${inputId}`).classList.add('atem-red');
document.getElementById(`me-${i}-input-${inputId}`).classList.add('atem-red');
} else {
document.getElementById(`me-${i}-program-input-${inputId}`).classList.remove('atem-red');
document.getElementById(`me-${i}-input-${inputId}`).classList.remove('atem-red');
}
if (mixEffect.previewInput === inputId) {
document.getElementById(`me-${i}-preview-input-${inputId}`).classList.add('atem-green');
document.getElementById(`me-${i}-input-${inputId}`).classList.add('atem-green');
} else {
document.getElementById(`me-${i}-preview-input-${inputId}`).classList.remove('atem-green');
document.getElementById(`me-${i}-input-${inputId}`).classList.remove('atem-green');
}
});
}
} else if (updateType.includes('transitionProperties')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const transitionProperties = data.video.mixEffects[i].transitionProperties;
Object.keys(TransitionStyle)
.filter((key) => !isNaN(Number(key)))
.map((key) => Number(key))
.forEach((style) => {
if (style === transitionProperties.style) {
document.getElementById(`me-${i}-transition-style-${style}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-style-${style}`).classList.remove('atem-yellow');
}
if (style === transitionProperties.nextStyle) {
document.getElementById(`me-${i}-transition-style-${style}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-style-${style}`).classList.remove('atem-yellow');
}
});
Object.keys(TransitionSelection)
.filter((key) => !isNaN(Number(key)))
.map((key) => Number(key))
.forEach((selection) => {
if (transitionProperties.selection.includes(selection)) {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.remove('atem-yellow');
}
if (transitionProperties.nextSelection.includes(selection)) {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.remove('atem-yellow');
}
});
}
} else if (updateType.includes('upstreamKeyers')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const upstreamKeyers = data.video.mixEffects[i].upstreamKeyers;
upstreamKeyers.forEach((upstreamKeyer) => {
if (upstreamKeyer.onAir) {
document.getElementById(`me-${i}-key-${upstreamKeyer.upstreamKeyerId + 1}-onair`).classList.add('atem-red');
} else {
document
.getElementById(`me-${i}-key-${upstreamKeyer.upstreamKeyerId + 1}-onair`)
.classList.remove('atem-red');
}
});
}
} else if (updateType.includes('transitionPreview')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const mixEffect = data.video.mixEffects[i];
if (mixEffect.transitionPreview) {
document.getElementById(`me-${i}-transition-preview`).classList.add('atem-red');
} else {
document.getElementById(`me-${i}-transition-preview`).classList.remove('atem-red');
}
}
} else if (updateType.includes('deviceInfo')) {
// videoModes pulled from https://github.com/nrkno/sofie-atem-connection/blob/master/src/enums/index.ts#L238
if ([27, 26, 23, 25, 19, 13, 11, 7, 5].includes(data.settings.videoMode)) {
timerFrameRate = 30;
} else if ([24, 22, 18, 16, 12, 10, 6, 4].includes(data.settings.videoMode)) {
timerFrameRate = 25;
} else if ([21, 20, 15, 14, 9, 8].includes(data.settings.videoMode)) {
timerFrameRate = 24;
} else {
timerFrameRate = 30;
}
if (!device.displayName) {
this.deviceInfoUpdate(device, 'displayName', data.info.productIdentifier);
}
} else {
console.log('unhandled update');
console.log(updateType);
console.log(device.data);
}
};
exports.data = function data(_device, msg) {
const device = _device;
this.deviceInfoUpdate(device, 'status', 'ok');
device.data = msg.state;
msg.pathToChange.forEach((path) => {
device.update(path, device.data);
});
};
function framesToTime(total) {
if (timerFrameRate) {
const seconds = Math.floor(total / timerFrameRate);
const frames = total - seconds * timerFrameRate;
let framesString = String(frames);
if (frames < 10) {
framesString = `0${framesString}`;
} else if (framesString === '0') {
framesString = '00';
}
return `${seconds}:${framesString}`;
}
return '0:00';
}
+203
View File
@@ -0,0 +1,203 @@
body {
background-color: #282828;
}
h1 {
font-family: 'Open Sans', sans-serif;
}
h3 {
color: #6d6d6d !important;
margin: 30px 0px 10px 9px;
font-size: 0.9em;
font-weight: 400;
font-family: 'Open Sans', sans-serif;
}
.me-label {
margin-top: 0;
margin-bottom: 2px;
}
.atem-input {
width: 52px;
height: 52px;
background-image: url('img/button_white.png');
}
.source-wrapper {
display: flex;
flex-wrap: wrap;
background-color: #1f1f1f;
border: #1a1a1a 2px solid;
border-radius: 8px;
max-width: 416px;
padding: 12px;
}
.atem-red {
background-image: url('img/button_red.png');
box-shadow: 0px 0px 10px 1px #ff0000;
z-index: 10000;
}
.atem-green {
background-image: url('img/button_green.png');
box-shadow: 0px 0px 10px 1px #06c300;
z-index: 10000;
}
.atem-yellow {
background-image: url('img/button_yellow.png');
box-shadow: 0px 0px 10px 1px #c7ca00;
z-index: 10000;
}
.atem-disabled {
background-image: url('img/button_off.png');
color: #3a3a3a;
}
.atem-gray {
color: #575757;
}
.source-label {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 11px;
font-weight: bold;
}
.transition-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.transition-settings {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-right: 30px;
}
.tbar-container {
display: flex;
flex-direction: column-reverse;
background-color: #1f1f1f;
border: 2px solid;
border-color: #1a1a1a;
border-radius: 6px;
width: 88px;
height: 429px;
position: relative;
margin-top: 58px;
margin-right: 60px;
}
.tbar-bg {
position: absolute;
background-image: url('img/tbar_bg.png');
width: 88px;
height: 429px;
}
.tbar-handle {
position: absolute;
background-image: url('img/tbar_handle.png');
width: 126px;
height: 50px;
left: -3px;
}
.tbar-div {
width: 100%;
background-color: #7aff58;
overflow: hidden;
}
.dsk {
width: 100%;
}
.dsk .source-wrapper {
display: flex;
flex-direction: column;
align-content: center;
width: 100%;
padding: 5px;
}
.dsk h3 {
text-align: center;
}
.fade-to-black-container {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-right: 30px;
}
.fade-to-black .source-wrapper {
display: flex;
width: 100%;
padding: 5px;
}
.fade-to-black h3 {
text-align: center;
}
.rate-heading {
text-align: center;
font-size: small;
margin-bottom: 4px;
}
.dsk-rate,
.ftb-rate,
.transition-rate {
display: flex;
justify-content: center;
flex-direction: column;
}
.dsk-rate-label,
.ftb-rate-label,
.transition-rate-label {
color: rgb(235, 110, 0);
background-color: black;
border-radius: 3px;
padding: 6px 2px;
text-align: center;
}
.clear {
background: transparent;
border-color: transparent;
background-color: transparent;
}
.hide {
display: none;
}
.no-wrap {
flex-wrap: nowrap;
}
.show-small {
display: none;
}
@media screen and (min-width: 0px) and (max-width: 480px) {
.hide-small {
display: none;
}
.show-small {
display: block;
}
}
.float-left {
float: left;
}
+189
View File
@@ -0,0 +1,189 @@
<header>
<h1><%= listName %></h1>
</header>
<% if (data.video.mixEffects) { %>
<% Object.entries(data.video.mixEffects).forEach(([meIndex, state])=>{ %>
<div class="mixeffect_wrapper" id="me-<%= meIndex %>">
<h1 class="atem-gray me-label">ME<%= Number(meIndex) + 1 %></h1>
<div class="inputs-container show-small">
<h3 class="atem-gray">Preview/Program</h3>
<div class="source-wrapper">
<% Object.values(data.inputs).sort((a,b)=>a.internalPortType<b.internalPortType).forEach((input)=>{ %>
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
<div id="me-<%= meIndex %>-input-<%= input.inputId %>" class="atem-input">
<div class="source-label"><%= input.shortName %></div>
</div>
<% } else {%>
<div id="me-<%= meIndex %>-input-<%= input.inputId %>" class="atem-input atem-disabled">
<div class="source-label"><%= input.shortName %></div>
</div>
<% }%>
<% }) %>
</div>
</div>
<div class="float-left" style="margin-right: 30px;">
<div class="program-container hide-small">
<h3 class="atem-gray">Program</h3>
<div class="source-wrapper">
<% Object.values(data.inputs).sort((a,b)=>a.internalPortType<b.internalPortType).forEach((input)=>{ %>
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
<div id="me-<%= meIndex %>-program-input-<%= input.inputId %>" class="atem-input">
<div class="source-label"><%= input.shortName %></div>
</div>
<% } else {%>
<div id="me-<%= meIndex %>-program-input-<%= input.inputId %>" class="atem-input atem-disabled">
<div class="source-label"><%= input.shortName %></div>
</div>
<% }%>
<% }) %>
</div>
</div>
<div class="preview hide-small">
<h3 class="atem-gray">Preview</h3>
<div class="source-wrapper">
<% Object.values(data.inputs).forEach((input)=>{ %>
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
<div id="me-<%= meIndex %>-preview-input-<%= input.inputId %>" class="atem-input">
<div class="source-label"><%= input.shortName %></div>
</div>
<% } else {%>
<div id="me-<%= meIndex %>-preview-input-<%= input.inputId %>" class="atem-input atem-disabled">
<div class="source-label"><%= input.shortName %></div>
</div>
<% }%>
<% }) %>
</div>
</div>
</div>
<div class="transition-container float-left">
<div class="transition-settings">
<div class="next-transition">
<h3>Next Transition</h3>
<div class="source-wrapper" style="width: 260px;">
<div class="atem-input clear"></div>
<div id="me-<%= meIndex %>-key-1-onair" class="atem-input">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-key-2-onair" class="atem-input <% if(data.video.downstreamKeyers.length<2){ %> atem-disabled <% } %>">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-key-3-onair" class="atem-input <% if(data.video.downstreamKeyers.length<3){ %> atem-disabled <% } %>">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-key-4-onair" class="atem-input <% if(data.video.downstreamKeyers.length<4){ %> atem-disabled <% } %>">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-1" class="atem-input">
<div class="source-label">BKGD</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-2" class="atem-input">
<div class="source-label">KEY 1</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-4" class="atem-input <% if(data.video.downstreamKeyers.length<2){ %> atem-disabled <% } %>">
<div class="source-label">KEY 2</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-8" class="atem-input <% if(data.video.downstreamKeyers.length<3){ %> atem-disabled <% } %>">
<div class="source-label">KEY 3</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-16" class="atem-input <% if(data.video.downstreamKeyers.length<4){ %> atem-disabled <% } %>">
<div class="source-label">KEY 4</div>
</div>
</div>
</div>
<div class="transition-style">
<h3>Transition Style</h3>
<div class="source-wrapper" style="width: 260px;">
<div id="me-<%= meIndex %>-transition-style-0" class="atem-input">
<div class="source-label">MIX</div>
</div>
<div id="me-<%= meIndex %>-transition-style-1" class="atem-input">
<div class="source-label">DIP</div>
</div>
<div id="me-<%= meIndex %>-transition-style-2" class="atem-input">
<div class="source-label">WIPE</div>
</div>
<div id="me-<%= meIndex %>-transition-style-4" class="atem-input">
<div class="source-label">STING</div>
</div>
<div id="me-<%= meIndex %>-transition-style-3" class="atem-input">
<div class="source-label">DVE</div>
</div>
<div id="me-<%= meIndex %>-transition-preview" class="atem-input">
<div class="source-label" style="flex-direction: column;">
<div>PREV</div> <div>TRANS</div>
</div>
</div>
<div class="atem-input clear"></div>
<div id="me-<%= meIndex %>-cut" class="atem-input">
<div class="source-label">CUT</div>
</div>
<div id="me-<%= meIndex %>-auto" class="atem-input">
<div class="source-label">AUTO</div>
</div>
<div class="transition-rate atem-input clear">
<div class="atem-gray rate-heading">Rate</div>
<div id="me-<%= meIndex %>-transition-rate" class="transition-rate-label">&nbsp;&nbsp;</div>
</div>
</div>
</div>
</div>
<div class="tbar-container">
<div class="tbar-bg"></div>
<div id="me-<%= meIndex %>-tbar-handle-div" class="tbar-handle"></div>
<div id="me-<%= meIndex %>-tbar-div" class="tbar-div" ></div>
</div>
<div class="fade-to-black-container">
<% if (data.video.downstreamKeyers) { %>
<% Object.entries(data.video.downstreamKeyers).forEach(([dskIndex, state])=>{ %>
<div class="dsk">
<h3 class="atem-gray">DSK<%= Number(dskIndex) + 1 %></h3>
<div class="source-wrapper">
<div id="dsk-<%= dskIndex %>-tie" class="atem-input">
<div class="source-label">TIE</div>
</div>
<div class="dsk-rate atem-input clear">
<div class="atem-gray rate-heading">Rate</div>
<div id="dsk-<%= dskIndex %>-rate" class="dsk-rate-label">&nbsp;&nbsp;</div>
</div>
<div id="dsk-<%= dskIndex %>-onair" class="atem-input">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="dsk-<%= dskIndex %>-auto" class="atem-input">
<div class="source-label">AUTO</div>
</div>
</div>
</div>
<% }) %>
<% } %>
<div class="fade-to-black">
<h3 class="atem-gray">Fade to Black</h3>
<div class="source-wrapper">
<div class="ftb-rate atem-input clear">
<div class="atem-gray rate-heading">Rate</div>
<div id="me-<%= meIndex %>-ftb-rate" class="ftb-rate-label">&nbsp;&nbsp;</div>
</div>
<div id="me-<%= meIndex %>-ftb" class="atem-input">
<div class="source-label">FTB</div>
</div>
</div>
</div>
</div>
</div>
</div>
<% }) %>
<% } %>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File
+441
View File
@@ -0,0 +1,441 @@
exports.config = {
defaultName: 'd&b Amps',
connectionType: 'TCPsocket',
defaultPort: 30013,
mayChangePort: false,
heartbeatInterval: 1000,
heartbeatTimeout: 5000,
searchOptions: {
type: 'Bonjour',
bonjourName: 'oca',
validateResponse(msg, info) {
return msg.includes('d&b audiotechnik');
},
},
};
exports.ready = function ready(_device) {
const device = _device;
device.data.oca = structuredClone(OCAStructure);
device.data.ocaKeys = Object.keys(device.data.oca);
device.data.ocaIDs = [];
for (let i = 0; i < device.data.ocaKeys.length; i++) {
const key = device.data.oca[device.data.ocaKeys[i]];
key.parameters = [0];
device.data.ocaIDs[i] = key;
ocaCommandRequest(i, key.target, key.method, device);
ocaSubscriptionRequest(i, key.target, key.method, device);
}
};
exports.data = function data(_device, _buffer) {
const device = _device;
if (_buffer[0] === 0x3b && _buffer[1] === 0 && _buffer[2] === 1) {
let start = 0;
while (start < _buffer.length - 3) {
const messageLength = _buffer.readInt32BE(start + 3) + 1;
const message = _buffer.slice(start, start + messageLength);
const messageType = message[7];
if (messageType === 0x04) {
// keep-alive message
} else if (messageType === 0x02) {
// console.log('NOTIFICATION!!');
console.log(message.slice(33));
} else if (messageType === 0x03) {
const messageHandle = _buffer.readInt32BE(start + 14);
const paramCount = message[19];
if (paramCount) {
let paramsStart = 20;
const ocaObj = device.data.ocaIDs[messageHandle];
ocaObj.parameters = [];
while (paramsStart < message.length) {
let paramLength = 0;
let param;
if (ocaObj.format === 'int16' || ocaObj.format === 'float' || ocaObj.format === 'int32') {
paramLength = 2;
param = message.slice(paramsStart, paramsStart + paramLength + 2);
} else if (ocaObj.format === 'int8') {
paramLength = 1;
param = message.slice(paramsStart, paramsStart + paramLength);
} else if (ocaObj.format === 'stringArray') {
const nextBreak = message.indexOf(0x00, paramsStart + 1);
if (nextBreak < 0) {
break;
}
paramLength = nextBreak - paramsStart + 1;
param = message.slice(paramsStart - 1, paramsStart + paramLength - 1).toString();
} else {
paramLength = message.readInt16BE(20);
param = message.slice(paramsStart + 2, paramsStart + 2 + paramLength);
}
if (ocaObj.format === 'string') {
ocaObj.parameters.push(param.toString());
} else if (ocaObj.format === 'stringArray') {
ocaObj.parameters.push(param.toString());
} else if (ocaObj.format === 'float') {
ocaObj.parameters.push(param.readFloatBE(0));
} else if (ocaObj.format === 'int16') {
ocaObj.parameters.push(param.readInt16BE(0));
} else if (ocaObj.format === 'int8') {
ocaObj.parameters.push(param.readInt8(0));
} else if (ocaObj.format === 'int32') {
ocaObj.parameters.push(param.readInt32BE(0));
} else {
ocaObj.parameters.push(param);
}
this.deviceInfoUpdate(device, 'status', 'ok');
paramsStart += paramLength + 2;
}
}
device.draw();
}
start += messageLength;
}
}
};
exports.heartbeat = function heartbeat(device) {
device.send(Buffer.from('3b00010000000b0400010001', 'hex'));
};
function ocaCommandRequest(handle, target, method, device) {
const buf = Buffer.from('3b00010000001a01000100000011AAAAAAAABBBBBBBBCCCCCCCC00', 'hex');
buf.writeInt32BE(handle, 14); // handle
buf.writeInt32BE(target, 18); // target
buf.writeInt32BE(method, 22); // method
device.send(buf);
return buf;
}
function ocaSubscriptionRequest(handle, target, method, device) {
const buf = Buffer.from(
'3b00010000002f01000100000026000000ae00000004000300010510008205000100010000041f000100010000010000',
'hex'
);
buf.writeInt32BE(handle, 14); // handle
buf.writeInt32BE(target, 27); // target
device.send(buf);
return buf;
}
const OCAStructure = {
ModelDescription: {
target: 0x1000010d,
method: 0x00050001,
format: 'string',
},
PwrOn: {
target: 0x10000100,
method: 0x00040001,
format: 'int16',
},
SubNet: {
target: 0x10000109,
method: 0x00050001,
format: 'int32',
},
DeviceId: {
target: 0x10000108,
method: 0x00050001,
format: 'int32',
},
DeviceType: {
target: 0x10000034,
method: 0x00050001,
format: 'string',
},
FirmwareRevision: {
target: 0x10000031,
method: 0x00050001,
format: 'string',
},
ChannelNameA: {
target: 0x10008215,
method: 0x00050001,
format: 'string',
},
ChannelNameB: {
target: 0x10010215,
method: 0x00050001,
format: 'string',
},
ChannelNameC: {
target: 0x10018215,
method: 0x00050001,
format: 'string',
},
ChannelNameD: {
target: 0x10020215,
method: 0x00050001,
format: 'string',
},
DelayA: {
target: 0x10008207,
method: 0x00050001,
format: 'float',
},
DelayB: {
target: 0x10010207,
method: 0x00050001,
format: 'float',
},
DelayC: {
target: 0x10018207,
method: 0x00050001,
format: 'float',
},
DelayD: {
target: 0x10020207,
method: 0x00050001,
format: 'float',
},
InputEnable1A: {
target: 0x10008220,
method: 0x00040001,
format: 'int16',
},
InputEnable2A: {
target: 0x10008221,
method: 0x00040001,
format: 'int16',
},
InputEnable3A: {
target: 0x10008222,
method: 0x00040001,
format: 'int16',
},
InputEnable4A: {
target: 0x10008223,
method: 0x00040001,
format: 'int16',
},
InputEnable5A: {
target: 0x10008224,
method: 0x00040001,
format: 'int16',
},
InputEnable6A: {
target: 0x10008225,
method: 0x00040001,
format: 'int16',
},
InputEnable7A: {
target: 0x10008226,
method: 0x00040001,
format: 'int16',
},
InputEnable8A: {
target: 0x10008227,
method: 0x00040001,
format: 'int16',
},
InputEnable1B: {
target: 0x10010220,
method: 0x00040001,
format: 'int16',
},
InputEnable2B: {
target: 0x10010221,
method: 0x00040001,
format: 'int16',
},
InputEnable3B: {
target: 0x10010222,
method: 0x00040001,
format: 'int16',
},
InputEnable4B: {
target: 0x10010223,
method: 0x00040001,
format: 'int16',
},
InputEnable5B: {
target: 0x10010224,
method: 0x00040001,
format: 'int16',
},
InputEnable6B: {
target: 0x10010225,
method: 0x00040001,
format: 'int16',
},
InputEnable7B: {
target: 0x10010226,
method: 0x00040001,
format: 'int16',
},
InputEnable8B: {
target: 0x10010227,
method: 0x00040001,
format: 'int16',
},
InputEnable1C: {
target: 0x10018220,
method: 0x00040001,
format: 'int16',
},
InputEnable2C: {
target: 0x10018221,
method: 0x00040001,
format: 'int16',
},
InputEnable3C: {
target: 0x10018222,
method: 0x00040001,
format: 'int16',
},
InputEnable4C: {
target: 0x10018223,
method: 0x00040001,
format: 'int16',
},
InputEnable5C: {
target: 0x10018224,
method: 0x00040001,
format: 'int16',
},
InputEnable6C: {
target: 0x10018225,
method: 0x00040001,
format: 'int16',
},
InputEnable7C: {
target: 0x10018226,
method: 0x00040001,
format: 'int16',
},
InputEnable8C: {
target: 0x10018227,
method: 0x00040001,
format: 'int16',
},
InputEnable1D: {
target: 0x10020220,
method: 0x00040001,
format: 'int16',
},
InputEnable2D: {
target: 0x10020221,
method: 0x00040001,
format: 'int16',
},
InputEnable3D: {
target: 0x10020222,
method: 0x00040001,
format: 'int16',
},
InputEnable4D: {
target: 0x10020223,
method: 0x00040001,
format: 'int16',
},
InputEnable5D: {
target: 0x10020224,
method: 0x00040001,
format: 'int16',
},
InputEnable6D: {
target: 0x10020225,
method: 0x00040001,
format: 'int16',
},
InputEnable7D: {
target: 0x10020226,
method: 0x00040001,
format: 'int16',
},
InputEnable8D: {
target: 0x10020227,
method: 0x00040001,
format: 'int16',
},
InputGainA: {
target: 0x10008206,
method: 0x00040001,
format: 'float',
},
InputGainB: {
target: 0x10010206,
method: 0x00040001,
format: 'float',
},
InputGainC: {
target: 0x10018206,
method: 0x00040001,
format: 'float',
},
InputGainD: {
target: 0x10020206,
method: 0x00040001,
format: 'float',
},
SpeakerIDA: {
target: 0x10008214,
method: 0x00040001,
format: 'int16',
},
SpeakerIDB: {
target: 0x10010214,
method: 0x00040001,
format: 'int16',
},
SpeakerIDC: {
target: 0x10018214,
method: 0x00040001,
format: 'int16',
},
SpeakerIDD: {
target: 0x10020214,
method: 0x00040001,
format: 'int16',
},
SpeakerIDList: {
target: 0x10010214,
method: 0x00040005,
format: 'stringArray',
},
OutputMode: {
target: 0x10000111,
method: 0x00040001,
format: 'int16',
},
OutputModeList: {
target: 0x10000111,
method: 0x00040005,
format: 'stringArray',
},
MuteA: {
target: 0x10008205,
method: 0x00040001,
format: 'int8',
},
MuteB: {
target: 0x10010205,
method: 0x00040001,
format: 'int8',
},
MuteC: {
target: 0x10018205,
method: 0x00040001,
format: 'int8',
},
MuteD: {
target: 0x10020205,
method: 0x00040001,
format: 'int8',
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.
+108
View File
@@ -0,0 +1,108 @@
table {
color: white;
background-color: #323232;
font-size: 14px;
}
.button {
padding: 4px;
height: 65px;
border-radius: 4px;
position: relative;
text-align: right;
margin: 2px;
box-sizing: border-box;
overflow: hidden;
white-space: nowrap;
}
.light-button {
background: linear-gradient(0deg, #484848, #737373);
outline: black 2px outset;
border-top: #8b8b8b 2px solid;
border-left: #8b8b8b 2px solid;
min-width: 100px;
}
.dark-button {
background: linear-gradient(0deg, #0e0e0e, #282828);
outline: #737373 2px solid;
text-overflow: ellipsis;
white-space: nowrap;
}
.mute {
text-align: center;
background-image: url('muteicon-on.png'), linear-gradient(0deg, #484848, #737373);
background-position: 52px 13px, 0px 0px;
background-repeat: no-repeat;
}
.gain-box {
background-color: #272727;
height: 60px;
width: 65px;
margin: 2px;
border: #656565 2px solid;
outline: #111111 2px solid;
text-align: right;
}
.gain-head {
box-sizing: border-box;
height: 12px;
width: 100%;
background: #686868;
border: #1a1a1a 2px solid;
outline: #656565 2px solid;
margin-bottom: 27px;
}
.gain-box span {
padding: 4px;
}
h3 {
margin: 7px 0px 3px;
margin-left: 12px;
text-align: left;
font-weight: normal;
font-size: 22px;
}
h4 {
text-align: left;
font-size: 16px;
margin: 0px;
}
.channel-input {
position: absolute;
left: 9px;
top: 27px;
border-right: white 1px solid;
padding-right: 40px;
text-align: left;
line-height: 14px;
}
.channel-delay {
position: absolute;
left: 170px;
top: 40px;
}
.power-1 {
background: linear-gradient(0deg, #5a8127, #789b50);
border: #506937 1px solid;
}
.indicate-1 {
background: #00ff00;
border: black 2px inset;
}
.mute-1 {
border-top: #772b26 2px solid;
border-left: #772b26 2px solid;
background-image: url('muteicon-mute.png'), linear-gradient(0deg, #b52714, #c4473e);
}
@media screen and (min-width: 0px) and (max-width: 450px) {
.hide-medium {
display: none;
}
}
@media screen and (min-width: 0px) and (max-width: 300px) {
.hide-small {
display: none;
}
}
+141
View File
@@ -0,0 +1,141 @@
<header>
<h1><%= listName %></h1>
<h2><%= data.oca.FirmwareRevision.parameters[0] %></h2>
</header>
<table>
<tr>
<td>
<div class="button dark-button indicate-<%=data.oca.PwrOn.parameters[0]%>"></div>
</td>
<td width="250px" class="hide-small">
<div class="button dark-button">
<h4><%= data.oca.ModelDescription.parameters[0] %></h4><br>
<span class="hide-medium"><%= data.oca.OutputModeList.parameters[data.oca.OutputMode.parameters[0]+1] %></span>
</div>
</td>
<td class="hide-medium">
<div class="button dark-button">
<h4>ID</h4><br>
<%= data.oca.SubNet.parameters[0] %>.<%= data.oca.DeviceId.parameters[0].toString().padStart(2, '0') %>
</div>
</td>
<td width="95px"><div class="button dark-button power-<%=data.oca.PwrOn.parameters[0]%>"><h4>Power</h4><br>On</div></td>
</tr>
<tr>
<td></td>
<td class="hide-small">
<div class="button light-button">
<h4><%= data.oca.ChannelNameA.parameters[0] %></h4>
<div class="channel-input">
Input<br>
<% if(data.oca.InputEnable1A.parameters[0]){ %> A1 <% } %>
<% if(data.oca.InputEnable2A.parameters[0]){ %> A2 <% } %>
<% if(data.oca.InputEnable3A.parameters[0]){ %> A3 <% } %>
<% if(data.oca.InputEnable4A.parameters[0]){ %> A4 <% } %>
<% if(data.oca.InputEnable5A.parameters[0]){ %> D1 <% } %>
<% if(data.oca.InputEnable6A.parameters[0]){ %> D2 <% } %>
<% if(data.oca.InputEnable7A.parameters[0]){ %> D3 <% } %>
<% if(data.oca.InputEnable8A.parameters[0]){ %> D4 <% } %>
</div>
<div class="channel-delay">
<%= data.oca.DelayA.parameters[0].toFixed(1) %>ms
</div>
</div>
</td>
<td class="hide-medium">
<div class="gain-box">
<div class="gain-head"></div>
<span><%= data.oca.InputGainA.parameters[0].toFixed(1) %>db</span>
</div>
</td>
<td><div class="button light-button mute mute-<%=data.oca.MuteA.parameters[0]%>"><h3>A</h3><%= data.oca.SpeakerIDList.parameters[data.oca.SpeakerIDA.parameters[0]] %></div></td>
</tr>
<tr>
<td></td>
<td class="hide-small">
<div class="button light-button">
<h4><%= data.oca.ChannelNameB.parameters[0] %></h4>
<div class="channel-input">
Input<br>
<% if(data.oca.InputEnable1B.parameters[0]){ %> A1 <% } %>
<% if(data.oca.InputEnable2B.parameters[0]){ %> A2 <% } %>
<% if(data.oca.InputEnable3B.parameters[0]){ %> A3 <% } %>
<% if(data.oca.InputEnable4B.parameters[0]){ %> A4 <% } %>
<% if(data.oca.InputEnable5B.parameters[0]){ %> D1 <% } %>
<% if(data.oca.InputEnable6B.parameters[0]){ %> D2 <% } %>
<% if(data.oca.InputEnable7B.parameters[0]){ %> D3 <% } %>
<% if(data.oca.InputEnable8B.parameters[0]){ %> D4 <% } %>
</div>
<div class="channel-delay">
<%= data.oca.DelayB.parameters[0].toFixed(1) %>ms
</div>
</div>
</td>
<td class="hide-medium">
<div class="gain-box">
<div class="gain-head"></div>
<span><%= data.oca.InputGainB.parameters[0].toFixed(1) %>db</span>
</div>
</td>
<td><div class="button light-button mute mute-<%=data.oca.MuteB.parameters[0]%>"><h3>B</h3><%= data.oca.SpeakerIDList.parameters[data.oca.SpeakerIDB.parameters[0]] %></div></td>
</tr>
<tr>
<td></td>
<td class="hide-small">
<div class="button light-button">
<h4><%= data.oca.ChannelNameC.parameters[0] %></h4>
<div class="channel-input">
Input<br>
<% if(data.oca.InputEnable1C.parameters[0]){ %> A1 <% } %>
<% if(data.oca.InputEnable2C.parameters[0]){ %> A2 <% } %>
<% if(data.oca.InputEnable3C.parameters[0]){ %> A3 <% } %>
<% if(data.oca.InputEnable4C.parameters[0]){ %> A4 <% } %>
<% if(data.oca.InputEnable5C.parameters[0]){ %> D1 <% } %>
<% if(data.oca.InputEnable6C.parameters[0]){ %> D2 <% } %>
<% if(data.oca.InputEnable7C.parameters[0]){ %> D3 <% } %>
<% if(data.oca.InputEnable8C.parameters[0]){ %> D4 <% } %>
</div>
<div class="channel-delay">
<%= data.oca.DelayC.parameters[0].toFixed(1) %>ms
</div>
</div>
</td>
<td class="hide-medium">
<div class="gain-box">
<div class="gain-head"></div>
<span><%= data.oca.InputGainC.parameters[0].toFixed(1) %>db</span>
</div>
</td>
<td><div class="button light-button mute mute-<%=data.oca.MuteC.parameters[0]%>"><h3>C</h3><%= data.oca.SpeakerIDList.parameters[data.oca.SpeakerIDC.parameters[0]] %></div></td>
</tr>
<tr>
<td></td>
<td class="hide-small">
<div class="button light-button">
<h4><%= data.oca.ChannelNameD.parameters[0] %></h4>
<div class="channel-input">
Input<br>
<% if(data.oca.InputEnable1D.parameters[0]){ %> A1 <% } %>
<% if(data.oca.InputEnable2D.parameters[0]){ %> A2 <% } %>
<% if(data.oca.InputEnable3D.parameters[0]){ %> A3 <% } %>
<% if(data.oca.InputEnable4D.parameters[0]){ %> A4 <% } %>
<% if(data.oca.InputEnable5D.parameters[0]){ %> D1 <% } %>
<% if(data.oca.InputEnable6D.parameters[0]){ %> D2 <% } %>
<% if(data.oca.InputEnable7D.parameters[0]){ %> D3 <% } %>
<% if(data.oca.InputEnable8D.parameters[0]){ %> D4 <% } %>
</div>
<div class="channel-delay">
<%= data.oca.DelayD.parameters[0].toFixed(1) %>ms
</div>
</div>
</td>
<td class="hide-medium">
<div class="gain-box">
<div class="gain-head"></div>
<span><%= data.oca.InputGainD.parameters[0].toFixed(1) %>db</span>
</div>
</td>
<td><div class="button light-button mute mute-<%=data.oca.MuteD.parameters[0]%>"><h3>D</h3><%= data.oca.SpeakerIDList.parameters[data.oca.SpeakerIDD.parameters[0]] %></div></td>
</tr>
</table>
+77 -80
View File
@@ -1,83 +1,80 @@
<tbody id="<%= q[0].uid %>">
<% for(var partNumber in q){ %>
<% var part = q[partNumber] %>
<%if(part.scene.length>0 && partNumber==0){ %>
<tr>
<td></td>
<td colspan="12" class="scene">
<hr />
<span><%= part.scene %></span>
</td>
</tr>
<% } %>
<% for(var partNumber in q){ %>
<% var part = q[partNumber] %>
<% if(part.scene.length>0 && partNumber==0){ %>
<tr>
<td></td>
<td colspan="12" class="scene"><hr><span><%= part.scene %></span></td>
</tr>
<% } %>
<% if(isActive){ %>
<tr class="active-cue scroll-position">
<% }else{ %>
<tr >
<% } %>
<% if(partNumber!=0){ %>
<td class="num" style="padding-left: 20px;"><div>P<%= partNumber %></div></td>
<% }else{ %>
<td class="num"><div><%= cueNumber %></div></td>
<% } %>
<% if(partNumber!=0 || part.partcount==0){ %>
<!-- Int Up/Int Down -->
<% if(part.uptimeduration == part.downtimeduration || part.downtimeduration==-1){ %>
<td class="black" colspan="2"><%= prettyDuration(part.uptimedelay) || "" %><%= prettyDuration(part.uptimeduration, true) %></td>
<% }else{ %>
<td class="black"><%= prettyDuration(part.uptimedelay) || "" %><%= prettyDuration(part.uptimeduration, true) %></td>
<td class="black "><%= prettyDuration(part.downtimeduration, true) %></td>
<% } %>
<td class="black hide-medium"><%= prettyDuration(part.focustimeduration, true) %></td>
<td class="black hide-medium"><%= prettyDuration(part.colortimeduration, true) %></td>
<td class="black hide-medium"><%= prettyDuration(part.beamtimeduration, true) %></td>
<td class="hide-small"><%= prettyDuration(part.duration) %></td>
<% }else{ %>
<td class="black" colspan="2"></td>
<td class="black hide-medium"></td>
<td class="black hide-medium"></td>
<td class="black hide-medium"></td>
<td class="hide-small"></td>
<% } %>
<td class="black hide-small"><%= part.mark %></td>
<td class="black hide-small"><%= part.block %></td>
<td class="black hide-small"><%= part.assert %></td>
<td class="black hide-small">
<%= (part.follow>=0)? "F"+prettyDuration(part.follow) : "" %>
<%= (part.hang>=0)? "H"+prettyDuration(part.hang) : "" %>
</td>
<td class="black" style="text-align: left">
<%= (partNumber==0 ? "": "&nbsp;&nbsp;&nbsp;") %>
<%= part.label %>
</td>
<td class="black hide-medium"><%= part.extlinks %></td>
</tr>
<% } %>
</tbody>
<%
function prettyDuration(milliseconds, box){
if(milliseconds==-1){
return "";
}
var num = Math.round(milliseconds/100)/10;
if(box){
return "<div class='time'>"+num+"</div>";
}
return num;
<% if(isActive){ %>
<tr class="active-cue scroll-position">
<% }else{ %>
</tr>
<tr>
<% } %>
}
%>
<% if(partNumber!=0){ %>
<td class="num" style="padding-left: 20px">
<div>P<%= partNumber %></div>
</td>
<% }else{ %>
<td class="num"><div><%= cueNumber %></div></td>
<% } %>
<% if(partNumber !== 0 || part.partCount === 0){ %>
<!-- Int Up/Int Down -->
<% if(part.uptimeDuration === part.downtimeDuration || part.downtimeDuration === -1){ %>
<td class="black" colspan="2">
<%= part.prettyDuration(part.uptimeDelay) || "" %>
<%= part.prettyDuration(part.uptimeDuration, true) %>
</td>
<% }else{ %>
<td class="black">
<%= part.prettyDuration(part.uptimeDelay) || "" %>
<%= part.prettyDuration(part.uptimeDuration, true) %>
</td>
<td class="black">
<%= part.prettyDuration(part.downtimeDuration, true) %>
</td>
<% } %>
<td class="black hide-medium">
<%= part.prettyDuration(part.focusTimeDuration, true) %>
</td>
<td class="black hide-medium">
<%= part.prettyDuration(part.colorTimeDuration, true) %>
</td>
<td class="black hide-medium">
<%= part.prettyDuration(part.beamTimeDuration, true) %>
</td>
<td class="hide-small"><%= part.prettyDuration(part.duration) %></td>
<% }else{ %>
<td class="black" colspan="2"></td>
<td class="black hide-medium"></td>
<td class="black hide-medium"></td>
<td class="black hide-medium"></td>
<td class="hide-small"></td>
<% } %>
<td class="black hide-small"><%= part.mark %></td>
<td class="black hide-small"><%= part.block %></td>
<td class="black hide-small"><%= part.assert %></td>
<td class="black hide-small">
<%= (part.follow>=0)? "F"+part.prettyDuration(part.follow) : "" %>
<%= (part.hang>=0)? "H"+part.prettyDuration(part.hang) : "" %>
</td>
<td class="black" style="text-align: left">
<%= (partNumber==0 ? "": "&nbsp;&nbsp;&nbsp;") %>
<%= part.label %>
</td>
<td class="black hide-medium"><%= part.extLinks %></td>
</tr>
<% } %>
</tbody>
+40
View File
@@ -0,0 +1,40 @@
/* eslint-disable class-methods-use-this */
class Cue {
constructor(args) {
this.uid = args[1];
this.label = args[2];
this.uptimeDuration = args[3];
this.uptimeDelay = args[4];
this.downtimeDuration = args[5];
this.downtimeDelay = args[6];
this.focusTimeDuration = args[7];
this.focusTimeDelay = args[8];
this.colorTimeDuration = args[9];
this.colorTimeDelay = args[10];
this.beamTimeDuration = args[11];
this.beamTimeDelay = args[12];
this.mark = args[16];
this.block = args[17];
this.assert = args[18];
this.follow = args[20];
this.hang = args[21];
this.partCount = args[26];
this.scene = args[28];
this.duration = Math.max(args[3], args[5], args[7], args[9], args[11]);
}
prettyDuration(milliseconds, box) {
if (milliseconds === -1) {
return '';
}
const num = Math.round(milliseconds / 100) / 10;
if (box) {
return `<div class="time">${num}</div>`;
}
return num;
}
}
module.exports = Cue;
+7 -6
View File
@@ -2,14 +2,15 @@
<h4>Shell/ECU &rarr; Network</h4>
<ul>
<li>Enable <em>&#10004; TCP OSC</em>. The TCP format dropdown does not matter.</li>
<li>Enable <em>Third Party OSC</em></li>
<li>Enable <em>&#10004; TCP OSC</em>. The TCP format should be "TCP Format for OSC 1.1 (SLIP)".</li>
<li>Enable <em>Third Party OSC</em></li>
</ul>
If the "Third Party OSC" option is not available, Eos must be updated to 3.1 or newer.
<h4>System Settings &rarr; Show Control &rarr; OSC</h4>
<ul>
<li>Enable <em>&#10004; OSC RX</em></li>
<li>Enable <em>&#10004; OSC TX</em></li>
<li>All other fields may be left to defaults or blank.</li>
</ul>
<li>Enable <em>&#10004; OSC RX</em></li>
<li>Enable <em>&#10004; OSC TX</em></li>
<li>All other fields may be left to defaults or blank.</li>
<li>If a custom port is desired specify it in under OSC TCP Server Ports.</li>
</ul>
+87 -129
View File
@@ -1,180 +1,138 @@
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const Cue = require('./cue');
exports.defaultName = 'ETC Eos';
exports.connectionType = 'osc';
exports.searchOptions = {
type: 'TCPport',
searchBuffer: Buffer.from(
'\xc0/eos/ping\x00\x00\x2c\x00\x00\x00\xc0',
'ascii'
),
testPort: 3032,
validateResponse (msg, info) {
return msg.toString().indexOf('/eos/out');
exports.config = {
defaultName: 'ETC Eos',
connectionType: 'osc',
defaultPort: 3037,
mayChangePort: true,
heartbeatInterval: 5000,
heartbeatTimeout: 6000,
searchOptions: {
type: 'TCPport',
searchBuffer: Buffer.from('\xc0/eos/ping\x00\x00\x2c\x00\x00\x00\xc0', 'ascii'),
testPort: 3037,
validateResponse(msg, info) {
return msg.toString().includes('/eos/out');
},
},
};
exports.defaultPort = 3037;
exports.ready = function ready(device) {
exports.ready = function ready(_device) {
const device = _device;
device.data.EOS = new EOS();
device.templates = {
cue: _.template(fs.readFileSync(path.join(__dirname, `cue.ejs`))),
};
device.send('/eos/get/cuelist/count');
device.send('/eos/get/version');
device.send('/eos/subscribe', [{ type: 'i', value: 1 }]);
};
exports.data = function data(device, osc) {
const p = osc.address.split('/');
const d = device;
p.shift();
exports.data = function data(_device, osc) {
const device = _device;
this.deviceInfoUpdate(device, 'status', 'ok');
// console.log(osc.address)
const addressParts = osc.address.split('/');
addressParts.shift();
if (match(p, ['eos','out','show','name'])) {
d.data.showName = osc.args[0];
d.data.cuelists = {};
if (match(addressParts, ['eos', 'out', 'show', 'name'])) {
device.data.EOS.showName = osc.args[0];
device.data.EOS.cueLists = {};
this.deviceInfoUpdate(device, 'defaultName', osc.args[0]);
} else if (match(p, ['eos','out','get','cuelist','count'])) {
} else if (match(addressParts, ['eos', 'out', 'get', 'cuelist', 'count'])) {
for (let i = 0; i < osc.args[0]; i++) {
d.send(`/eos/get/cuelist/index/${i}`);
device.send(`/eos/get/cuelist/index/${i}`);
}
} else if (match(p, ['eos','out','get','cuelist','*','list','*','*'])) {
d.data.cuelists[p[4]] = {};
d.send(`/eos/get/cue/${p[4]}/count`);
} else if (match(p, ['eos','out','get','cue','*','count'])) {
} else if (match(addressParts, ['eos', 'out', 'get', 'cuelist', '*', 'list', '*', '*'])) {
device.data.EOS.cueLists[addressParts[4]] = {};
device.send(`/eos/get/cue/${addressParts[4]}/count`);
} else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', 'count'])) {
for (let i = 0; i < osc.args[0]; i++) {
device.send(`/eos/get/cue/${p[4]}/index/${i}`);
device.send(`/eos/get/cue/${addressParts[4]}/index/${i}`);
}
} else if (match(p, ['eos','out','get','cue','*','*','*','list','*','*'])) {
this.deviceInfoUpdate(device, 'status', 'ok');
if (d.data.cuelists[p[4]][p[5]] === undefined) {
d.data.cuelists[p[4]][p[5]] = {};
} else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', '*', '*', 'list', '*', '*'])) {
if (device.data.EOS.cueLists[addressParts[4]] === undefined) {
device.data.EOS.cueLists[addressParts[4]] = {};
device.send(`/eos/get/cue/${addressParts[4]}/count`);
}
d.data.cuelists[p[4]][p[5]][p[6]] = {
if (device.data.EOS.cueLists[addressParts[4]][addressParts[5]] === undefined) {
device.data.EOS.cueLists[addressParts[4]][addressParts[5]] = {};
}
device.data.EOS.cueLists[addressParts[4]][addressParts[5]][addressParts[6]] = new Cue(osc.args);
device.update('cueData', {
cue: device.data.EOS.cueLists[addressParts[4]][addressParts[5]],
cueNumber: addressParts[5],
uid: osc.args[1],
label: osc.args[2],
uptimeduration: osc.args[3],
uptimedelay: osc.args[4],
downtimeduration: osc.args[5],
downtimedelay: osc.args[6],
focustimeduration: osc.args[7],
focustimedelay: osc.args[8],
colortimeduration: osc.args[9],
colortimedelay: osc.args[10],
beamtimeduration: osc.args[11],
beamtimedelay: osc.args[12],
mark: osc.args[16],
block: osc.args[17],
assert: osc.args[18],
follow: osc.args[20],
hang: osc.args[21],
partcount: osc.args[26],
scene: osc.args[28],
duration: Math.max(
osc.args[3],
osc.args[5],
osc.args[7],
osc.args[9],
osc.args[11]
),
};
device.update("cueData", {
cue: device.data.cuelists[p[4]][p[5]],
cueNumber: p[5],
uid: osc.args[1]
});
} else if (match(p, ['eos','out','get','cue','*','*'])) {
} else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', '*'])) {
// There's no OSC notification of the deletion of a part. It just tells you to update the parent cue and remaining children.
// So: we should fetch all the parts of a cue somehow every time there's an update to a cue.
delete d.data.cuelists[p[4]][p[5]];
d.draw();
} else if (match(p, ['eos','out','get','cue','*','*','*','actions','list','*','*'])) {
delete device.data.EOS.cueLists[addressParts[4]][addressParts[5]];
device.draw();
} else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', '*', '*', 'actions', 'list', '*', '*'])) {
if (osc.args.length === 3) {
d.data.cuelists[p[4]][p[5]][0].extlinks = osc.args[2];
device.data.EOS.cueLists[addressParts[4]][addressParts[5]][0].extLinks = osc.args[2];
}
} else if (match(p, ['eos','out','event','cue','*','*','fire'])) {
d.data.activeCue = p[5];
d.draw();
d.update("activeCue", {uid: device.data.cuelists[p[4]][p[5]][0].uid});
} else if (match(p, ['eos','out','notify','cue','*','*','*','*'])) {
const cueList = p[4];
} else if (match(addressParts, ['eos', 'out', 'event', 'cue', '*', '*', 'fire'])) {
device.data.EOS.activeCue = addressParts[5];
device.draw();
device.update('activeCue', {
uid: device.data.EOS.cueLists[addressParts[4]][addressParts[5]][0].uid,
});
} else if (match(addressParts, ['eos', 'out', 'notify', 'cue', '*', '*', '*', '*'])) {
const cueList = addressParts[4];
const cueNumber = osc.args[1];
d.send(`/eos/get/cue/${cueList}/${cueNumber}`);
} else if (match(p, ['eos','out','get','version'])) {
d.data.version = osc.args[0];
device.send(`/eos/get/cue/${cueList}/${cueNumber}`);
} else if (match(addressParts, ['eos', 'out', 'get', 'version'])) {
device.data.EOS.version = osc.args[0];
} else {
// console.log(osc);
}
// console.log(p)
};
const cueTemplate = _.template(fs.readFileSync(`./plugins/eos/cue.ejs`));
exports.update = function update(device, doc, updateType, data){
if(updateType === "cueData"){
exports.update = function update(device, doc, updateType, data) {
if (updateType === 'cueData') {
const $elem = doc.getElementById(data.uid);
if($elem){
$elem.outerHTML = cueTemplate({
if ($elem) {
$elem.outerHTML = device.templates.cue({
q: data.cue,
cueNumber: data.cueNumber,
isActive: false
isActive: false,
});
}else{
} else {
device.draw();
}
}else if(updateType === "activeCue"){
} else if (updateType === 'activeCue') {
const $elem = doc.getElementById(data.uid);
$elem.scrollIntoView({behavior: "smooth", block: "center"});
$elem.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
};
exports.heartbeat = function heartbeat(device) {
device.send('/eos/ping');
};
function match(osc, array){
function match(testArray, patternArray) {
let out = true;
if(osc.length !== array.length){
if (testArray.length !== patternArray.length) {
return false;
}
array.forEach((m, i)=> {
if(osc[i] !== m && m !== "*"){
patternArray.forEach((patternPart, i) => {
if (testArray[i] !== patternPart && patternPart !== '*') {
out = false;
}
});
return out;
}
}
class EOS {
constructor() {
this.version = '';
this.showName = '';
this.cueLists = {};
this.activeCue = undefined;
}
}
+11
View File
@@ -3,6 +3,7 @@ table {
border: #404040 1px solid;
border-radius: 4px;
color: #a59baa;
width: 100%;
}
th {
font-size: 14px;
@@ -62,6 +63,16 @@ tr.active-cue .time {
border-color: #c78b07;
}
.list_name {
color: lightgray;
margin-left: 5px;
margin-bottom: 2px;
}
.list_container {
margin-bottom: 10px;
}
@media screen and (min-width: 0px) and (max-width: 750px) {
.hide-medium {
display: none;
+32 -36
View File
@@ -3,44 +3,40 @@
<h2>Eos <%= data.version %></h2>
</header>
<%
const fs = require('fs');
let cueTemplate = _.template(fs.readFileSync(`./plugins/eos/cue.ejs`));
%>
<% for(var i in data.EOS.cueLists){ %>
<div class="list_container">
<div class="list_name">List <%= i %></div>
<table cellspacing="0">
<tr>
<th width="70px">Cue</th>
<th width="30px">Int Up</th>
<th width="30px">Int Down</th>
<th width="40px" class="hide-medium">Focus</th>
<th width="40px" class="hide-medium">Color</th>
<th width="40px" class="hide-medium">Beam</th>
<th width="40px" class="hide-small">Dur</th>
<th width="20px" class="hide-small">M</th>
<th width="20px" class="hide-small">B</th>
<th width="20px" class="hide-small">A</th>
<th width="70px" class="hide-small">Fw/Hg</th>
<th width="150px">Label</th>
<th width="10px" class="hide-medium">Ext Links</th>
</tr>
<% var cues = Object.keys(data.EOS.cueLists[i]).sort(function(a, b){return Number(a)-Number(b)}) %>
<% for(var i in data.cuelists){ %>
<strong>List <%= i %></strong>
<table cellspacing="0">
<tr>
<th width="70px">Cue</th>
<th width="30px">Int Up</th>
<th width="30px">Int Down</th>
<th width="40px" class="hide-medium">Focus</th>
<th width="40px" class="hide-medium">Color</th>
<th width="40px" class="hide-medium">Beam</th>
<th width="40px" class="hide-small">Dur</th>
<th width="20px" class="hide-small">M</th>
<th width="20px" class="hide-small">B</th>
<th width="20px" class="hide-small">A</th>
<th width="70px" class="hide-small">Fw/Hg</th>
<th width="150px">Label</th>
<th width="10px" class="hide-medium">Ext Links</th>
</tr>
<% for(var j=0; j<cues.length; j++){ %>
<% var q = data.EOS.cueLists[i][cues[j]] %>
<%= templates.cue({
q: q,
cues: cues,
cueNumber: cues[j],
isActive: (cues[j]==data.EOS.activeCue+"")
}) %>
<% var cues = Object.keys(data.cuelists[i]).sort(function(a, b){return Number(a)-Number(b)}) %>
<% for(var j=0; j<cues.length; j++){ %>
<% var q = data.cuelists[i][cues[j]] %>
<%= cueTemplate({
q: q,
cues: cues,
cueNumber: cues[j],
isActive: (cues[j]==data.activeCue+"")
}) %>
<% } %>
</table>
<% } %>
</table>
</div>
<% } %>
+2 -2
View File
@@ -1,4 +1,4 @@
<h3>Connection Requirements</h3>
<ul>
<li>Requires no password on the projector</li>
</ul>
<li>Requires no password on the projector</li>
</ul>
+124 -90
View File
@@ -1,126 +1,160 @@
const md5 = require('md5');
exports.defaultName = 'PJLink Projector';
exports.connectionType = 'TCPsocket';
exports.heartbeatInterval = 5000;
exports.heartbeatTimeout = 15000;
exports.searchOptions = {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x25, 0x32, 0x53, 0x52, 0x43, 0x48, 0x0d]),
devicePort: 4352,
listenPort: 4352,
validateResponse (msg, info) {
return msg.toString().indexOf('%2ACKN=') >= 0;
exports.config = {
defaultName: 'PJLink Projector',
connectionType: 'TCPsocket',
defaultPort: 4352,
mayChangePort: false,
heartbeatInterval: 5000,
heartbeatTimeout: 15000,
searchOptions: {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x25, 0x32, 0x53, 0x52, 0x43, 0x48, 0x0d]),
devicePort: 4352,
listenPort: 4352,
validateResponse(msg, info) {
return msg.toString().includes('%2ACKN=');
},
},
fields: [
{
key: 'password',
label: 'Pass',
type: 'textinput',
value: '',
action(device) {
device.plugin.heartbeat(device);
},
},
],
};
exports.defaultPort = 4352;
exports.ready = function ready(device) {
// Power status query
// device.send("%1POWR ?\r");
};
let password = false;
function processPJLink(device, str, that) {
const PJLinkCmds = [
'%1POWR=',
'%1INPT=',
'%1AVMT=',
'%1ERST=',
'%1LAMP=',
'%1NAME=',
'%1INF1=',
'%1INF2=',
'%2SNUM=',
'%2SVER=',
];
let passwordMD5 = false;
let passwordSeed = false;
function processPJLink(_device, str, that) {
const arr = str.split('%');
arr.shift();
const d = device;
const device = _device;
arr.forEach((s) => {
const split = s.split('=');
const key = split[0];
const value = split[1];
arr.forEach((response) => {
const parts = response.split('=');
const cmd = parts[0];
const value = parts[1];
if(key === '1POWR'){
d.data.power = value;
switch (cmd) {
case '1POWR':
device.data.power = value;
break;
}else if(key === '1INPT'){
d.data.input = value;
}else if(key === '1AVMT'){
d.data.avmute = value;
}else if(key === '1ERST'){
d.data.fanError = value[0];
d.data.lampError = value[1];
d.data.tempError = value[2];
d.data.coverError = value[3];
d.data.filterError = value[4];
d.data.otherError = value[5];
}else if(key === '1LAMP'){
d.data.lamp = value.split(' ');
}else if(key === '1NAME'){
d.data.name = value;
that.deviceInfoUpdate(d, 'defaultName', d.data.name);
}else if(key === '1INF1'){
d.data.info1 = value;
}else if(key === '1INF2'){
d.data.info2 = value;
}else if(key === '2SNUM'){
d.data.serial = value;
}else if(key === '2SVER'){
d.data.version = value;
case '1INPT':
device.data.input = value;
break;
case '1AVMT':
device.data.avmute = value;
break;
case '1ERST':
device.data.fanError = value[0];
device.data.lampError = value[1];
device.data.tempError = value[2];
device.data.coverError = value[3];
device.data.filterError = value[4];
device.data.otherError = value[5];
break;
case '1LAMP':
device.data.lamp = value.split(' ');
break;
case '1NAME':
device.data.name = value;
that.deviceInfoUpdate(device, 'defaultName', device.data.name);
break;
case '1INF1':
device.data.info1 = value;
break;
case '1INF2':
device.data.info2 = value;
break;
case '2SNUM':
device.data.serial = value;
break;
case '2SVER':
device.data.version = value;
break;
default:
break;
}
});
d.draw();
device.draw();
}
exports.data = function data(device, message) {
exports.data = function data(_device, message) {
const device = _device;
this.deviceInfoUpdate(device, 'status', 'ok');
const msg = message.toString();
if (msg.substring(0, 8) === 'PJLINK 1') {
password = md5(`${msg.substring(9, 17)}JBMIAProjectorLink`);
passwordSeed = msg.substring(9, 17);
passwordMD5 = md5(`${passwordSeed}${device.fields.password}`);
device.data.authentication = 'ON';
device.send(
`${password}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r`
`${passwordMD5}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r`
);
device.draw();
} else if (msg.substring(0, 8) === 'PJLINK 0') {
device.data.authentication = 'OFF';
device.draw();
} else if (msg.startsWith('PJLINK ERRA')) {
device.data.passwordOK = false;
device.draw();
}
if (msg.substring(0, 7) === '%1POWR=') {
processPJLink(device, msg, this);
}
if (msg.substring(0, 7) === '%1INPT=') {
processPJLink(device, msg, this);
}
if (msg.substring(0, 7) === '%1AVMT=') {
processPJLink(device, msg, this);
}
if (msg.substring(0, 7) === '%1ERST=') {
processPJLink(device, msg, this);
}
if (msg.substring(0, 7) === '%1LAMP=') {
processPJLink(device, msg, this);
}
if (msg.substring(0, 7) === '%1NAME=') {
processPJLink(device, msg, this);
}
if (msg.substring(0, 7) === '%1INF1=') {
processPJLink(device, msg, this);
}
if (msg.substring(0, 7) === '%1INF2=') {
processPJLink(device, msg, this);
}
if (msg.substring(0, 7) === '%2SNUM=') {
processPJLink(device, msg, this);
}
if (msg.substring(0, 7) === '%2SVER=') {
if (PJLinkCmds.includes(msg.substring(0, 7))) {
processPJLink(device, msg, this);
device.data.passwordOK = true;
}
};
exports.heartbeat = function heartbeat(device) {
if (password) {
passwordMD5 = md5(`${passwordSeed}${device.fields.password}`);
if (device.fields.password.length > 0 && device.data.passwordOK) {
device.send(
`${password}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r`
`${passwordMD5}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r`
);
} else if (device.fields.password.length > 0) {
device.send(`${passwordMD5}%1POWR ?\r`);
} else if (device.data.passwordOK) {
device.send(`%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r`);
} else {
device.send(`%1POWR ?\r`);
}
device.draw();
};
+4
View File
@@ -7,3 +7,7 @@
.ok {
color: #79b757;
}
table {
margin-bottom: 30px;
width: 350px;
}
+15 -2
View File
@@ -2,7 +2,6 @@
<h1><%= listName %></h1>
</header>
<table class="cv-table">
<tr>
<td>Power</td>
@@ -73,7 +72,6 @@
<% } %>
<% } %>
<tr>
<td>Name</td>
<td><div><%= data.name %></div></td>
@@ -94,4 +92,19 @@
<td>Version</td>
<td><div><%= data.version %></div></td>
</tr>
</table>
<table class="cv-table">
<tr>
<td>Authentication</td>
<td>
<% if(!data.passwordOK){ %>
<div class='error'>INCORRECT PASSWORD</div>
<% }else if(data.authentication=="ON"){ %>
<div class='ok'>ON</div>
<% }else{ %>
<div class='error'><%= data.authentication %></div>
<% } %>
</td>
</tr>
</table>
+36 -24
View File
@@ -5,52 +5,64 @@
<td><center>
<% if(cue.broken){ %>
<img src="plugins/qlab/img/status_broken.png" height="18px">
<img src="./plugins/qlab/img/status_broken.png" height="18px">
<% }else if(cue.running){ %>
<img src="plugins/qlab/img/status_running.png" height="18px">
<img src="./plugins/qlab/img/status_running.png" height="18px">
<% }else if(cue.paused){ %>
<img src="plugins/qlab/img/status_paused.png" height="18px">
<img src="./plugins/qlab/img/status_paused.png" height="18px">
<% }else if(cue.loaded){ %>
<img src="plugins/qlab/img/status_loaded.png" height="18px">
<img src="./plugins/qlab/img/status_loaded.png" height="18px">
<% }else if(cue.flagged){ %>
<img src="plugins/qlab/img/status_flagged.png" height="20px">
<img src="./plugins/qlab/img/status_flagged.png" height="20px">
<% }else{ %>
<% } %>
</center></td>
<% if(cue.type == "Group" && cue.groupMode){ %>
<td><img src="./plugins/qlab/img/v5/group-<%= cue.groupMode %>.png" height="20px"></td>
<% }else{ %>
<td><img src="./plugins/qlab/img/<%= cue.type.toLowerCase().replace(' ','-') %>.png" height="20px"></td>
<% } %>
<td><img src="plugins/qlab/img/<%= cue.type.toLowerCase().replace(' ','-') %>.png" height="20px"></td>
<td><center><%= cue.number || "&nbsp;" %></center></td>
<% for(let i=0; i<cue.nestedGroupModes.length; i++){ %>
<% if(cue.cues && cue.cues.length==0 && i==cue.nestedGroupModes.length-1){ %>
<td class="gMode-<%= cue.nestedGroupModes[i] %> gLeft gBot gTop group-arrow"></td>
<% }else if(cue.cues && i==cue.nestedGroupModes.length-1){ %>
<td class="gMode-<%= cue.nestedGroupModes[i] %> gLeft gTop group-arrow"></td>
<% for(let i=cue.nestedGroupModes.length; i>0; i--){ %>
<% }else if(cue.nestedGroupPosition[i]<=0){ %>
<% if(i<cue.nestedGroupModes.length-1){ %>
<td class="gMode-<%= cue.nestedGroupModes[i] %> gLeft gBot"></td>
<% let test = "" %>
<% }else{ %>
<td class="gMode-<%= cue.nestedGroupModes[i] %> gBot"></td>
<% if(cue.type=="Group" && i==1 && cue.cues.length==0){ %>
<td class="gMode-<%= cue.nestedGroupModes[i-1] %> gLeft gTop gBot group-arrow"><%= test %></td>
<% } %>
<% }else if(cue.type=="Group" && i==1){ %>
<td class="gMode-<%= cue.nestedGroupModes[i-1] %> gLeft gTop group-arrow"><%= test %></td>
<% }else if(i<cue.nestedGroupModes.length-1){ %>
<td class="gMode-<%= cue.nestedGroupModes[i] %> gLeft"></td>
<% }else if(cue.nestedGroupPosition[i-1]==0 && cue.nestedGroupPosition[i-2]==0){ %>
<td class="gMode-<%= cue.nestedGroupModes[i-1] %> gLeft gBot"><%= test %></td>
<% }else{ %>
<td class="gMode-<%= cue.nestedGroupModes[i] %>"></td>
<% }else if(i==1 && cue.nestedGroupPosition[i]==0){ %>
<td class="gMode-<%= cue.nestedGroupModes[i] %> gBot"><%= test %></td>
<% }else{ %>
<td class="gMode-<%= cue.nestedGroupModes[i-1] %> gLeft"><%= test %></td>
<% } %>
<% } %>
<% } %>
@@ -69,10 +81,10 @@
<% } %>
<% }else if(cue.nestedGroupPosition[1]==0){ %>
<td colspan="<%= 11-cue.nestedGroupModes.length %>" class="cue-name gMode-<%= cue.nestedGroupModes[1] %> gBot gRight"><%= cue.listName %></td>
<td colspan="<%= 11-cue.nestedGroupModes.length %>" class="cue-name gMode-<%= cue.nestedGroupModes[cue.nestedGroupModes.length-1] %> gBot gRight"><%= cue.listName %></td>
<% }else{ %>
<td colspan="<%= 11-cue.nestedGroupModes.length %>" class="cue-name gMode-<%= cue.nestedGroupModes[1] %> gRight"><%= cue.listName %></td>
<td colspan="<%= 11-cue.nestedGroupModes.length %>" class="cue-name gMode-<%= cue.nestedGroupModes[cue.nestedGroupModes.length-1] %> gRight"><%= cue.listName %></td>
<% } %>
@@ -83,7 +95,7 @@
<% if(cue.preWait){ %>
<td class="q-time hide-medium"><%= elapsedTime(cue.preWait, cue.preWaitElapsed, "preWait", cue) %></td>
<td class="q-time hide-small"><%= elapsedTime(cue.preWait, cue.preWaitElapsed, "preWait", cue) %></td>
<% }else{ %>
<td class="q-time q-gray-text hide-small">00:00.00</td>
+2 -4
View File
@@ -40,13 +40,11 @@
function displayCueRow(q){
let html = "";
html+= rowTemplate({cue: allCues[q.uniqueID], allCues: allCues, workspace: workspace});
let html = rowTemplate({cue: allCues[q.uniqueID], allCues: allCues, workspace: workspace});
if(q.cues){
q.cues.forEach(q =>{
html+=displayCueRow(q);
html += displayCueRow(q);
})
}
return html;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+3 -2
View File
@@ -1,4 +1,5 @@
<h3>Connection Requirements</h3>
<ul>
<li>No Passcode</li>
</ul>
<li><em>View</em> permission enabled in OSC Access</li>
<li>Provide passcode if necessary</li>
</ul>
+275 -240
View File
@@ -1,121 +1,138 @@
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
let lastElapsedUpdate = Date.now();
exports.defaultName = 'QLab 4';
exports.connectionType = 'osc';
exports.heartbeatInterval = 50;
exports.heartbeatTimeout = 2000;
exports.searchOptions = {
type: 'Bonjour',
bonjourName: 'qlab',
exports.config = {
defaultName: 'QLab',
connectionType: 'osc',
defaultPort: 53000,
mayChangePort: true,
heartbeatInterval: 100,
heartbeatTimeout: 5000,
searchOptions: {
type: 'Bonjour',
bonjourName: 'qlab',
},
fields: [
{
key: 'passcode',
label: 'Pass',
type: 'textinput',
value: '',
action(device) {
device.send('/workspaces');
},
},
],
};
exports.ready = function ready(device) {
let lastElapsedUpdate = Date.now();
let interval = 5;
let heartbeatCount = 0;
const valuesForKeysString =
'["uniqueID","number","name","listName","isBroken","isRunning","isLoaded","isFlagged",' +
'"type","children","preWait","postWait","currentDuration","colorName","continueMode",' +
'"mode","parent","cartRows","cartColumns","cartPosition","displayName","preWaitElapsed",' +
'"actionElapsed","postWaitElapsed","isPaused"]';
exports.ready = function ready(_device) {
const device = _device;
device.templates = {
cue: _.template(fs.readFileSync(path.join(__dirname, `cue.ejs`))),
tile: _.template(fs.readFileSync(path.join(__dirname, `tile.ejs`))),
cart: _.template(fs.readFileSync(path.join(__dirname, `cart.ejs`))),
cuelist: _.template(fs.readFileSync(path.join(__dirname, `cuelist.ejs`))),
};
device.send(`/version`);
device.send('/workspaces');
};
exports.data = function data(device, oscData) {
const osc = oscData.address.split('/');
osc.shift();
const d = device;
// console.log(oscData.address)
exports.data = function data(_device, oscData) {
const device = _device;
if(match(osc, ["reply", "version"])){
const oscAddressParts = oscData.address.split('/');
oscAddressParts.shift();
if (match(oscAddressParts, ['reply', 'version'])) {
const json = JSON.parse(oscData.args[0]);
d.data.version = json.data;
device.data.version = json.data;
this.deviceInfoUpdate(d, 'status', 'ok');
}else if(match(osc, ["reply", "workspaces"])){
this.deviceInfoUpdate(device, 'status', 'ok');
} else if (match(oscAddressParts, ['reply', 'workspaces'])) {
const json = JSON.parse(oscData.args[0]);
d.data.workspaces = {};
device.data.workspaces = {};
json.data.forEach(wksp => {
d.data.workspaces[wksp.uniqueID] = {
json.data.forEach((wksp) => {
device.data.workspaces[wksp.uniqueID] = {
uniqueID: wksp.uniqueID,
displayName: wksp.displayName,
cueLists: {},
cues: {}
}
d.send(`/workspace/${wksp.uniqueID}/connect`);
cues: {},
};
device.send(`/workspace/${wksp.uniqueID}/connect`, device.fields.passcode);
});
}else if(match(osc, ["reply", "workspace", "*", "connect"])){
d.send(`/workspace/${osc[2]}/updates`, [
{ type: 'i', value: 1 },
]);
d.send(`/workspace/${osc[2]}/cueLists`);
}else if(match(osc, ["reply", "workspace", "*", "cueLists"])){
this.deviceInfoUpdate(d, 'status', 'ok');
const workspace = d.data.workspaces[osc[2]];
if (Object.keys(device.data.workspaces).length === 0) {
device.data.permission = 'no workspaces';
device.draw();
}
} else if (match(oscAddressParts, ['reply', 'workspace', '*', 'connect'])) {
device.send(`/workspace/${oscAddressParts[2]}/updates`, [{ type: 'i', value: 1 }]);
device.send(`/workspace/${oscAddressParts[2]}/cueLists`);
} else if (match(oscAddressParts, ['reply', 'workspace', '*', 'cueLists'])) {
this.deviceInfoUpdate(device, 'status', 'ok');
const workspace = device.data.workspaces[oscAddressParts[2]];
const json = JSON.parse(oscData.args[0]);
workspace.cueLists = {};
workspace.cues = {};
if (json.status === 'denied') {
device.data.permission = 'denied';
device.draw();
} else if (json.data) {
device.data.permission = 'ok';
json.data.forEach(ql => {
workspace.cueLists[ql.uniqueID] = ql;
addChildrenToWorkspace(workspace, ql);
});
d.draw();
setTimeout(() => {
json.data.forEach(ql => {
workspace.cueLists[ql.uniqueID] = ql;
getValuesForKeys(d, json.workspace_id, ql);
json.data.forEach((cueList) => {
workspace.cueLists[cueList.uniqueID] = cueList;
addCueToWorkspace(workspace, cueList);
});
}, 0);
device.draw();
}else if(match(osc, ["reply", "cue_id", "*", "children"])){
setTimeout(() => {
json.data.forEach((ql) => {
workspace.cueLists[ql.uniqueID] = ql;
getValuesForKeys(device, json.workspace_id, ql);
});
}, 0);
}
} else if (
match(oscAddressParts, ['reply', 'cue_id', '*', 'children']) ||
match(oscAddressParts, ['reply', 'workspace', '*', 'cue_id', '*', 'children'])
) {
const json = JSON.parse(oscData.args[0]);
const workspace = d.data.workspaces[json.workspace_id];
const cueID = osc[2];
const workspace = device.data.workspaces[json.workspace_id];
const cueID = json.address.substring(55, 91);
const cue = workspace.cues[cueID];
if(!_.isEqual(cue.cues, json.data)){
if (!_.isEqual(cue.cues, json.data)) {
workspace.cueLists[cueID].cues = json.data;
addChildrenToWorkspace(workspace, workspace.cueLists[cueID]);
d.draw();
getValuesForKeys(d, json.workspace_id, cue);
addCueToWorkspace(workspace, workspace.cueLists[cueID]);
device.draw();
getValuesForKeys(device, json.workspace_id, cue);
}
}else if(match(osc, ["reply", "cue_id", "*", "valuesForKeys"])){
} else if (
match(oscAddressParts, ['reply', 'cue_id', '*', 'valuesForKeys']) ||
match(oscAddressParts, ['reply', 'workspace', '*', 'cue_id', '*', 'valuesForKeys'])
) {
const json = JSON.parse(oscData.args[0]);
const cueValues = json.data;
const workspace = d.data.workspaces[json.workspace_id];
const workspace = device.data.workspaces[json.workspace_id];
let cue = workspace.cues[cueValues.uniqueID];
if(!cue){
if (!cue) {
workspace.cues[cueValues.uniqueID] = {};
cue = workspace.cues[cueValues.uniqueID];
}
@@ -131,7 +148,7 @@ exports.data = function data(device, oscData) {
cue.flagged = cueValues.isFlagged;
cue.paused = cueValues.isPaused;
cue.type = cueValues.type;
cue.cues = cueValues.children;
// cue.cues = cueValues.children;
cue.preWait = cueValues.preWait;
cue.postWait = cueValues.postWait;
cue.duration = cueValues.currentDuration;
@@ -146,236 +163,254 @@ exports.data = function data(device, oscData) {
cue.actionElapsed = cueValues.actionElapsed;
cue.postWaitElapsed = cueValues.postWaitElapsed;
// QLab 5 fix
if (cueValues.type === 'Group' || cueValues.type === 'Cue List') {
cue.cues = cueValues.children;
} else {
cue.cues = undefined;
}
const nestedGroupModes = [];
const nestedGroupPosition = [0];
const nestedGroupPosition = [];
let obj = cue;
let sum = 0;
while(obj.parent !== "[root group of cue lists]"){
nestedGroupModes.push(obj.groupMode);
if (obj.cues) {
sum += obj.cues.length;
}
let pos = _.findIndex(workspace.cues[obj.parent].cues, {uniqueID: obj.uniqueID});
while (obj.parent !== '[root group of cue lists]') {
let pos = _.findIndex(workspace.cues[obj.parent].cues, {
uniqueID: obj.uniqueID,
});
pos = Math.abs(pos - workspace.cues[obj.parent].cues.length) - 1;
nestedGroupPosition.push(pos)
if (obj.cues === undefined) {
sum += pos;
}
nestedGroupPosition.unshift(sum);
if (obj.cues) {
sum += pos;
nestedGroupModes.unshift(obj.groupMode);
} else {
nestedGroupModes.unshift(workspace.cues[obj.parent].groupMode);
}
obj = workspace.cues[obj.parent];
}
cue.nestedGroupModes = nestedGroupModes;
cue.nestedGroupPosition = nestedGroupPosition;
// console.log("draw "+cue.number)
device.update('updateCueData', {
cue,
allCues: workspace.cues,
workspace,
});
} else if (match(oscAddressParts, ['reply', 'cue_id', '*', 'preWaitElapsed'])) {
const oscArgs = JSON.parse(oscData.args[0]);
if (oscArgs.status !== 'error') {
const workspace = device.data.workspaces[oscArgs.workspace_id];
if (workspace) {
const cue = workspace.cues[oscArgs.address.substring(55, 91)];
d.update("updateCueData", {'cue': cue, 'allCues': workspace.cues, 'workspace': workspace});
if (cue) {
cue.preWaitElapsed = oscArgs.data;
lastElapsedUpdate = Date.now();
device.update('updateCueData', {
cue,
allCues: workspace.cues,
workspace,
});
}
}
}
} else if (match(oscAddressParts, ['reply', 'cue_id', '*', 'actionElapsed'])) {
const oscArgs = JSON.parse(oscData.args[0]);
if (oscArgs.status !== 'error') {
const workspace = device.data.workspaces[oscArgs.workspace_id];
if (workspace) {
const cue = workspace.cues[oscArgs.address.substring(55, 91)];
}else if(match(osc, ["reply", "cue_id", "*", "preWaitElapsed"])){
if (cue) {
cue.actionElapsed = oscArgs.data;
lastElapsedUpdate = Date.now();
const json = JSON.parse(oscData.args[0]);
const workspace = d.data.workspaces[json.workspace_id];
const cue = workspace.cues[osc[2]];
device.update('updateCueData', {
cue,
allCues: workspace.cues,
workspace,
});
}
}
}
} else if (match(oscAddressParts, ['reply', 'cue_id', '*', 'postWaitElapsed'])) {
const oscArgs = JSON.parse(oscData.args[0]);
if (oscArgs.status !== 'error') {
const workspace = device.data.workspaces[oscArgs.workspace_id];
if (workspace) {
const cue = workspace.cues[oscArgs.address.substring(55, 91)];
cue.preWaitElapsed = json.data;
lastElapsedUpdate = Date.now();
if (cue) {
cue.postWaitElapsed = oscArgs.data;
lastElapsedUpdate = Date.now();
d.update("updateCueData", {'cue': cue, 'allCues': workspace.cues, 'workspace': workspace});
device.update('updateCueData', {
cue,
allCues: workspace.cues,
workspace,
});
}
}
}
} else if (match(oscAddressParts, ['update', 'workspace', '*', 'cue_id', '*'])) {
const workspace = device.data.workspaces[oscAddressParts[2]];
}else if(match(osc, ["reply", "cue_id", "*", "actionElapsed"])){
const json = JSON.parse(oscData.args[0]);
const workspace = d.data.workspaces[json.workspace_id];
const cue = workspace.cues[osc[2]];
cue.actionElapsed = json.data;
lastElapsedUpdate = Date.now();
d.update("updateCueData", {'cue': cue, 'allCues': workspace.cues, 'workspace': workspace});
}else if(match(osc, ["reply", "cue_id", "*", "postWaitElapsed"])){
const json = JSON.parse(oscData.args[0]);
const workspace = device.data.workspaces[json.workspace_id];
const cue = workspace.cues[osc[2]];
cue.postWaitElapsed = json.data;
lastElapsedUpdate = Date.now();
d.update("updateCueData", {'cue': cue, 'allCues': workspace.cues, 'workspace': workspace});
}else if(match(osc, ["update", "workspace", "*", "cue_id", "*"])){
const workspace = d.data.workspaces[osc[2]];
if(workspace){
if (workspace) {
const cueLists = Object.keys(workspace.cueLists);
const cueID = osc[4];
const cueID = oscAddressParts[4];
if(cueID !== "[root group of cue lists"){
if(cueLists.includes(cueID)){
d.send(`/workspace/${workspace.uniqueID}/cue_id/${cueID}/children`);
if (cueID !== '[root group of cue lists') {
if (cueLists.includes(cueID)) {
device.send(`/workspace/${workspace.uniqueID}/cue_id/${cueID}/children`);
}
d.send(`/workspace/${osc[2]}/cue_id/${cueID}/valuesForKeys`, [
{type: 's', value: valuesForKeysString}
device.send(`/workspace/${oscAddressParts[2]}/cue_id/${cueID}/valuesForKeys`, [
{ type: 's', value: valuesForKeysString },
]);
}
}
}else if(match(osc, ["update", "workspace", "*"])){
} else if (match(oscAddressParts, ['update', 'workspace', '*'])) {
// occurs when cue lists are reordered or a list is deleted
d.send(`/workspace/${osc[2]}/cueLists`);
}else if(match(osc, ["update", "workspace", "*", "dashboard"])){
device.send(`/workspace/${oscAddressParts[2]}/cueLists`);
} else if (match(oscAddressParts, ['update', 'workspace', '*', 'dashboard'])) {
// this workspace might be new, let's check
if(d.data.workspaces[osc[2]]){
//
}else{
console.log("new workspace!");
d.send('/workspaces');
if (device.data.workspaces[oscAddressParts[2]] === undefined) {
device.send('/workspaces');
}
} else if (match(oscAddressParts, ['update', 'workspace', '*', 'cueList', '*', 'playbackPosition'])) {
const workspace = device.data.workspaces[oscAddressParts[2]];
}else if(match(osc, ["update", "workspace", "*", "cueList", "*", "playbackPosition"])){
const workspace = d.data.workspaces[osc[2]];
if(workspace){
if (workspace) {
const cue = workspace.cues[oscData.args[0]];
if(cue){
workspace.playbackPosition = oscData.args[0] ? cue.uniqueID : "";
d.update("updatePlaybackPosition", {'cue': cue});
if (cue) {
workspace.playbackPosition = oscData.args[0] ? cue.uniqueID : '';
device.update('updatePlaybackPosition', { cue });
}
}
}else if(match(osc, ["update", "workspace", "*", "disconnect"])){
delete d.data.workspaces[osc[2]];
d.draw();
}else{
} else if (match(oscAddressParts, ['update', 'workspace', '*', 'disconnect'])) {
delete device.data.workspaces[oscAddressParts[2]];
if (Object.keys(device.data.workspaces).length === 0) {
device.data.permission = 'no workspaces';
}
device.draw();
} else if (match(oscAddressParts, ['reply', 'thump'])) {
// intermittent connection check even if nothing's happening
} else {
// console.log(address)
}
};
const cueTemplate = _.template(fs.readFileSync(`./plugins/qlab/cue.ejs`));
const tileTemplate = _.template(fs.readFileSync(`./plugins/qlab/tile.ejs`));
const cartTemplate = _.template(fs.readFileSync(`./plugins/qlab/cart.ejs`));
exports.update = function update(device, doc, updateType, data){
if(updateType === "updateCueData"){
exports.update = function update(device, doc, updateType, data) {
if (updateType === 'updateCueData') {
const $elem = doc.getElementById(data.cue.uniqueID);
if($elem){
if(data.cue.type === "Cue List"){
if ($elem) {
if (data.cue.type === 'Cue List') {
$elem.outerHTML = `<h3>${data.workspace.displayName} &mdash; ${data.cue.name}</h3>`;
}else if(data.cue.type === "Cart"){
$elem.outerHTML = cartTemplate({'tileTemplate': tileTemplate, 'cueList': data.cue, 'allCues': data.workspace.cues});
}else if(data.cue.cartPosition && data.cue.cartPosition[0] !== 0){
$elem.outerHTML = tileTemplate(data);
}else{
$elem.outerHTML = cueTemplate(data);
} else if (data.cue.type === 'Cart') {
$elem.outerHTML = device.templates.cart({
tileTemplate: device.templates.tile,
cueList: data.cue,
allCues: data.workspace.cues,
});
} else if (data.cue.cartPosition && data.cue.cartPosition[0] !== 0) {
// checking that the parent cue is a cart cue
const parentCue = data.workspace.cues[data.cue.parent];
if (parentCue && parentCue.type === 'Cart') {
$elem.outerHTML = device.templates.tile(data);
} else {
$elem.outerHTML = device.templates.cue(data);
}
} else {
$elem.outerHTML = device.templates.cue(data);
}
}
}else if(updateType === "updatePlaybackPosition"){
Array.from(doc.getElementsByClassName("playback-position")).forEach(
($elem, index, array) => {
$elem.classList.remove("playback-position");
} else if (updateType === 'updatePlaybackPosition') {
Array.from(doc.getElementsByClassName('playback-position')).forEach(($elem, index, array) => {
$elem.classList.remove('playback-position');
});
const $elem = doc.getElementById(data.cue.uniqueID);
$elem.classList.add("playback-position");
$elem.scrollIntoView({behavior: "smooth", block: "center"});
$elem.classList.add('playback-position');
$elem.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
};
function addCueToWorkspace(_workspace, cue) {
const workspace = _workspace;
workspace.cues[cue.uniqueID] = cue;
workspace.cues[cue.uniqueID].nestedGroupModes = [];
workspace.cues[cue.uniqueID].nestedGroupPosition = [];
const valuesForKeysString =
'["uniqueID","number","name","listName","isBroken","isRunning","isLoaded","isFlagged",'
+ '"type","children","preWait","postWait","currentDuration","colorName","continueMode",'
+ '"mode","parent","cartRows","cartColumns","cartPosition","displayName","preWaitElapsed",'
+ '"actionElapsed","postWaitElapsed","isPaused"]';
function addChildrenToWorkspace(workspace, q){
const w = workspace;
w.cues[q.uniqueID] = q;
w.cues[q.uniqueID].nestedGroupModes = [];
w.cues[q.uniqueID].nestedGroupPosition = [];
if(q.cues){
q.cues.forEach(cue => {
addChildrenToWorkspace(w, cue);
if (cue.cues) {
// this cue has children so add them as well
cue.cues.forEach((childCue) => {
addCueToWorkspace(workspace, childCue);
});
}
}
function getValuesForKeys(device, workspaceID, q){
device.send(`/workspace/${workspaceID}/cue_id/${q.uniqueID}/valuesForKeys`, [
{type: 's', value: valuesForKeysString}
function getValuesForKeys(device, workspaceID, cue) {
device.send(`/workspace/${workspaceID}/cue_id/${cue.uniqueID}/valuesForKeys`, [
{ type: 's', value: valuesForKeysString },
]);
if(q.cues){
q.cues.forEach(cue => {
getValuesForKeys(device, workspaceID, cue);
if (cue.cues) {
cue.cues.forEach((childCue) => {
getValuesForKeys(device, workspaceID, childCue);
});
}
}
function match(osc, array){
function match(testArray, patternArray) {
let out = true;
if(osc.length !== array.length){
if (testArray.length !== patternArray.length) {
return false;
}
array.forEach((m, i)=> {
if(osc[i] !== m && m !== "*"){
patternArray.forEach((patternPart, i) => {
if (testArray[i] !== patternPart && patternPart !== '*') {
out = false;
}
});
return out;
}
let interval = 5;
let heartbeatCount = 0;
exports.heartbeat = function heartbeat(device) {
heartbeatCount++;
if(Date.now() - lastElapsedUpdate > 300){
interval = 5;
}else{
if (Date.now() - lastElapsedUpdate > 300) {
interval = 24;
} else {
interval = 1;
}
if(heartbeatCount % interval === 0){
device.send(`/cue/active/preWaitElapsed`);
device.send(`/cue/active/actionElapsed`);
device.send(`/cue/active/postWaitElapsed`);
if (heartbeatCount % 20 === 0 && device.data.workspaces && Object.keys(device.data.workspaces).length === 0) {
device.send(`/version`);
device.send('/workspaces');
}
if (heartbeatCount % 16 === 0) {
device.send(`/thump`);
}
if (heartbeatCount % interval === 0 && device.data.workspaces && Object.keys(device.data.workspaces).length > 0) {
device.send(`/cue_id/active/preWaitElapsed`);
device.send(`/cue_id/active/actionElapsed`);
device.send(`/cue_id/active/postWaitElapsed`);
}
};
+7 -1
View File
@@ -151,13 +151,19 @@ tr.playback-position .q-gray-text {
.gMode-4 {
border-color: #925fc0;
}
.gMode-4 {
border-color: #925fc0;
}
.gMode-6 {
border-color: #d05b15;
}
.gMode-,
.gMode-0 {
border-color: rgba(0, 0, 0, 0);
}
.group-arrow {
width: 13px;
background-image: url('img/arrow-down.png');
background-image: url('img/new_group_arrow.png');
background-position: center;
background-repeat: no-repeat;
}
+18 -35
View File
@@ -3,43 +3,26 @@
<h2>QLab <%= data.version || "" %></h2>
</header>
<%
const fs = require('fs');
let _ = require('lodash');
<% if(data.permission=="ok"){
for(const workspace_id in data.workspaces){
const workspace = data.workspaces[workspace_id];
for(const cueList_id in workspace.cueLists){
const ql = workspace.cueLists[cueList_id]; %>
<% if(ql.type=="Cue List"){ %>
<%= templates.cuelist({cueList: ql, allCues: workspace.cues, rowTemplate: templates.cue, workspace: workspace}) %>
<% }else if(ql.type=="Cart"){ %>
<%= templates.cart({cueList: ql, allCues: workspace.cues, tileTemplate: templates.tile}) %>
<% } %>
let cueTemplate = _.template(fs.readFileSync(`./plugins/qlab/cue.ejs`));
let tileTemplate = _.template(fs.readFileSync(`./plugins/qlab/tile.ejs`));
let cartTemplate = _.template(fs.readFileSync(`./plugins/qlab/cart.ejs`));
let listTemplate = _.template(fs.readFileSync(`./plugins/qlab/cuelist.ejs`));
<% }}}else if(data.permission=="no workspaces"){ %>
%>
<%
for(const workspace_id in data.workspaces){
const workspace = data.workspaces[workspace_id];
for(const cueList_id in workspace.cueLists){
const ql = workspace.cueLists[cueList_id];
%>
<% if(ql.type=="Cue List"){ %>
<%= listTemplate({cueList: ql, allCues: workspace.cues, rowTemplate: cueTemplate, workspace: workspace}) %>
<% }else if(ql.type=="Cart"){ %>
<%= cartTemplate({cueList: ql, allCues: workspace.cues, tileTemplate: tileTemplate}) %>
<% } %>
<% }} %>
<h3>QLab is launched but there are no open Workspaces</h3>
<% }else if(data.permission=="denied"){ %>
<h3>Incorrect Passcode or OSC Access Permissions</h3>
<button href="https://qlab.app/docs/v5/fundamentals/workspace-settings#the-osc-access-tab">QLab Documentation: OSC Access Tab</button>
<% } %>
+1 -1
View File
@@ -1 +1 @@
<p><em>sACN</em> requires no configuration.</p>
<p><em>sACN</em> requires no configuration.</p>
+98 -113
View File
@@ -1,152 +1,138 @@
const _ = require('lodash');
exports.defaultName = 'sACN';
exports.connectionType = 'multicast';
exports.defaultPort = 5568;
exports.heartbeatInterval = 5000;
exports.searchOptions = {
type: "multicast",
address: getMulticastGroup(1),
port: 5568,
validateResponse (msg, info) {
return msg.toString('utf8', 4, 13) === "ASC-E1.17";
exports.config = {
defaultName: 'sACN',
connectionType: 'multicast',
defaultPort: 5568,
mayChangePort: false,
heartbeatInterval: 5000,
heartbeatTimeout: 15000,
searchOptions: {
type: 'multicast',
address: getMulticastGroup(1),
port: 5568,
validateResponse(msg, info) {
return msg.toString('utf8', 4, 13) === 'ASC-E1.17';
},
},
};
exports.defaultPort = 5568;
exports.ready = function ready(device) {
const d = device;
d.data.universes = {};
d.data.source = "Unknown Source";
d.data.priorities = {};
d.data.source = 'Unknown Source';
d.data.orderedUniverses = [];
// device.draw();
for(let i = 1; i <= 16; i++){
d.connection.addMembership(getMulticastGroup(i));
const networkInterfaces = d.getNetworkInterfaces();
for (let i = 1; i <= 16; i++) {
for (let j = 0; j < Object.keys(networkInterfaces).length; j++) {
const networkInterfaceID = Object.keys(networkInterfaces)[j];
const networkInterface = networkInterfaces[networkInterfaceID];
d.connection.addMembership(getMulticastGroup(i), networkInterface[0].address);
}
}
};
exports.data = function data(_device, buf) {
const universeIndex = buf.readUInt16BE(113);
const device = _device;
exports.data = function data(device, buf) {
let universe = device.data.universes[universeIndex];
let priorities = device.data.priorities[universeIndex];
const univ = buf.readUInt16BE(113);
const d = device;
let u = d.data.universes[univ];
if(!u){
d.data.universes[univ] = {};
u = d.data.universes[univ];
if (!universe) {
device.data.universes[universeIndex] = {};
universe = device.data.universes[universeIndex];
}
if (!priorities) {
device.data.priorities[universeIndex] = new Array(512).fill(0);
priorities = device.data.priorities[universeIndex];
}
u.sequence = buf.readUInt8(111);
u.priority = buf.readUInt8(108);
u.cid = buf.toString('hex', 22, 38);
u.slots = Array.prototype.slice.call(buf, 126);
if(buf.readUInt8(125) !== 0){
u.startCode = buf.readUInt8(125);
universe.sequence = buf.readUInt8(111);
universe.priority = buf.readUInt8(108);
universe.cid = buf.toString('hex', 22, 38);
universe.slots = buf.slice(126);
if (buf.readUInt8(125) !== 0) {
universe.startCode = buf.readUInt8(125);
}
device.data.source = buf.toString('utf8', 44, 108);
device.displayName = `${device.data.source} sACN`;
device.data.ip = device.addresses[0];
d.data.source = buf.toString('utf8', 44, 108);
d.displayName = `${d.data.source} sACN`;
d.data.ip = d.addresses[0];
if(!_.includes(d.data.orderedUniverses, univ)){
d.data.orderedUniverses.push(univ);
d.data.orderedUniverses.sort();
u.slotElems = [];
u.slotElemsSet = false;
d.draw();
updateElementsCache(d, d.data.universes, d.data.orderedUniverses);
if (!_.includes(device.data.orderedUniverses, universeIndex)) {
device.data.orderedUniverses.push(universeIndex);
device.data.orderedUniverses.sort();
universe.slotElems = [];
universe.slotElemsSet = false;
if (universe.priority > 0) {
device.draw();
device.update('elementCache');
}
}
if (universe.priority > 0) {
device.update('universeData', {
universeIndex,
universe,
startCode: universe.startCode,
});
} else {
device.data.priorities[universeIndex] = buf.slice(126);
}
d.update("universeData", {
u: univ,
universe: u,
startCode: u.startCode
});
};
exports.heartbeat = function heartbeat(device) {
};
exports.heartbeat = function heartbeat(device) {};
let lastUpdate = Date.now();
exports.update = function update(device, doc, updateType, updateData){
const d = device;
exports.update = function update(_device, doc, updateType, updateData) {
const device = _device;
const data = updateData;
if(updateType === "universeData" && data.universe){
if(Date.now() - lastUpdate > 1000){
if (updateType === 'universeData' && data.universe) {
if (Date.now() - lastUpdate > 1000) {
lastUpdate = Date.now();
updateElementsCache(d, d.data.universes, d.data.orderedUniverses);
device.update('elementCache');
}
const $elem = doc.getElementById(`universe-${data.u}`);
if($elem && data.universe.slotElemsSet){
if(data.universe.priority > 0){
for(let i = 0; i < 512; i++){
data.universe.slotElems[i].innerText = data.universe.slots[i];
}
const $code = doc.getElementById(`universe-${data.u}-code`);
if(data.startCode === 0xDD){
$code.innerText = "Net3"
}else if(data.startCode === 0x17){
$code.innerText = "Text"
}else if(data.startCode === 0xCF){
$code.innerText = "SIP"
}else if(data.startCode === 0xCC){
$code.innerText = "RDM"
}
const $elem = doc.getElementById(`universe-${data.universeIndex}`);
if ($elem && data.universe.slotElemsSet) {
for (let i = 0; i < 512; i++) {
data.universe.slotElems[i].textContent = data.universe.slots[i];
}
}else{
d.draw();
updateElementsCache(d, d.data.universes, d.data.orderedUniverses);
const $code = doc.getElementById(`universe-${data.universeIndex}-code`);
if (data.startCode === 0xdd) {
$code.textContent = 'Net3';
} else if (data.startCode === 0x17) {
$code.textContent = 'Text';
} else if (data.startCode === 0xcf) {
$code.textContent = 'SIP';
} else if (data.startCode === 0xcc) {
$code.textContent = 'RDM';
}
} else {
device.draw();
device.update('elementCache');
}
} else if (updateType === 'elementCache') {
device.data.orderedUniverses.forEach((universeIndex) => {
const universe = device.data.universes[universeIndex];
}else if(updateType === "elementCache"){
data.orderedUniverses.forEach(univ => {
for(let i = 0; i < 512; i++){
data.universes[univ].slotElems[i] = doc.getElementById(`${univ}-${i}`);
if (doc.getElementById(`${universeIndex}-0`)) {
for (let i = 0; i < 512; i++) {
universe.slotElems[i] = doc.getElementById(`${universeIndex}-${i}`);
universe.slotElems[i].title = `${universeIndex}/${i} ${device.data.priorities[universeIndex][i]}`;
}
universe.slotElemsSet = true;
}
data.universes[univ].slotElemsSet = true;
});
}
}
function updateElementsCache(device, univ, ordered){
device.update("elementCache", {
universes: univ,
orderedUniverses: ordered
});
}
};
// From https://github.com/hhromic/e131-node/blob/master/lib/e131.js
function getMulticastGroup(universe) {
@@ -154,5 +140,4 @@ function getMulticastGroup(universe) {
throw new RangeError('universe should be in the range [1-63999]');
}
return `239.255.${universe >> 8}.${universe & 0xff}`;
}
}
+6 -6
View File
@@ -4,15 +4,15 @@
<div id="universes">
<% data.orderedUniverses.forEach(univ => {
let universe = data.universes[univ];
<% data.orderedUniverses.forEach(universeIndex => {
let universe = data.universes[universeIndex];
%>
<table id="universe-<%= univ %>">
<table id="universe-<%= universeIndex %>">
<tr>
<td class="data" colspan="2">
Universe
<br><em><%= univ %></em>
<br><em><%= universeIndex %></em>
</td>
<td class="data" colspan="2">
Priority
@@ -28,7 +28,7 @@
</td>
<td class="data" colspan="2">
Flavor
<br><em id="universe-<%= univ %>-code">0</em>
<br><em id="universe-<%= universeIndex %>-code">0</em>
</td>
</tr>
@@ -45,7 +45,7 @@
<th><%= row*16+1 %></th>
<% for(col=0; col<16; col++){ %>
<% slot++%>
<td id="<%= univ %>-<%= row*16+col %>" title="<%= univ %>/<%= slot %>"><%= universe.slots[slot-1] %></td>
<td id="<%= universeIndex %>-<%= row*16+col %>" title="<%= universeIndex %>/<%= slot %>"><%= universe.slots[slot-1] %></td>
<% } %>
</tr>
<% } %>
+14
View File
@@ -0,0 +1,14 @@
class Channel {
constructor() {
this.chan_name = '?';
this.batt_bars = 255;
this.batt_run_time = 65535;
this.audio_gain = 0;
this.audio_lvl = 0;
this.rx_rf_lvl = 0;
this.rf_antenna = 0;
this.tx_type = 0;
}
}
module.exports = Channel;
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File
+122
View File
@@ -0,0 +1,122 @@
const Channel = require('./channel');
exports.config = {
defaultName: 'Shure Wireless',
connectionType: 'TCPsocket',
defaultPort: 2202,
mayChangePort: false,
heartbeatInterval: 5000,
heartbeatTimeout: 10000,
searchOptions: {
type: 'TCPport',
searchBuffer: Buffer.from('< GET DEVICE_ID >', 'ascii'),
testPort: 2202,
validateResponse(msg, info) {
return msg.toString().includes('DEVICE_ID');
},
},
};
exports.ready = function ready(_device) {
const device = _device;
device.data.channelCount = 0;
device.data.channels = [{}, new Channel(), new Channel(), new Channel(), new Channel()];
};
exports.data = function data(_device, message) {
const device = _device;
let msgStr = message.toString();
if (!msgStr.startsWith('< ')) {
return;
}
msgStr = msgStr.slice(2).slice(0, -1);
const msgs = msgStr.split('><');
msgs.forEach((ms, i) => {
const msg = ms.trim();
const msgParts = msg.split(' ');
const channelNumber = Number(msgParts[1]);
const channel = device.data.channels[channelNumber];
if (msgParts[0] === 'REP') {
if (msgParts[2] === 'CHAN_NAME') {
channel.chan_name = msg.substring(17).slice(0, -2).trim();
if (device.data.channelCount < channelNumber) {
device.data.channelCount = channelNumber;
}
device.draw();
} else if (msgParts[2] === 'BATT_BARS') {
channel.batt_bars = Number(msgParts[3]);
} else if (msgParts[2] === 'BATT_RUN_TIME') {
channel.batt_run_time = Number(msgParts[3]);
} else if (msgParts[2] === 'AUDIO_GAIN') {
channel.audio_gain = Number(msgParts[3]) - 18;
} else if (msgParts[2] === 'AUDIO_LVL') {
channel.audio_lvl = Number(msgParts[3]);
} else if (msgParts[2] === 'RX_RF_LVL') {
channel.rx_rf_lvl = Number(msgParts[3]) - 128;
} else if (msgParts[2] === 'RF_ANTENNA') {
channel.rf_antenna = msgParts[3];
} else if (msgParts[2] === 'TX_TYPE') {
channel.tx_type = msgParts[3];
device.draw();
} else if (msgParts[1] === 'DEVICE_ID') {
const id = msg.substring(15).slice(0, -1).trim();
this.deviceInfoUpdate(device, 'defaultName', id);
} else if (msgParts[1] === 'FW_VER') {
device.data.version = msgParts[2].substring(1);
}
} else if (msgParts[0] === 'SAMPLE') {
channel.rf_antenna = msgParts[3];
channel.rx_rf_lvl = Number(msgParts[4]) - 128;
channel.audio_lvl = Number(msgParts[5]);
if (channelNumber === 4) {
device.update('updateSample', {
channels: device.data.channels,
});
}
}
});
};
exports.update = function update(device, doc, updateType, data) {
for (let i = 1; i < data.channels.length; i++) {
const channel = data.channels[i];
const $rf = doc.getElementById(`ch-${i}-rf`);
if ($rf) {
$rf.style.height = 90 - (channel.rx_rf_lvl + 90) * 2;
}
const $audio = doc.getElementById(`ch-${i}-audio`);
if ($audio) {
$audio.style.height = 90 - channel.audio_lvl * 2;
}
const $rfA = doc.getElementById(`ch-${i}-a`);
const $rfB = doc.getElementById(`ch-${i}-b`);
if ($rfA) {
if (channel.rf_antenna.charAt(0) === 'A') {
$rfA.style.color = '#53c3c3';
} else {
$rfA.style.color = '#333';
}
}
if ($rfB) {
if (channel.rf_antenna.charAt(1) === 'B') {
$rfB.style.color = '#53c3c3';
} else {
$rfB.style.color = '#333';
}
}
}
};
exports.heartbeat = function heartbeat(device) {
device.send('< GET 0 ALL >');
device.send('< SET 0 METER_RATE 00100 >');
device.send('< SAMPLE 0 AUDIO_LVL>');
};
+101
View File
@@ -0,0 +1,101 @@
table.channel {
width: 120px;
height: 320px;
background-color: #1a1a1d;
border: #3b3c42 1px solid;
border-collapse: collapse;
color: white;
text-align: center;
float: left;
margin: 2px;
margin-bottom: 20px;
}
table td {
border: #3b3c42 1px solid;
vertical-align: top;
}
.chan_name {
width: 100px;
color: #b2ff33 !important;
font-size: 18px;
font-weight: bold;
}
.bar-wrapper {
width: 20px;
height: 90px;
margin: 0px auto;
margin-top: 5px;
margin-bottom: 5px;
border-radius: 6px;
outline: #1a1a1d 2px solid;
outline-offset: -1px;
overflow: hidden;
}
.bar-wrapper.green {
background: linear-gradient(0deg, #4742af, #554ed4 30%, #554ed4 80%, #df664d);
}
.bar-wrapper.pink {
background: linear-gradient(0deg, #383943, #4e505d 40%);
}
.bar-wrapper.orange {
background: linear-gradient(0deg, #50c3c3, #5bdfe0 30%, #5bdfe0 80%);
}
.bar {
width: 20px;
background-color: #202124;
outline: rgba(255, 255, 255, 0.1) 1px solid;
}
.batt-wrapper {
width: 84px;
height: 35px;
margin: 0px auto;
border: #333 2px solid;
border-radius: 5px;
padding: 2px;
margin: 10px auto;
position: relative;
}
.batt-wrapper.green {
border-color: greenyellow;
}
.batt-knob {
background-color: #333;
position: absolute;
right: -5px;
top: 10px;
width: 5px;
height: 16px;
border-radius: 2px;
}
.batt-knob.green {
background-color: greenyellow;
}
.batt-bar {
height: 26px;
background: linear-gradient(0deg, rgb(145, 220, 33), greenyellow 100%);
border-radius: 2px;
color: #1a1a1d;
padding-top: 9px;
}
.rf-indicator-wrapper {
line-height: 8px;
margin-bottom: 4px;
font-size: 6px;
padding-top: 4px;
letter-spacing: 1px;
}
.rf-indicator {
color: #333;
}
small {
color: dimgray;
}
small small {
color: #444;
}
+72
View File
@@ -0,0 +1,72 @@
<header>
<h1><%= listName %></h1>
<h2><%= data.version || "" %></h2>
</header>
<% for(let i=1; i<=data.channelCount; i++){ %> <% let ch = data.channels[i]; %>
<table class="channel">
<tr>
<td colspan="3"><small><%= i %></small></td>
</tr>
<tr>
<td colspan="3" class="chan_name"><%= ch.chan_name %></td>
</tr>
<tr>
<td style="width: 40px">
<div class="rf-indicator-wrapper">
<span class="rf-indicator" id="ch-<%= i %>-a">&#11044;</span>
<span class="rf-indicator" id="ch-<%= i %>-b">&#11044;</span>
</div>
<small>rf</small>
<div class="bar-wrapper orange">
<div
class="bar"
id="ch-<%= i %>-rf"
style="height: <%= 90- ((ch.rx_rf_lvl + 90) * 2) %>"></div>
</div>
<small><%= ch.rx_rf_lvl %><br /><small>dBm</small></small>
</td>
<td style="width: 40px">
<div class="rf-indicator-wrapper">&nbsp;</div>
<small>audio</small>
<div class="bar-wrapper green">
<div
class="bar"
id="ch-<%= i %>-audio"
style="height: <%= 90- (ch.audio_lvl * 2) %>"></div>
</div>
<small><%= ch.audio_lvl %><br /><small>dBFS</small></small>
</td>
<td style="width: 40px">
<div class="rf-indicator-wrapper">&nbsp;</div>
<small>gain</small>
<div class="bar-wrapper pink">
<div class="bar" style="height: <%= 60- (ch.audio_gain * 1.4) %>"></div>
</div>
<small><%= ch.audio_gain %><br /><small>dB</small></small>
</td>
</tr>
<tr>
<td colspan="3">
<% if(ch.batt_bars==255){ %>
<div class="batt-wrapper"><div class="batt-knob"></div></div>
<% }else{ %>
<div class="batt-wrapper green">
<div class="batt-bar" style="width: <%= ch.batt_bars * 16 %>">
<% if(ch.batt_run_time<=65532){ %> <%= Math.floor(ch.batt_run_time/60)
%>: <%= ch.batt_run_time%60 %> <% } %>
</div>
<div class="batt-knob green"></div>
</div>
<% } %>
</td>
</tr>
<tr>
<td colspan="3">
<% if(ch.tx_type=="UNKN"){ %> &nbsp;<small>No Transmitter</small>&nbsp; <%
}else{ %> <%= ch.tx_type %> <% } %>
</td>
</tr>
</table>
<% } %>
+35 -37
View File
@@ -1,58 +1,56 @@
exports.defaultName = 'Dataton Watchout';
exports.connectionType = 'TCPsocket';
exports.heartbeatInterval = 500;
exports.searchOptions = {
type: 'TCPport',
searchBuffer: Buffer.from('authenticate 1\n', 'ascii'),
testPort: 3040,
validateResponse (msg, info) {
return msg.toString().substring(0, 5) === 'Ready';
exports.config = {
defaultName: 'Dataton Watchout',
connectionType: 'TCPsocket',
defaultPort: 3040,
mayChangePort: false,
heartbeatInterval: 250,
heartbeatTimeout: 5000,
searchOptions: {
type: 'TCPport',
searchBuffer: Buffer.from('authenticate 1\n', 'ascii'),
testPort: 3040,
validateResponse(msg, info) {
return msg.toString().substring(0, 5) === 'Ready';
},
},
};
exports.defaultPort = 3040;
exports.ready = function ready(device) {
device.send('authenticate 1\n');
};
exports.data = function data(device, message) {
const msg = message.toString();
const d = device;
exports.data = function data(_device, _message) {
const message = _message.toString();
const device = _device;
if (msg.substring(0, 5) === 'Ready') {
d.send('getStatus\n');
if (message.substring(0, 5) === 'Ready') {
// device.send('getStatus\n');
} else if (message.substring(0, 5) === 'Reply') {
const arr = message.split(' ');
}else if (msg.substring(0, 5) === 'Reply') {
const arr = msg.split(' ');
device.data.showName = '';
d.data.showName = '';
let i = 0;
while (arr[i][arr[i].length - 1] !== '"') {
i++;
d.data.showName += `${arr[i]} `;
device.data.showName += `${arr[i]} `;
}
d.data.showName = d.data.showName.substring(
1,
d.data.showName.length - 2
);
device.data.showName = device.data.showName.substring(1, device.data.showName.length - 2);
i--;
d.data.busy = arr[i + 2];
d.data.health = arr[i + 3];
d.data.displayOpen = arr[i + 4];
d.data.showActive = arr[i + 5];
d.data.programmerOnline = arr[i + 6];
d.data.position = Number(arr[i + 7]).toFixed(2);
d.data.rate = arr[i + 8];
d.data.standby = arr[i + 9];
device.data.busy = arr[i + 2];
device.data.health = arr[i + 3];
device.data.displayOpen = arr[i + 4];
device.data.showActive = arr[i + 5];
device.data.programmerOnline = arr[i + 6];
device.data.position = Number(arr[i + 7]).toFixed(2);
device.data.rate = arr[i + 8];
device.data.standby = arr[i + 9];
this.deviceInfoUpdate(d, 'defaultName', d.data.showName);
d.draw();
this.deviceInfoUpdate(device, 'defaultName', device.data.showName);
device.draw();
}
// if(msg.substring(0, 5)=="Error"){
// device.data.error = msg.substring(6, 7);
// }
// console.log(msg)
};
exports.heartbeat = function heartbeat(device) {
+9 -9
View File
@@ -2,7 +2,6 @@
<h1><%= listName %></h1>
</header>
<table class="cv-table">
<tr>
<td colspan="2">
@@ -14,19 +13,20 @@
</td>
</tr>
<tr>
<% var seconds = Math.floor(data.position/1000) %>
<% var minutes = Math.floor(seconds/60) %>
<% var millis = data.position %>
<td colspan="2"><h1><%= minutes %>:<%= ((seconds%60)+"").padStart(2, "0") %>.<%= millis%1000 %></h1></td>
<% if(data.position){ %>
<% var seconds = Math.floor(data.position/1000) %>
<% var minutes = Math.floor(seconds/60) %>
<% var millis = data.position %>
<td colspan="2"><h1><%= minutes %>:<%= ((seconds%60)+"").padStart(2, "0") %>.<%= millis%1000 %></h1></td>
<% }else{ %>
<td colspan="2"><h1>00:00.00</h1></td>
<% } %>
</tr>
<tr>
<td>Busy</td>
<td><div><%= data.busy %></div></td></td>
</tr>
</tr>
<tr>
<td>Display Open</td>
<td><div><%= data.displayOpen %></td></td>
+91 -101
View File
@@ -1,24 +1,41 @@
exports.defaultName = 'X32 Mixer';
exports.connectionType = 'UDPsocket';
exports.heartbeatInterval = 10000;
exports.searchOptions = {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]),
devicePort: 10023,
listenPort: 0,
validateResponse(msg, info) {
return msg.toString().indexOf('/xinfo') === 0;
exports.config = {
defaultName: 'X32 Mixer',
connectionType: 'osc-udp',
defaultPort: 10023,
mayChangePort: false,
heartbeatInterval: 9000,
heartbeatTimeout: 11000,
searchOptions: {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]),
devicePort: 10023,
listenPort: 0,
validateResponse(msg, info) {
return msg.toString().includes('/xinfo');
},
},
};
exports.defaultPort = 10023;
exports.ready = function ready(device) {
const d = device;
d.data.X32 = new Console();
exports.ready = function ready(_device) {
const device = _device;
device.data = new Console();
device.send('/xinfo');
d.send(Buffer.from('/xinfo'));
device.send('/batchsubscribe', [
{ type: 's', value: '/ch/meters' },
{ type: 's', value: '/meters/0' },
{ type: 'i', value: 0 },
{ type: 'i', value: 0 },
{ type: 'i', value: 1 },
]);
// device.send(Buffer.from("/subscribe\x00,si\x00/-stat/solosw/01\x001"));
device.send('/batchsubscribe', [
{ type: 's', value: '/main/meters' },
{ type: 's', value: '/meters/2' },
{ type: 'i', value: 0 },
{ type: 'i', value: 0 },
{ type: 'i', value: 1 },
]);
};
function parseAddress(msg) {
@@ -27,138 +44,111 @@ function parseAddress(msg) {
return addr;
}
exports.data = function data(device, buf) {
this.deviceInfoUpdate(device, 'status', 'ok');
exports.data = function data(_device, oscData) {
this.deviceInfoUpdate(_device, 'status', 'ok');
// replace one or more nulls with new line then split on that
const msg = buf.toString().replace(/(\0+)/gm, '\n').split('\n');
const device = _device;
const d = device;
if (oscData.address === '/xinfo') {
device.data.info.name = oscData.args[1];
device.data.info.ip = oscData.args[0];
device.data.info.firmware = oscData.args[3];
device.data.info.model = oscData.args[2];
if (msg[0] === '/xinfo') {
d.data.X32.info.name = msg[3];
d.data.X32.info.ip = msg[2];
d.data.X32.info.firmware = msg[5];
d.data.X32.info.model = msg[4];
this.deviceInfoUpdate(_device, 'defaultName', device.data.info.name);
this.deviceInfoUpdate(device, 'defaultName', d.data.X32.info.name);
d.send(Buffer.from('/main/st/config/name\x00\x00\x00\x00'));
device.send('/main/st/config/name');
for (let i = 0; i <= 32; i++) {
d.send(
Buffer.from(
`/ch/${i.toString().padStart(2, '0')}/config/name\x00\x00\x00\x00`
)
);
device.send(`/ch/${i.toString().padStart(2, '0')}/config/name`);
}
d.draw();
} else if (msg[0].includes('meters/0')) {
let offset = 24;
device.draw();
} else if (oscData.address.includes('/ch/meters')) {
const buf = Buffer.from(oscData.args[0]);
let offset = 4; // skip first 4 bytes they are the length bytes
for (let i = 0; i < 70; i++) {
if (i >= 0 && i < 32) {
// These are channel meters
d.data.X32.inputs.channels[i].meter = Console.getBehringerDB(
buf.readFloatLE(offset)
);
device.data.inputs.channels[i].meter = Console.getBehringerDB(buf.readFloatLE(offset));
}
offset += 4;
}
d.draw();
} else if (msg[0].includes('meters/2')) {
let offset = 24;
device.draw();
} else if (oscData.address.includes('/main/meters')) {
const buf = Buffer.from(oscData.args[0]);
let offset = 4; // skip first 4 bytes they are the length bytes
for (let i = 0; i < 49; i++) {
if (i === 22) {
// STEREO LEFT METER
d.data.X32.main.stereo.meter[0] = Console.getBehringerDB(
buf.readFloatLE(offset)
);
device.data.main.stereo.meter[0] = Console.getBehringerDB(buf.readFloatLE(offset));
} else if (i === 23) {
// STEREO RIGHT METER
d.data.X32.main.stereo.meter[1] = Console.getBehringerDB(
buf.readFloatLE(offset)
);
device.data.main.stereo.meter[1] = Console.getBehringerDB(buf.readFloatLE(offset));
}
offset += 4;
}
} else if (msg[0].indexOf('/mix/fader') >= 0) {
const addr = parseAddress(msg[0]);
const channel = Number(addr[1]);
} else if (oscData.address.includes('/mix/fader')) {
const addr = parseAddress(oscData.address);
if (addr[0] === 'ch') {
d.data.X32.inputs.channels[channel - 1].fader = buf.readFloatBE(24);
d.data.X32.inputs.channels[channel - 1].faderDB = Console.getBehringerDB(
buf.readFloatBE(24)
);
const channel = Number(addr[1]);
device.data.inputs.channels[channel - 1].fader = oscData.args[0];
device.data.inputs.channels[channel - 1].faderDB = Console.getBehringerDB(oscData.args[0]);
} else if (addr[0] === 'main') {
d.data.X32.main.stereo.fader = buf.readFloatBE(24);
d.data.X32.main.stereo.faderDB = Console.getBehringerDB(
buf.readFloatBE(24)
);
device.data.main.stereo.fader = oscData.args[0];
device.data.main.stereo.faderDB = Console.getBehringerDB(oscData.args[0]);
}
d.draw();
} else if (msg[0].indexOf('/mix/on') >= 0) {
const addr = parseAddress(msg[0]);
const channel = Number(addr[1]);
device.draw();
} else if (oscData.address.includes('/mix/on')) {
const addr = parseAddress(oscData.address);
if (addr[0] === 'ch') {
d.data.X32.inputs.channels[channel - 1].mute = buf[23];
d.send(Buffer.from(`/ch/${addr[1]}/mix/fader\x00\x00\x00\x00`));
const channel = Number(addr[1]);
device.data.inputs.channels[channel - 1].mute = oscData.args[0];
device.send(`/ch/${addr[1]}/mix/fader`);
} else if (addr[0] === 'main') {
d.data.X32.main.stereo.mute = buf.slice(-1)[0];
d.send(Buffer.from(`/main/${addr[1]}/mix/fader\x00\x00\x00\x00`));
device.data.main.stereo.mute = oscData.args[0];
device.send(`/main/${addr[1]}/mix/fader`);
}
d.draw();
} else if (msg[0].indexOf('/config/name') > 0) {
const addr = parseAddress(msg[0]);
device.draw();
} else if (oscData.address.includes('/config/name')) {
const addr = parseAddress(oscData.address);
if (addr[0] === 'main') {
if (addr[1] === 'st') {
d.data.X32.main.stereo.name = msg[2];
if (d.data.X32.main.stereo.name === '') {
d.data.X32.main.stereo.name = 'LR';
device.data.main.stereo.name = oscData.args[0];
if (device.data.main.stereo.name === '') {
device.data.main.stereo.name = 'LR';
}
d.send(Buffer.from(`/main/${addr[1]}/config/color\x00\x00\x00\x00`));
device.send(`/main/${addr[1]}/config/color`);
}
} else if (addr[0] === 'ch') {
const channel = Number(addr[1]);
d.data.X32.inputs.channels[channel - 1].name = msg[2];
d.send(Buffer.from(`/ch/${addr[1]}/config/color\x00\x00\x00\x00`));
device.data.inputs.channels[channel - 1].name = oscData.args[0];
device.send(`/ch/${addr[1]}/config/color`);
}
d.draw();
} else if (msg[0].indexOf('/config/color') > 0) {
const addr = parseAddress(msg[0]);
device.draw();
} else if (oscData.address.includes('/config/color')) {
const addr = parseAddress(oscData.address);
if (addr[0] === 'main') {
d.data.X32.main.stereo.color = Buffer.from(msg[2]).readInt8();
d.send(Buffer.from(`/main/${addr[1]}/mix/on\x00\x00\x00\x00`));
device.data.main.stereo.color = oscData.args[0];
device.send(`/main/${addr[1]}/mix/on`);
} else if (addr[0] === 'ch') {
const channel = Number(addr[1]);
d.data.X32.inputs.channels[channel - 1].color = buf.readInt8(27);
d.send(Buffer.from(`/ch/${addr[1]}/mix/on\x00\x00\x00\x00`));
device.data.inputs.channels[channel - 1].color = oscData.args[0];
device.send(`/ch/${addr[1]}/mix/on`);
}
d.draw();
} else {
// console.log(msg)
device.draw();
}
// console.log(msg)
};
exports.heartbeat = function heartbeat(device) {
device.send(Buffer.from('/xremote'));
device.send('/xremote');
// subscribe to meter groups
device.send(
Buffer.from(
'/batchsubscribe\x00,ssiii\x00\x00meters/0\x00\x00\x00\x00/meters/0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'
)
);
device.send(
Buffer.from(
'/batchsubscribe\x00,ssiii\x00\x00meters/2\x00\x00\x00\x00/meters/2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'
)
);
device.send('/renew', [{ type: 's', value: '/ch/meters' }]);
device.send('/renew', [{ type: 's', value: '/main/meters' }]);
};
class Console {
+1 -7
View File
@@ -30,7 +30,6 @@ tr td:first-child {
padding: 1px 6px;
border-radius: 10px;
color: black;
font-family: PlexMono !important;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
@@ -136,12 +135,7 @@ input[type='range']::-webkit-slider-runnable-track {
.meter {
height: 3px;
background: linear-gradient(
90deg,
rgba(19, 126, 30, 1) 0%,
rgba(255, 238, 30, 1) 85%,
rgba(255, 0, 0, 1) 100%
);
background: linear-gradient(90deg, rgba(19, 126, 30, 1) 0%, rgba(255, 238, 30, 1) 85%, rgba(255, 0, 0, 1) 100%);
}
/* need to find a way to match the color of this with the table row */
+14 -14
View File
@@ -1,6 +1,6 @@
<header>
<h1><%= listName %></h1>
<h2><%= data.model %></h2>
<h2><%= data.info.model %></h2>
</header>
<table class="cv-table">
@@ -13,15 +13,15 @@
</tr>
<tr>
<td width="40px">LR</td>
<td width="100px"><div class="color color-<%= data.X32.main.stereo.color %>"><%- data.X32.main.stereo.name%></div></td>
<td><div class="fader-mute mute-<%=data.X32.main.stereo.mute%>">M</div></td>
<td><%= formatAsDB(data.X32.main.stereo.faderDB) %></td>
<td width="100px"><div class="color color-<%= data.main.stereo.color %>"><%- data.main.stereo.name%></div></td>
<td><div class="fader-mute mute-<%=data.main.stereo.mute%>">M</div></td>
<td><%= formatAsDB(data.main.stereo.faderDB) %></td>
<td>
<% let style = `style=width:${Math.abs(data.X32.main.stereo.meter[0] - 10)}%`; %>
<% let style = `style=width:${Math.abs(data.main.stereo.meter[0] - 10)}%`; %>
<div class="meter">
<div class="meter-cover" <%= style %>></div>
</div>
<% style = `style=width:${Math.abs(data.X32.main.stereo.meter[1] - 10)}%`; %>
<% style = `style=width:${Math.abs(data.main.stereo.meter[1] - 10)}%`; %>
<div class="meter">
<div class="meter-cover" <%= style %>></div>
</div>
@@ -29,22 +29,22 @@
type="range"
min="0"
max="100"
value="<%= data.X32.main.stereo.fader*100 %>"
value="<%= data.main.stereo.fader*100 %>"
disabled />
</td>
</tr>
<% for(var i=0; i<32; i++){ %> <% if(data.X32.inputs.channels[i].name == "end"){break;} %>
<% for(var i=0; i<32; i++){ %> <% if(data.inputs.channels[i].name == "end"){break;} %>
<tr>
<td><%= i+1 %></td>
<td>
<div class="color color-<%= data.X32.inputs.channels[i].color %>">
<%- data.X32.inputs.channels[i].name || i+1 %>
<div class="color color-<%= data.inputs.channels[i].color %>">
<%- data.inputs.channels[i].name || i+1 %>
</div>
</td>
<td><div class="fader-mute mute-<%=data.X32.inputs.channels[i].mute%>">M</div></td>
<td><%= formatAsDB(data.X32.inputs.channels[i].faderDB) %></td>
<td><div class="fader-mute mute-<%=data.inputs.channels[i].mute%>">M</div></td>
<td><%= formatAsDB(data.inputs.channels[i].faderDB) %></td>
<td>
<% let style = `style=width:${Math.abs(data.X32.inputs.channels[i].meter - 10)}%`; %>
<% let style = `style=width:${Math.abs(data.inputs.channels[i].meter - 10)}%`; %>
<div class="meter">
<div class="meter-cover" <%= style %>></div>
</div>
@@ -52,7 +52,7 @@
type="range"
min="0"
max="100"
value="<%= data.X32.inputs.channels[i].fader*100 %>"
value="<%= data.inputs.channels[i].fader*100 %>"
disabled />
</td>
</tr>
+21 -27
View File
@@ -1,16 +1,20 @@
exports.defaultName = 'X Air Mixer';
exports.connectionType = 'UDPsocket';
exports.heartbeatInterval = 10000;
exports.searchOptions = {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]),
devicePort: 10024,
listenPort: 0,
validateResponse(msg, info) {
return msg.toString().indexOf('/xinfo') === 0;
exports.config = {
defaultName: 'X Air Mixer',
connectionType: 'UDPsocket',
defaultPort: 10024,
mayChangePort: false,
heartbeatInterval: 9000,
heartbeatTimeout: 15000,
searchOptions: {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]),
devicePort: 10024,
listenPort: 0,
validateResponse(msg, info) {
return msg.toString().startWith('/xinfo');
},
},
};
exports.defaultPort = 10024;
exports.ready = function ready(device) {
const d = device;
@@ -25,10 +29,6 @@ exports.ready = function ready(device) {
d.data.stereoMute = 0;
d.send('/xinfo');
// device.send(Buffer.from("\x2f\x62\x61\x74\x63\x68\x73\x75\x62\x73\x63\x72\x69\x62\x65\x00\x2c\x73\x73\x69\x69\x69\x00\x00\x6d\x65\x74\x65\x72\x73\x2f\x30\x00\x00\x00\x00\x2f\x6d\x65\x74\x65\x72\x73\x2f\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"));
// device.send(Buffer.from("/batchsubscribe\x00,ssiii\x00\x00meters/0\x00\x00\x00\x00/meters/0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"));
// device.send(Buffer.from("/subscribe\x00,si\x00/-stat/solosw/01\x001"));
};
function parseAddress(msg) {
@@ -71,31 +71,25 @@ exports.data = function data(device, buf) {
d.send('/lr/mix/on\x00\x00\x00\x00');
for (let i = 0; i <= 32; i++) {
d.send(
Buffer.from(
`/ch/${i.toString().padStart(2, '0')}/config/name\x00\x00\x00\x00`
)
);
d.send(Buffer.from(`/ch/${i.toString().padStart(2, '0')}/config/name\x00\x00\x00\x00`));
}
d.draw();
} else if (msg[0] === '/meters/0') {
// console.log(msg)
} else if (msg[0].indexOf('/mix/fader') >= 0) {
} else if (msg[0].includes('/mix/fader')) {
const addr = parseAddress(msg[0]);
const channel = Number(addr[1]);
if (addr[0] === 'ch') {
d.data.channelFaders[channel - 1] = buf.readFloatBE(24);
d.data.channelFadersDB[channel - 1] = convertToDBTheBehringerWay(
buf.readFloatBE(24)
);
d.data.channelFadersDB[channel - 1] = convertToDBTheBehringerWay(buf.readFloatBE(24));
} else if (addr[0] === 'lr') {
d.data.stereoFader = buf.readFloatBE(20);
d.data.stereoFaderDB = convertToDBTheBehringerWay(buf.readFloatBE(20));
}
d.draw();
} else if (msg[0].indexOf('/mix/on') >= 0) {
} else if (msg[0].includes('/mix/on')) {
const addr = parseAddress(msg[0]);
const channel = Number(addr[1]);
@@ -106,13 +100,13 @@ exports.data = function data(device, buf) {
}
device.draw();
device.send(`/ch/${addr[1]}/mix/fader\x00\x00\x00\x00`);
} else if (msg[0].indexOf('/config/name') > 0) {
} else if (msg[0].includes('/config/name')) {
const addr = parseAddress(msg[0]);
const channel = Number(addr[1]);
d.data.channelNames[channel - 1] = msg[4];
d.draw();
d.send(`/ch/${addr[1]}/config/color\x00\x00\x00\x00`);
} else if (msg[0].indexOf('/config/color') > 0) {
} else if (msg[0].includes('/config/color')) {
const addr = parseAddress(msg[0]);
const channel = Number(addr[1]);
d.data.channelColors[channel - 1] = buf.readInt8(27);
+1 -1
View File
@@ -128,7 +128,7 @@ input[type='range']::-webkit-slider-runnable-track {
border-radius: 4px;
}
.infin{
.infin {
font-size: 20px;
vertical-align: middle;
}
+63 -52
View File
@@ -15,6 +15,16 @@ window.init = function init() {
ipcRenderer.send('enableDeviceDropdown');
ipcRenderer.send('enableSearchAll');
// load autoUpdate setting from storage and send to main process
const autoUpdate = JSON.parse(localStorage.getItem('autoUpdate'));
if (autoUpdate !== undefined && autoUpdate !== null) {
if (autoUpdate) {
ipcRenderer.send('checkForUpdates');
}
// send message so main process knows the state of autoUpdate
ipcRenderer.send('setAutoUpdate', autoUpdate);
}
PLUGINS.init(() => {
VIEW.init();
SAVESLOTS.loadDevices();
@@ -28,12 +38,12 @@ window.init = function init() {
document.getElementById('device-settings-table').onclick = function settingsClick(e) {
e.stopPropagation();
};
document.getElementById('device-settings-name').onchange = function nameChange(e) {
e.stopPropagation();
DEVICE.changeActiveName(e.target.value);
};
document.getElementById('device-settings-plugin-dropdown').onchange = function dropdownChange(e) {
e.stopPropagation();
DEVICE.changeActiveType(e.target.value);
@@ -58,20 +68,18 @@ window.init = function init() {
}
};
document.getElementById('save-slot-1').onclick = function slot1click(e) {
e.stopPropagation();
SAVESLOTS.loadSlot(1);
};
const saveSlots = document.getElementsByClassName('save-slot');
document.getElementById('save-slot-2').onclick = function slot2click(e) {
e.stopPropagation();
SAVESLOTS.loadSlot(2);
};
document.getElementById('save-slot-3').onclick = function slot3click(e) {
e.stopPropagation();
SAVESLOTS.loadSlot(3);
};
for (let i = 0; i < saveSlots.length; i++) {
const saveSlot = saveSlots[i];
saveSlot.addEventListener('click', (event) => {
// get save slot from button id save-slot-1 = 1
const saveSlotIndex = parseInt(event.target.id.replace('save-slot-', ''), 10);
if (saveSlotIndex) {
SAVESLOTS.loadSlot(saveSlotIndex);
}
});
}
document.getElementById('refresh-device-button').onclick = function refreshClick(e) {
e.stopPropagation();
@@ -87,48 +95,46 @@ window.init = function init() {
};
document.getElementById('add-device-button').onchange = function addDeviceClick(e) {
DEVICE.registerDevice({
type: e.target.value,
defaultName: 'New Device',
port: undefined,
addresses: [],
});
const newDevice = DEVICE.registerDevice(
{
type: e.target.value,
defaultName: 'New Device',
port: undefined,
addresses: [],
},
'fromAddButton'
);
e.target.selectedIndex = 0;
VIEW.switchDevice(newDevice.id);
SAVESLOTS.saveAll();
};
document.onkeyup = function keyUp(e) {
if(e.key === 'ArrowUp'){
VIEW.selectPreviousDevice();
}else if(e.key === 'ArrowDown'){
VIEW.selectNextDevice();
document.getElementById('network-info-button').onclick = function fooBar(e) {
ipcRenderer.send('openNetworkInfoWindow');
};
}else if(e.key === 'Tab'){
if (
document.activeElement.tagName !== 'INPUT' &&
document.activeElement.tagName !== 'SELECT'
) {
document.onkeyup = function keyUp(e) {
if (e.key === 'ArrowUp') {
VIEW.selectPreviousDevice();
} else if (e.key === 'ArrowDown') {
VIEW.selectNextDevice();
} else if (e.key === 'Tab') {
if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'SELECT') {
document.getElementById('device-settings-name').select();
}
}
};
document.getElementById('device-list-col').onclick = function deviceListClick(e) {
try {
document
.querySelector('#device-list .active-device')
.classList.remove('active-device');
document.querySelector('#device-list .active-device').classList.remove('active-device');
ipcRenderer.send('disableDeviceDropdown', '');
} catch (err) {
// console.log(err)
}
try {
document
.getElementsByClassName('active-device-outline')[0]
.classList.remove('active-device-outline');
document.getElementsByClassName('active-device-outline')[0].classList.remove('active-device-outline');
} catch (err) {
// console.log(err)
}
@@ -145,28 +151,33 @@ ipcRenderer.on('setActiveDevicePinned', (event, message) => {
}
});
ipcRenderer.on('doSearch', (event, message) => {
ipcRenderer.on('searchAll', (event, message) => {
window.searchAll();
});
ipcRenderer.on('doDelete', (event, message) => {
ipcRenderer.on('deleteActive', (event, message) => {
DEVICE.deleteActive();
VIEW.selectPreviousDevice();
});
ipcRenderer.on('resetViews', (event, message) => {
SAVESLOTS.resetSlots();
ipcRenderer.on('clearSavedData', (event, message) => {
SAVESLOTS.clearSavedData();
});
ipcRenderer.on('doSlots1', (event, message) => {
SAVESLOTS.loadSlot(1);
ipcRenderer.on('loadSlot', (event, slot) => {
if (slot) {
SAVESLOTS.loadSlot(slot);
}
});
ipcRenderer.on('doSlots2', (event, message) => {
SAVESLOTS.loadSlot(2);
});
ipcRenderer.on('doSlots3', (event, message) => {
SAVESLOTS.loadSlot(3);
// message from main process to set autoUpdate state
ipcRenderer.on('setAutoUpdate', (event, autoUpdate) => {
localStorage.setItem('autoUpdate', autoUpdate);
if (autoUpdate) {
ipcRenderer.send('checkForUpdates');
}
// message to main process that we have updated the state
ipcRenderer.send('setAutoUpdate', autoUpdate);
});
function switchClass(element, className) {
@@ -177,4 +188,4 @@ function switchClass(element, className) {
}
element.classList.add(className);
}
window.switchClass = switchClass;
window.switchClass = switchClass;
+33 -21
View File
@@ -27,6 +27,9 @@ a {
.left {
float: left;
}
.right {
float: right;
}
#main {
box-sizing: border-box;
@@ -53,13 +56,14 @@ a {
text-align: right;
padding-left: 100px;
padding-right: 7px;
user-select: text;
-webkit-app-region: drag;
z-index: 1000;
/*border-bottom: black 1px solid;*/
/*background: linear-gradient(#3f3f3f 0%, #343434 100%);*/
/*background-color: #363636;*/
/*background-color: rgba(0, 0, 0, 0.4);*/
}
#view-buttons-bar .active {
background-color: rgba(255, 255, 255, 0.3);
color: white;
border: #61c650;
border-style: solid;
}
#view-buttons-bar button {
background-color: rgba(255, 255, 255, 0.07);
@@ -123,12 +127,16 @@ a {
width: 210px;
pointer-events: none;
}
#device-tools {
#device-inspector {
position: absolute;
bottom: 210px;
left: 0px;
bottom: 0px;
width: 100%;
}
#device-tools {
/* position: absolute;
bottom: 210px;
left: 0px;*/
width: 262px;
height: 30px;
padding-right: 10px;
/*background: linear-gradient(#2f2f2f 0%, #2c2c2c 100%);*/
@@ -142,10 +150,7 @@ a {
}
#device-settings {
position: absolute;
left: 0px;
bottom: 0px;
height: 200px;
height: 300px;
padding: 5px;
color: white;
font-weight: 300;
@@ -165,7 +170,7 @@ a {
display: none;
}
#device-settings h3 {
padding-top: 20px;
padding-top: 90px;
text-align: center;
font-weight: 300;
}
@@ -187,7 +192,7 @@ a {
.device-pin {
position: absolute;
right: 15px;
top: 8px;
top: 4px;
height: 20px;
padding: inherit;
}
@@ -220,6 +225,7 @@ input {
color: #fff;
font-size: 16px;
font-weight: 500;
cursor: text;
}
select {
display: block;
@@ -250,6 +256,7 @@ input[type='checkbox'] {
width: 14px;
padding: 2px;
-webkit-appearance: none;
cursor: default;
}
input[type='checkbox']:checked {
background-color: #616064;
@@ -267,6 +274,10 @@ input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input:disabled {
color: gray;
cursor: not-allowed;
}
button,
select.button {
box-sizing: content-box;
@@ -291,10 +302,6 @@ button:hover {
button:focus {
outline: none;
}
button.active {
background-color: rgba(255, 255, 255, 0.3);
color: white;
}
button:disabled {
background: rgba(255, 255, 255, 0.03);
}
@@ -310,8 +317,8 @@ select.button:focus {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('img/Material Icons'), local('img/MaterialIcons-Regular'),
url(img/MaterialIcons-Regular.ttf) format('truetype');
src: local('Material Icons'), local('MaterialIcons-Regular'),
url(../font/MaterialIcons-Regular.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
@@ -337,3 +344,8 @@ select.button:focus {
/* Support for IE. */
font-feature-settings: 'liga';
}
/* only allow select on input fields */
body :not(input):not(select):not(textarea) {
user-select: none;
}
@@ -1,5 +1,5 @@
body {
font-family: sans-serif;
font-family: system-ui;
margin: 0px;
margin-top: 40px;
user-select: none;
@@ -11,14 +11,14 @@ header {
position: fixed;
top: 0px;
left: 0px;
height: 36px;
height: 33px;
width: 100%;
box-sizing: content-box;
white-space: nowrap;
box-sizing: border-box;
padding-right: 34px;
overflow: hidden;
background-color: #111111;
border-bottom: black 1px solid;
-webkit-user-select: none;
-webkit-app-region: drag;
}
h1,
@@ -32,11 +32,13 @@ h6 {
header h1,
header h2 {
margin: 8px;
margin: 4px;
margin-left: 20px;
font-weight: normal;
font-size: 18px;
float: left;
height: 24px;
overflow: hidden;
}
header h2 {
color: #b6b6b6;
@@ -104,3 +106,16 @@ table.cv-table td {
color: #fdea08;
margin: 1px;
}
button {
display: block;
box-sizing: content-box;
background-color: rgba(255, 255, 255, 0.15);
height: 28px;
padding: 2px 20px;
margin: 0px;
border: rgba(255, 255, 255, 0.1) 1px solid;
border-radius: 4px;
color: #fff;
font-size: 16px;
font-weight: 500;
}

Before

Width:  |  Height:  |  Size: 401 KiB

After

Width:  |  Height:  |  Size: 401 KiB

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Before

Width:  |  Height:  |  Size: 224 B

After

Width:  |  Height:  |  Size: 224 B

Before

Width:  |  Height:  |  Size: 138 B

After

Width:  |  Height:  |  Size: 138 B

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

Before

Width:  |  Height:  |  Size: 212 B

After

Width:  |  Height:  |  Size: 212 B

Before

Width:  |  Height:  |  Size: 171 B

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Before

Width:  |  Height:  |  Size: 236 B

After

Width:  |  Height:  |  Size: 236 B

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 415 B

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 383 B

+122 -86
View File
@@ -2,17 +2,16 @@ const { v4: uuid } = require('uuid');
const osc = require('osc');
const net = require('net');
const udp = require('dgram');
const _ = require('lodash');
const { Atem } = require('atem-connection');
const PLUGINS = require('./plugins.js');
const VIEW = require('./view.js');
const SAVESLOTS = require('./saveSlots.js');
const SEARCH = require('./search.js');
const devices = {};
module.exports.all = devices;
function registerDevice(newDevice) {
function registerDevice(newDevice, discoveryMethod) {
if (PLUGINS.all[newDevice.type] === undefined) {
console.error(`Plugin for device ${newDevice.type} does not exist.`);
return true;
@@ -23,23 +22,11 @@ function registerDevice(newDevice) {
initElements[i].style.display = 'none';
}
// only register device if it hasn't already been added
if (newDevice.addresses.length > 0) {
const existing = _.find(devices, (e) => {
const typeMatch = e.type === newDevice.type;
const addressMatch = JSON.stringify(e.addresses) === JSON.stringify(newDevice.addresses);
return typeMatch && addressMatch;
});
if(existing){
return false;
}
// prevent duplicate devices from being added via search
if (isDeviceAlreadyAdded(newDevice) && discoveryMethod === 'fromSearch') {
return false;
}
// console.log("Registered new "+newDevice.type)
const id = newDevice.id || uuid();
devices[id] = {
id,
@@ -51,52 +38,78 @@ function registerDevice(newDevice) {
port: newDevice.port,
addresses: newDevice.addresses,
data: {},
templates: {},
fields: newDevice.fields || {},
pinIndex: false,
lastDrawn: 0,
lastHeartbeat: 0,
lastMessage: 0,
heartbeatInterval: PLUGINS.all[newDevice.type].heartbeatInterval,
draw(){
heartbeatTimeout: PLUGINS.all[newDevice.type].heartbeatTimeout,
draw() {
VIEW.draw(this);
},
update(type, data){
if(this.drawn){
update(type, data) {
if (this.drawn) {
VIEW.update(this, type, data);
}else{
} else {
this.draw();
}
}
},
getNetworkInterfaces() {
return SEARCH.getNetworkInterfaces();
},
};
devices[id].plugin = PLUGINS.all[newDevice.type];
if (Object.keys(devices[id].fields).length === 0 && PLUGINS.all[newDevice.type].config.fields) {
PLUGINS.all[newDevice.type].config.fields.forEach((field) => {
devices[id].fields[field.key] = field.value;
});
}
VIEW.addDeviceToList(devices[id]);
initDeviceConnection(id);
return true;
};
return devices[id];
}
module.exports.registerDevice = registerDevice;
function initDeviceConnection(id) {
const device = devices[id];
infoUpdate(device, 'status', 'new');
if (device.port === undefined) {
device.port = device.plugin.config.defaultPort;
}
if (device.port === undefined || device.addresses.length === 0) {
return true;
}
try {
// mostly only useful for UDP
device.connection.close();
} catch (err){
} catch (err) {
//
}
const { type } = devices[id];
const plugins = PLUGINS.all;
if (plugins[type].connectionType === 'osc') {
device.connection = new osc.TCPSocketPort({
address: device.addresses[0],
port: device.port,
});
if (plugins[type].config.connectionType.includes('osc')) {
if (plugins[type].config.connectionType.includes('udp')) {
device.connection = new osc.UDPPort({
localAddress: '0.0.0.0',
remoteAddress: device.addresses[0],
remotePort: device.port,
});
} else {
device.connection = new osc.TCPSocketPort({
address: device.addresses[0],
port: device.port,
});
}
device.connection.open();
device.connection.on('error', (error) => {
// console.error(error)
@@ -109,26 +122,25 @@ function initDeviceConnection(id) {
}
});
device.connection.on('message', (message) => {
try{
try {
plugins[type].data(device, message);
}catch(err){
console.error(err)
} catch (err) {
console.error(err);
}
device.lastMessage = Date.now();
});
device.send = (address, args) => {
device.connection.send({ 'address': address, 'args': args });
device.connection.send({ address, args });
};
device.plugin = plugins[type];
} else if (plugins[type].connectionType === 'TCPsocket') {
} else if (plugins[type].config.connectionType === 'TCPsocket') {
device.connection = new net.Socket();
device.connection.connect({
port: device.port,
host: device.addresses[0]
}, () => {});
device.connection.connect(
{
port: device.port,
host: device.addresses[0],
},
() => {}
);
device.connection.on('error', (error) => {
// console.error(error)
@@ -149,16 +161,15 @@ function initDeviceConnection(id) {
// log("SOCK OUT", data);
device.connection.write(data);
};
} else if (plugins[type].connectionType === 'UDPsocket') {
} else if (plugins[type].config.connectionType === 'UDPsocket') {
device.connection = udp.createSocket('udp4');
device.connection.bind({port: plugins[type].defaultPort}, () => {
device.connection.bind({ port: plugins[type].config.defaultPort }, () => {
plugins[type].ready(device);
device.connection.on('message', (msg, info) => {
plugins[type].data(device, msg);
device.lastMessage = Date.now();
infoUpdate(device, 'status', 'ok');
});
});
@@ -168,9 +179,7 @@ function initDeviceConnection(id) {
// console.log(err);
});
};
} else if (plugins[type].connectionType === 'multicast') {
} else if (plugins[type].config.connectionType === 'multicast') {
device.connection = udp.createSocket('udp4');
device.connection.bind(device.port, () => {
@@ -178,30 +187,51 @@ function initDeviceConnection(id) {
device.connection.on('message', (msg, info) => {
plugins[type].data(device, msg);
device.lastMessage = Date.now();
infoUpdate(device, 'status', 'ok');
});
});
device.send = (data) => { };
device.send = (data) => {};
} else if (plugins[type].config.connectionType === 'atem') {
device.connection = new Atem({
// this gets around the no workers nodejs error
disableMultithreaded: true,
});
device.connection.connect(device.addresses[0]);
device.connection.on('connected', () => {
infoUpdate(device, 'status', 'ok');
plugins[type].ready(device);
});
device.connection.on('stateChanged', (state, pathToChange) => {
device.lastMessage = Date.now();
plugins[type].data(device, {
pathToChange,
state,
});
infoUpdate(device, 'status', 'ok');
});
}
return true;
};
}
module.exports.initDeviceConnection = initDeviceConnection;
module.exports.deleteActive = function deleteActive() {
const device = VIEW.getActiveDevice();
const choice = confirm(
`Are you sure you want to delete ${device.type} device "${
device.displayName || device.defaultName
}"?`
`Are you sure you want to delete ${device.type} device "${device.displayName || device.defaultName}"?`
);
if (choice) {
if (device.plugin.connectionType === 'TCPsocket') {
device.connection.destroy();
}
VIEW.removeDeviceFromList(device);
delete devices[device.id];
SAVESLOTS.deleteFromSlots(device);
SAVESLOTS.saveAll();
SAVESLOTS.removeDevice(device);
SAVESLOTS.reloadActiveSlot();
}
};
@@ -209,10 +239,18 @@ module.exports.deleteActive = function deleteActive() {
module.exports.changeActiveType = function changeActiveType(newType) {
const device = VIEW.getActiveDevice();
device.type = newType;
device.fields = [];
device.plugin = PLUGINS.all[newType];
if (PLUGINS.all[device.type].config.fields) {
PLUGINS.all[newType].config.fields.forEach((field) => {
device.fields[field.key] = field.value;
});
}
initDeviceConnection(device.id);
VIEW.draw(device);
// SAVESLOTS.saveAll();
VIEW.updateFields();
};
module.exports.changeActiveIP = function changeActiveIP(newIP) {
@@ -248,7 +286,6 @@ module.exports.changeActivePinIndex = function changeActivePinIndex(newPin) {
module.exports.changePinIndex = function changePinIndex(device, newPin) {
const d = device;
d.pinIndex = newPin;
// SAVESLOTS.saveAll();
};
module.exports.refreshActive = function refreshActive() {
const device = VIEW.getActiveDevice();
@@ -268,11 +305,10 @@ function infoUpdate(device, param, value) {
d[param] = value;
}
VIEW.addDeviceToList(device);
};
}
module.exports.infoUpdate = infoUpdate;
function heartbeat() {
Object.keys(devices).forEach((deviceID) => {
const d = devices[deviceID];
@@ -281,34 +317,34 @@ function heartbeat() {
initDeviceConnection(deviceID);
} else if (Date.now() - d.lastMessage > d.heartbeatTimeout) {
infoUpdate(d, 'status', 'broken');
} else if (d.port !== undefined && d.addresses.length > 0) {
} else if (d.port !== undefined && d.addresses.length > 0 && d.send) {
PLUGINS.all[d.type].heartbeat(d);
} else {
//
}
d.lastHeartbeat = Date.now();
}
});
// for (let i in devices) {
// const device = devices[i];
// if (Date.now() >= device.lastHeartbeat + device.heartbeatInterval) {
// if (device.status == 'broken') {
// initDeviceConnection(i);
// } else if (Date.now() - device.lastMessage > device.heartbeatTimeout) {
// infoUpdate(device, 'status', 'broken');
// } else {
// if (device.port != undefined && device.addresses.length > 0) {
// PLUGINS.all[device.type].heartbeat(device);
// } else {
// // console.error("Invalid IP/Port on device "+device.name)
// }
// }
// device.lastHeartbeat = Date.now();
// }
// }
}
setInterval(heartbeat, 100);
function isDeviceAlreadyAdded(newDevice) {
let deviceAlreadyAdded = false;
if (newDevice.addresses.length === 0) {
return false;
}
for (let i = 0; i < Object.keys(devices).length; i++) {
const device = devices[Object.keys(devices)[i]];
const typeMatch = device.type === newDevice.type;
const addressMatch = JSON.stringify(device.addresses) === JSON.stringify(newDevice.addresses);
const idMatch = device.id === newDevice.id;
if (typeMatch && addressMatch && !idMatch) {
deviceAlreadyAdded = true;
break;
}
}
return deviceAlreadyAdded;
}
module.exports.isDeviceAlreadyAdded = isDeviceAlreadyAdded;
+1 -1
View File
@@ -1 +1 @@
window.init();
window.init();
+17 -36
View File
@@ -1,6 +1,8 @@
/* eslint-disable global-require */
/* eslint-disable import/no-dynamic-require */
const fs = require('fs');
const _ = require('lodash');
const path = require('path');
const DEVICE = require('./device.js');
const VIEW = require('./view.js');
@@ -9,58 +11,37 @@ const allPlugins = {};
module.exports.all = allPlugins;
module.exports.init = function init(callback) {
console.log(`Loading plugin files... ${__dirname}/../plugins`);
const pluginDirectoryPath = path.normalize(path.join(__dirname, `../plugins`));
fs.readdir(`${__dirname}/../plugins`, (err, files) => {
files.forEach((plugin)=>{
if (plugin[0] !== '.') {
console.log(`${plugin} started`);
console.log(`Loading plugin files... ${pluginDirectoryPath}`);
// eslint-disable-next-line import/no-dynamic-require
allPlugins[plugin] = require(`${__dirname}/../plugins/${plugin}/main.js`);
fs.readdir(pluginDirectoryPath, (err, files) => {
files.forEach((pluginDir) => {
if (pluginDir[0] !== '.') {
allPlugins[pluginDir] = require(path.join(pluginDirectoryPath, `/${pluginDir}/main.js`));
const p = allPlugins[plugin];
const plugin = allPlugins[pluginDir];
p.deviceInfoUpdate = function deviceInfoUpdate(device, param, value) {
plugin.deviceInfoUpdate = function deviceInfoUpdate(device, param, value) {
DEVICE.infoUpdate(device, param, value);
};
p.draw = (device) => {
plugin.draw = (device) => {
VIEW.draw(device);
};
p.template = _.template(
fs.readFileSync(
`${__dirname}/../plugins/${plugin}/template.ejs`,
'utf8'
)
plugin.template = _.template(
fs.readFileSync(path.join(pluginDirectoryPath, `/${pluginDir}/template.ejs`), 'utf8')
);
p.info = _.template(
fs.readFileSync(
`${__dirname}/../plugins/${plugin}/info.html`,
'utf8'
)
);
plugin.info = _.template(fs.readFileSync(path.join(pluginDirectoryPath, `/${pluginDir}/info.html`), 'utf8'));
plugin.heartbeatTimeout = plugin.config.heartbeatTimeout;
plugin.heartbeatInterval = plugin.config.heartbeatInterval;
if (p.heartbeatTimeout === undefined || p.heartbeatTimeout < 50) {
if(p.heartbeatInterval){
p.heartbeatTimeout = p.heartbeatInterval * 1.5;
}else{
p.heartbeatTimeout = 10000;
}
}
if (p.heartbeatInterval === undefined || p.heartbeatInterval < 50) {
p.heartbeatInterval = 5000;
}
console.log(`${pluginDir} loaded`);
}
});
callback();
});
};
+56 -35
View File
@@ -8,6 +8,13 @@ let savedDevices = [];
const storedSlots = localStorage.getItem('savedSlots');
if (storedSlots) {
savedSlots = JSON.parse(storedSlots);
// can probably be removed but I've noticed some older versions left some [null] slots in the savedSlots
savedSlots = savedSlots.map((savedSlot) => {
if (savedSlot.length === 1 && savedSlot[0] === null) {
return [];
}
return savedSlot;
});
}
const storedDevices = localStorage.getItem('savedDevices');
@@ -19,7 +26,7 @@ function loadSlot(slotIndex) {
VIEW.toggleSlotButtons(slotIndex);
activeSlot = slotIndex;
Object.keys(DEVICE.all).forEach((d)=>{
Object.keys(DEVICE.all).forEach((d) => {
DEVICE.changePinIndex(DEVICE.all[d], false);
});
VIEW.resetPinned();
@@ -33,7 +40,6 @@ function loadSlot(slotIndex) {
if (device.id === savedDevice.id) {
VIEW.pinDevice(device);
VIEW.switchDevice(device.id);
} else if (
device.addresses[0] === savedDevice.addresses[0] &&
device.type === savedDevice.type &&
@@ -43,44 +49,57 @@ function loadSlot(slotIndex) {
VIEW.switchDevice(device.id);
}
});
});
};
}
module.exports.loadSlot = loadSlot;
module.exports.loadDevices = function loadDevices() {
console.log(`Loading ${savedDevices.length} saved devices...`);
for (let i = 0; i < savedDevices.length; i++) {
DEVICE.registerDevice({
type: savedDevices[i].type,
displayName: savedDevices[i].displayName,
defaultName: savedDevices[i].defaultName,
port: savedDevices[i].port,
addresses: savedDevices[i].addresses,
id: savedDevices[i].id,
});
DEVICE.registerDevice(
{
type: savedDevices[i].type,
displayName: savedDevices[i].displayName,
defaultName: savedDevices[i].defaultName,
port: savedDevices[i].port,
addresses: savedDevices[i].addresses,
id: savedDevices[i].id,
fields: savedDevices[i].fields,
},
'fromSave'
);
}
};
module.exports.saveAll = function saveAll() {
console.log('Saving...');
const currentPins = VIEW.getPinnedDevices();
savedSlots[activeSlot] = [];
for (let i = 0; i < currentPins.length; i++) {
savedSlots[activeSlot][i] = {
addresses: currentPins[i].addresses,
type: currentPins[i].type,
id: currentPins[i].id,
};
if (currentPins[i]) {
// only include saved devices
if (currentPins[i].id in DEVICE.all) {
savedSlots[activeSlot][i] = {
addresses: currentPins[i].addresses,
type: currentPins[i].type,
id: currentPins[i].id,
};
}
}
}
// can probably be removed but I've noticed some older versions left some [null] slots in the savedSlots
savedSlots = savedSlots.map((savedSlot) => {
if (savedSlot.length === 1 && savedSlot[0] === null) {
return [];
}
return savedSlot;
});
localStorage.setItem('savedSlots', JSON.stringify(savedSlots));
console.log(
`Saved ${currentPins.length} pinned devices to slot ${activeSlot}!`
);
console.log(`Saved ${currentPins.length} pinned devices to slot ${activeSlot}!`);
savedDevices = [];
let i = 0;
@@ -93,31 +112,33 @@ module.exports.saveAll = function saveAll() {
defaultName: device.defaultName,
port: device.port,
id: device.id,
fields: device.fields,
};
i++;
});
localStorage.setItem('savedDevices', JSON.stringify(savedDevices));
console.log(`Saved ${savedDevices.length} devices to storage!`);
};
module.exports.removeDevice = function removeDevice(_device) {
// remove devices from local savedDevices
savedDevices = savedDevices.filter((device) => device.id !== _device.id);
module.exports.deleteFromSlots = function deleteFromSlots(device) {
for (let i = 1; i <= 3; i++) {
console.log(savedSlots);
for (let j = 0; j < savedSlots[i].length; j++) {
if (savedSlots[i][j].id === device.id) {
delete savedSlots[i][j];
}
}
// remove devices from all saved slots
for (let i = 1; i < savedSlots.length; i++) {
savedSlots[i] = savedSlots[i].filter((device) => device.id !== _device.id);
}
};
// things might have changed so run a save
this.saveAll();
};
module.exports.reloadActiveSlot = function reloadActiveSlot() {
loadSlot(activeSlot);
};
module.exports.resetSlots = function resetSlots() {
localStorage.clear();
module.exports.clearSavedData = function clearSavedData() {
localStorage.removeItem('savedSlots');
localStorage.removeItem('savedDevices');
};

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