├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── release.yaml └── workflows │ └── release.yaml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── index.html ├── main.js ├── networkInterfaces.html ├── networkInterfaces.js ├── package-lock.json ├── package.json ├── plugins ├── artnet │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── atem │ ├── icon.png │ ├── img │ │ ├── button_green.png │ │ ├── button_off.png │ │ ├── button_red.png │ │ ├── button_white.png │ │ ├── button_yellow.png │ │ ├── tbar_bg.png │ │ └── tbar_handle.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── digico │ ├── fader.ejs │ ├── icon.afphoto │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── eos │ ├── cue.ejs │ ├── cue.js │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── lightfactory │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── livestream-studio │ ├── icon.png │ ├── img │ │ ├── tbar_bg.png │ │ └── tbar_handle.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── pjlink │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── posistagenet │ ├── icon.afphoto │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── qlab │ ├── cart.ejs │ ├── cue.ejs │ ├── cuelist.ejs │ ├── icon.png │ ├── img │ │ ├── arm.png │ │ ├── arrow-down.png │ │ ├── arrow-right.png │ │ ├── audio.png │ │ ├── auto_continue.png │ │ ├── auto_continue_stubby.png │ │ ├── auto_follow.png │ │ ├── camera.png │ │ ├── devamp.png │ │ ├── disarm.png │ │ ├── disarmed-pattern-light.png │ │ ├── disarmed-pattern-light.tiff │ │ ├── empty.png │ │ ├── fade.png │ │ ├── goto.png │ │ ├── group-arrow.png │ │ ├── group.png │ │ ├── light.png │ │ ├── load.png │ │ ├── memo.png │ │ ├── mic.png │ │ ├── midi-file.png │ │ ├── midi.png │ │ ├── network.png │ │ ├── new_group_arrow.png │ │ ├── pause.png │ │ ├── pause_circled.png │ │ ├── play_circled.png │ │ ├── playhead.afdesign │ │ ├── playhead.png │ │ ├── reset.png │ │ ├── script.png │ │ ├── start.png │ │ ├── status_broken.png │ │ ├── status_broken_white.png │ │ ├── status_flagged.png │ │ ├── status_loaded.png │ │ ├── status_paused.png │ │ ├── status_running.png │ │ ├── status_spinner.gif │ │ ├── status_spinner.psd │ │ ├── stop.png │ │ ├── target.png │ │ ├── text.png │ │ ├── timecode.png │ │ ├── v5 │ │ │ ├── group-1.png │ │ │ ├── group-2.png │ │ │ ├── group-3.png │ │ │ ├── group-4.png │ │ │ └── group-6.png │ │ ├── video.png │ │ └── wait.png │ ├── info.html │ ├── main.js │ ├── styles.css │ ├── template.ejs │ └── tile.ejs ├── sacn │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── shure │ ├── channel.js │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── watchout │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── x32 │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs └── xair │ ├── icon.png │ ├── info.html │ ├── main.js │ ├── styles.css │ └── template.ejs ├── preload.js └── src ├── assets ├── css │ ├── index.css │ └── plugin_default.css ├── font │ └── MaterialIcons-Regular.ttf └── img │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── outline_add_box_white_18dp.png │ ├── outline_add_white_18dp.png │ ├── outline_broken_image_white_18dp.png │ ├── outline_clear_white_18dp.png │ ├── outline_done_white_18dp.png │ ├── outline_info_white_18dp.png │ ├── outline_link_white_18dp.png │ ├── outline_push_pin_white_18dp.png │ ├── outline_refresh_white_18dp.png │ └── outline_search_white_18dp.png ├── device.js ├── index.js ├── plugins.js ├── saveSlots.js ├── search.js └── view.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | extends: ['airbnb-base', 'prettier'], 9 | parserOptions: { 10 | ecmaVersion: 13, 11 | }, 12 | rules: { 13 | 'import/no-extraneous-dependencies': 0, 14 | 'no-unused-vars': ['error', { args: 'none' }], 15 | 'no-console': 'off', 16 | 'no-alert': 'off', 17 | 'no-plusplus': 'off', 18 | 'import/extensions': 'off', 19 | 'prefer-destructuring': 'off', 20 | 'no-use-before-define': 'off', 21 | 'space-infix-ops': 'warn', 22 | 'no-bitwise': 'off', 23 | 'no-restricted-globals': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Let us know what isn't working 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows 10] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | - package-ecosystem: 'github-actions' 8 | directory: '/' 9 | schedule: 10 | interval: 'weekly' 11 | -------------------------------------------------------------------------------- /.github/release.yaml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | categories: 6 | - title: Plugins 🖥️ 7 | labels: 8 | - plugin 9 | - title: Interface 10 | labels: 11 | - interface 12 | - title: Other Changes 13 | labels: 14 | - '*' 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | workflow_dispatch: 4 | 5 | env: 6 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 7 | 8 | jobs: 9 | build-linux: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | cache: 'npm' 17 | - run: npm ci 18 | - run: npm run release 19 | build-windows: 20 | runs-on: windows-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | cache: 'npm' 27 | - run: npm ci 28 | - run: npm run release 29 | build-macos: 30 | runs-on: macos-13 31 | env: 32 | CSC_LINK: ${{ secrets.MACOS_CSC_LINK }} 33 | CSC_KEY_PASSWORD: ${{ secrets.MACOS_CSC_KEY_PASSWORD }} 34 | APPLE_ID: ${{ secrets.APPLE_ID }} 35 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 36 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: actions/setup-node@v4 40 | with: 41 | node-version: 20 42 | cache: 'npm' 43 | - run: npm ci 44 | - run: npm run release 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.12.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | dist 4 | *.ejs -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | bracketSameLine: true, 7 | printWidth: 120, 8 | }; 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Electron: Main", 8 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 9 | "runtimeArgs": ["--remote-debugging-port=9223", "."], 10 | "windows": { 11 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" 12 | } 13 | }, 14 | { 15 | "name": "Electron: Renderer", 16 | "type": "chrome", 17 | "request": "attach", 18 | "port": 9223, 19 | "webRoot": "${workspaceFolder}", 20 | "timeout": 30000 21 | } 22 | ], 23 | "compounds": [ 24 | { 25 | "name": "Electron: All", 26 | "configurations": ["Electron: Main", "Electron: Renderer"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 6 | }, 7 | "eslint.validate": ["javascript"], 8 | "html.validate.styles": false // accept templates inside HTML style attributes 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # icon Cue View 2 | 3 | A dashboard for everything in your show. 4 | 5 | screenshot 6 | 7 | ## Features 8 | 9 | - Tons of supported equipment 10 | - Auto discover devices on the network 11 | - Live updating 12 | - Configurable layout 13 | 14 | ## Supported Devices 15 | 16 | - QLab 4 & 5 17 | - ETC Eos Consoles 18 | - Watchout 19 | - PJLink Projectors 20 | - X32 Audio Consoles 21 | - XAir Audio Consoles 22 | - Art-Net Universes 23 | - sACN Universes 24 | - ATEM Video Mixers 25 | - Shure ULXD Wireless 26 | - DiGico SD Consoles 27 | - PosiStageNet 28 | - Livestream Studio 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cue View 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | 15 |
16 | 17 |
18 |

Click the Search button to find devices on the network.

19 |
20 | 21 |
22 |
23 | 24 | 27 | 30 | 33 |
34 | 35 |
36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 64 | 65 | 66 | 67 | 68 | 69 | 76 | 77 | 78 | 79 | 80 |
Name: 40 | 41 |
Type: 46 | 47 |
Addr: 52 | 53 |
Port: 58 | 63 | Pin:
Local: 70 | 75 |
81 | 82 |

No Device Selected

83 |
84 |
85 |
86 | 87 |
88 |
89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /networkInterfaces.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Network Information 4 | 76 | 77 | 78 |

Attached Network Interfaces:

79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
IDIP AddressSubnet MaskSearchable Address Ranges
89 | 90 | 93 | -------------------------------------------------------------------------------- /networkInterfaces.js: -------------------------------------------------------------------------------- 1 | const SEARCH = require('./src/search.js'); 2 | 3 | window.init = function init() { 4 | const networkInterfaces = SEARCH.getNetworkInterfaces(); 5 | let html = ''; 6 | 7 | for (let i = 0; i < Object.keys(networkInterfaces).length; i++) { 8 | const interfaceID = Object.keys(networkInterfaces)[i]; 9 | const interfaceObj = networkInterfaces[interfaceID]; 10 | 11 | html += ` 12 | 13 | ${interfaceID} 14 | ${interfaceObj[0].address} 15 | ${interfaceObj[0].netmask} 16 | `; 17 | 18 | if (interfaceObj[0].searchTruncated) { 19 | html += ``; 20 | } else { 21 | html += ``; 22 | } 23 | 24 | html += ` 25 | ${interfaceObj[0].firstSearchAddress} - ${interfaceObj[0].lastSearchAddress} 26 | 27 | `; 28 | 29 | document.getElementById('network-interfaces').innerHTML = html; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cue-view", 3 | "productName": "Cue View", 4 | "version": "1.2.2", 5 | "description": "A dashboard for everything in your show", 6 | "main": "main.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "pretty": "prettier --write **/*", 10 | "start": "electron .", 11 | "build": "electron-builder -m -w -l", 12 | "build:mac": "electron-builder -m", 13 | "build:windows": "electron-builder -w", 14 | "build:linux": "electron-builder -l", 15 | "release": "electron-builder" 16 | }, 17 | "author": "alec@stagehacks.com", 18 | "license": "CC BY-SA 4.0", 19 | "homepage": "https://github.com/stagehacks/Cue-View", 20 | "repository": "https://github.com/stagehacks/Cue-View", 21 | "devDependencies": { 22 | "@electron/notarize": "^2.5.0", 23 | "electron": "^34.2.0", 24 | "electron-builder": "^25.1.8", 25 | "eslint": "^8.57.0", 26 | "eslint-config-airbnb-base": "^15.0.0", 27 | "eslint-config-prettier": "^9.1.0", 28 | "eslint-plugin-import": "^2.31.0", 29 | "prettier": "^3.4.2" 30 | }, 31 | "dependencies": { 32 | "@jwetzell/posistagenet": "^1.1.0", 33 | "atem-connection": "^3.5.0", 34 | "bonjour": "^3.5.0", 35 | "electron-updater": "^6.3.9", 36 | "lodash": "^4.17.21", 37 | "md5": "^2.3.0", 38 | "netmask": "^2.0.2", 39 | "osc": "^2.4.5", 40 | "uuid": "^11.0.5" 41 | }, 42 | "build": { 43 | "appId": "com.stagehacks.cueview", 44 | "icon": "src/assets/img/", 45 | "artifactName": "${name}.${os}-${arch}.v${version}.${ext}", 46 | "npmRebuild": false, 47 | "mac": { 48 | "category": "Utilities", 49 | "icon": "src/assets/img/icon.icns", 50 | "hardenedRuntime": true, 51 | "electronLanguages": [ 52 | "en" 53 | ], 54 | "target": [ 55 | { 56 | "target": "zip", 57 | "arch": [ 58 | "x64", 59 | "arm64" 60 | ] 61 | } 62 | ], 63 | "publish": [ 64 | "github" 65 | ] 66 | }, 67 | "win": { 68 | "target": "NSIS", 69 | "icon": "src/assets/img/icon.ico", 70 | "publish": [ 71 | "github" 72 | ] 73 | }, 74 | "nsis": { 75 | "oneClick": false 76 | }, 77 | "linux": { 78 | "target": [ 79 | { 80 | "target": "AppImage", 81 | "arch": [ 82 | "x64", 83 | "arm64", 84 | "armv7l" 85 | ] 86 | } 87 | ], 88 | "maintainer": "alec@stagehacks.com", 89 | "category": "Utility", 90 | "publish": [ 91 | "github" 92 | ] 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /plugins/artnet/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/artnet/icon.png -------------------------------------------------------------------------------- /plugins/artnet/info.html: -------------------------------------------------------------------------------- 1 |

Quit any other applications that use Art-Net before using this plugin.

2 |

Art-Net™ Designed by and Copyright Artistic Licence Holdings Ltd

3 | -------------------------------------------------------------------------------- /plugins/artnet/main.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | defaultName: 'Art-Net', 3 | connectionType: 'UDPsocket', 4 | remotePort: 6454, 5 | mayChangePorts: false, 6 | heartbeatInterval: 5000, 7 | heartbeatTimeout: 15000, 8 | searchOptions: { 9 | type: 'UDPsocket', 10 | searchBuffer: Buffer.from([0x00]), 11 | devicePort: 6454, 12 | listenPort: 6454, 13 | validateResponse(msg, info, devices) { 14 | return msg.toString('utf8', 0, 7) === 'Art-Net'; 15 | }, 16 | }, 17 | }; 18 | 19 | exports.ready = function ready(_device) { 20 | const device = _device; 21 | device.data.universes = {}; 22 | device.data.orderedUniverses = []; 23 | }; 24 | 25 | exports.data = function data(_device, buf) { 26 | const device = _device; 27 | 28 | if (buf.length < 18 || buf.slice(0, 7).toString() !== 'Art-Net') { 29 | return; 30 | } 31 | 32 | const opCode = buf.readUInt16BE(8); 33 | 34 | if (opCode === 33) { 35 | sendArtPollReply(device); 36 | return; 37 | } 38 | 39 | const universeIndex = buf.readUInt8(14); 40 | 41 | let universe = device.data.universes[universeIndex]; 42 | 43 | if (!universe) { 44 | device.data.universes[universeIndex] = {}; 45 | universe = device.data.universes[universeIndex]; 46 | } 47 | 48 | universe.sequence = buf.readUInt8(12); 49 | universe.subnet = buf.readUInt8(15); 50 | universe.opCode = buf.readUInt8(9); 51 | universe.version = buf.readUInt16BE(10); 52 | universe.slots = buf.slice(18); 53 | device.data.ip = device.addresses[0]; 54 | 55 | if (!device.data.orderedUniverses.includes(universeIndex)) { 56 | device.data.orderedUniverses.push(universeIndex); 57 | device.data.orderedUniverses.sort(); 58 | universe.slotElems = []; 59 | universe.slotElemsSet = false; 60 | 61 | device.draw(); 62 | device.update('elementCache'); 63 | } 64 | 65 | device.update('universeData', { 66 | universeIndex, 67 | universe, 68 | }); 69 | }; 70 | 71 | exports.heartbeat = function heartbeat(device) {}; 72 | 73 | let lastUpdate = Date.now(); 74 | exports.update = function update(_device, _document, updateType, updateData) { 75 | const device = _device; 76 | const data = updateData; 77 | const document = _document; 78 | 79 | if (updateType === 'universeData' && data.universe) { 80 | if (Date.now() - lastUpdate > 1000) { 81 | lastUpdate = Date.now(); 82 | device.update('elementCache'); 83 | } 84 | 85 | const $elem = document.getElementById(`universe-${data.universeIndex}`); 86 | 87 | if ($elem && data.universe.slotElemsSet) { 88 | for (let i = 0; i < 512; i++) { 89 | data.universe.slotElems[i].textContent = data.universe.slots[i]; 90 | } 91 | 92 | document.getElementById(`universe-${data.universeIndex}-sequence`).textContent = data.universe.sequence; 93 | } else { 94 | device.draw(); 95 | device.update('elementCache'); 96 | } 97 | } else if (updateType === 'elementCache') { 98 | device.data.orderedUniverses.forEach((universeIndex) => { 99 | for (let i = 0; i < 512; i++) { 100 | device.data.universes[universeIndex].slotElems[i] = document.getElementById(`${universeIndex}-${i}`); 101 | } 102 | device.data.universes[universeIndex].slotElemsSet = true; 103 | }); 104 | } 105 | }; 106 | 107 | function sendArtPollReply(device) { 108 | // minimum viable Art-Net packet 109 | // not to full Art-Net spec 110 | // https://art-net.org.uk/how-it-works/discovery-packets/artpollreply/ 111 | 112 | const interfaces = device.getNetworkInterfaces(); 113 | 114 | for (let i = 0; i < Object.keys(interfaces).length; i++) { 115 | const buf = Buffer.alloc(213); 116 | buf.write('Art-Net', 0); 117 | 118 | buf.writeInt16LE(0x2100, 8); 119 | 120 | const addr = interfaces[Object.keys(interfaces)[i]][0].address.split('.'); 121 | 122 | buf.writeUInt8(addr[0], 10); 123 | buf.writeUInt8(addr[1], 11); 124 | buf.writeUInt8(addr[2], 12); 125 | buf.writeUInt8(addr[3], 13); 126 | 127 | buf.writeInt16LE(6454, 14); 128 | 129 | buf.write('Cue View', 26); 130 | 131 | buf.writeUInt8(4, 173); 132 | 133 | buf.writeUInt8(0xc0, 174); 134 | buf.writeUInt8(0xc0, 175); 135 | buf.writeUInt8(0xc0, 176); 136 | buf.writeUInt8(0xc0, 177); 137 | 138 | buf.writeUInt8(0x80, 178); 139 | buf.writeUInt8(0x80, 179); 140 | buf.writeUInt8(0x80, 180); 141 | buf.writeUInt8(0x80, 181); 142 | 143 | buf.writeUInt8(0x80, 182); 144 | buf.writeUInt8(0x80, 183); 145 | buf.writeUInt8(0x80, 184); 146 | buf.writeUInt8(0x80, 185); 147 | 148 | buf.writeUInt8(0x00, 186); 149 | buf.writeUInt8(0x01, 187); 150 | buf.writeUInt8(0x02, 188); 151 | buf.writeUInt8(0x03, 189); 152 | 153 | device.send(buf); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /plugins/artnet/styles.css: -------------------------------------------------------------------------------- 1 | table { 2 | margin-bottom: 50px; 3 | background-color: #333; 4 | } 5 | td, 6 | th { 7 | width: 35px; 8 | } 9 | td { 10 | background-color: black; 11 | text-align: center; 12 | border-radius: 3px; 13 | color: white; 14 | font-size: 13px; 15 | } 16 | th { 17 | background-color: #222; 18 | color: #ccc; 19 | font-size: 11px; 20 | } 21 | td.data { 22 | padding: 7px; 23 | font-size: 12px; 24 | text-align: left; 25 | } 26 | td.data em { 27 | font-size: 14px; 28 | color: #008bd2; 29 | } 30 | -------------------------------------------------------------------------------- /plugins/artnet/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= data.source %> Art-Net

3 |
4 | 5 | 6 |
7 | <% data.orderedUniverses.forEach(universeIndex => { 8 | let universe = data.universes[universeIndex]; 9 | %> 10 | 11 | 12 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | <% for(col=1; col<=16; col++){ %> 43 | 44 | <% } %> 45 | 46 | 47 | <% let slot = 0; %> 48 | <% for(row=0; row<32; row++){ %> 49 | 50 | 51 | <% for(col=0; col<16; col++){ %> 52 | <% slot++%> 53 | 54 | <% } %> 55 | 56 | <% } %> 57 |
14 | Universe 15 |
<%= universeIndex %> 16 |
18 | Sub-Net 19 |
<%= universe.subnet %> 20 |
22 | Sequence 23 |
<%= universe.sequence %> 24 |
26 | OpCode 27 |
<%= universe.opCode %> 28 |
30 | Version 31 |
<%= universe.version %> 32 |
34 | IP 35 |
<%= data.ip %> 36 |
<%= col %>
<%= row*16+1 %><%= universe.slots[slot-1] %>
58 | <% }); %> 59 |
-------------------------------------------------------------------------------- /plugins/atem/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/icon.png -------------------------------------------------------------------------------- /plugins/atem/img/button_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/button_green.png -------------------------------------------------------------------------------- /plugins/atem/img/button_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/button_off.png -------------------------------------------------------------------------------- /plugins/atem/img/button_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/button_red.png -------------------------------------------------------------------------------- /plugins/atem/img/button_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/button_white.png -------------------------------------------------------------------------------- /plugins/atem/img/button_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/button_yellow.png -------------------------------------------------------------------------------- /plugins/atem/img/tbar_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/tbar_bg.png -------------------------------------------------------------------------------- /plugins/atem/img/tbar_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/atem/img/tbar_handle.png -------------------------------------------------------------------------------- /plugins/atem/info.html: -------------------------------------------------------------------------------- 1 |

ATEM Configuration

2 | 6 | 7 |

ATEM Software Download

8 | 11 | (Look for ATEM Switchers Update) 12 | -------------------------------------------------------------------------------- /plugins/atem/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #282828; 3 | } 4 | h1 { 5 | font-family: 'Open Sans', sans-serif; 6 | } 7 | h3 { 8 | color: #6d6d6d !important; 9 | margin: 30px 0px 10px 9px; 10 | font-size: 0.9em; 11 | font-weight: 400; 12 | font-family: 'Open Sans', sans-serif; 13 | } 14 | 15 | .me-label { 16 | margin-top: 0; 17 | margin-bottom: 2px; 18 | } 19 | 20 | .atem-input { 21 | width: 52px; 22 | height: 52px; 23 | background-image: url('img/button_white.png'); 24 | } 25 | 26 | .source-wrapper { 27 | display: flex; 28 | flex-wrap: wrap; 29 | background-color: #1f1f1f; 30 | border: #1a1a1a 2px solid; 31 | border-radius: 8px; 32 | max-width: 416px; 33 | padding: 12px; 34 | } 35 | 36 | .atem-red { 37 | background-image: url('img/button_red.png'); 38 | box-shadow: 0px 0px 10px 1px #ff0000; 39 | z-index: 10000; 40 | } 41 | 42 | .atem-green { 43 | background-image: url('img/button_green.png'); 44 | box-shadow: 0px 0px 10px 1px #06c300; 45 | z-index: 10000; 46 | } 47 | 48 | .atem-yellow { 49 | background-image: url('img/button_yellow.png'); 50 | box-shadow: 0px 0px 10px 1px #c7ca00; 51 | z-index: 10000; 52 | } 53 | 54 | .atem-disabled { 55 | background-image: url('img/button_off.png'); 56 | color: #3a3a3a; 57 | } 58 | 59 | .atem-gray { 60 | color: #575757; 61 | } 62 | 63 | .source-label { 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | height: 100%; 68 | font-size: 11px; 69 | font-weight: bold; 70 | } 71 | 72 | .transition-container { 73 | display: flex; 74 | flex-wrap: wrap; 75 | justify-content: space-between; 76 | } 77 | 78 | .transition-settings { 79 | display: flex; 80 | flex-direction: column; 81 | justify-content: space-between; 82 | margin-right: 30px; 83 | } 84 | 85 | .tbar-container { 86 | display: flex; 87 | flex-direction: column-reverse; 88 | background-color: #1f1f1f; 89 | border: 2px solid; 90 | border-color: #1a1a1a; 91 | border-radius: 6px; 92 | width: 88px; 93 | height: 429px; 94 | position: relative; 95 | margin-top: 58px; 96 | margin-right: 60px; 97 | } 98 | 99 | .tbar-bg { 100 | position: absolute; 101 | background-image: url('img/tbar_bg.png'); 102 | width: 88px; 103 | height: 429px; 104 | } 105 | .tbar-handle { 106 | position: absolute; 107 | background-image: url('img/tbar_handle.png'); 108 | width: 126px; 109 | height: 50px; 110 | left: -3px; 111 | } 112 | .tbar-div { 113 | width: 100%; 114 | background-color: #7aff58; 115 | overflow: hidden; 116 | } 117 | 118 | .dsk { 119 | width: 100%; 120 | } 121 | 122 | .dsk .source-wrapper { 123 | display: flex; 124 | flex-direction: column; 125 | align-content: center; 126 | width: 100%; 127 | padding: 5px; 128 | } 129 | 130 | .dsk h3 { 131 | text-align: center; 132 | } 133 | 134 | .fade-to-black-container { 135 | display: flex; 136 | flex-direction: column; 137 | align-items: flex-end; 138 | margin-right: 30px; 139 | } 140 | 141 | .fade-to-black .source-wrapper { 142 | display: flex; 143 | width: 100%; 144 | padding: 5px; 145 | } 146 | 147 | .fade-to-black h3 { 148 | text-align: center; 149 | } 150 | .rate-heading { 151 | text-align: center; 152 | font-size: small; 153 | margin-bottom: 4px; 154 | } 155 | 156 | .dsk-rate, 157 | .ftb-rate, 158 | .transition-rate { 159 | display: flex; 160 | justify-content: center; 161 | flex-direction: column; 162 | } 163 | 164 | .dsk-rate-label, 165 | .ftb-rate-label, 166 | .transition-rate-label { 167 | color: rgb(235, 110, 0); 168 | background-color: black; 169 | border-radius: 3px; 170 | padding: 6px 2px; 171 | text-align: center; 172 | } 173 | 174 | .clear { 175 | background: transparent; 176 | border-color: transparent; 177 | background-color: transparent; 178 | } 179 | 180 | .hide { 181 | display: none; 182 | } 183 | 184 | .no-wrap { 185 | flex-wrap: nowrap; 186 | } 187 | 188 | .show-small { 189 | display: none; 190 | } 191 | 192 | @media screen and (min-width: 0px) and (max-width: 480px) { 193 | .hide-small { 194 | display: none; 195 | } 196 | .show-small { 197 | display: block; 198 | } 199 | } 200 | 201 | .float-left { 202 | float: left; 203 | } 204 | -------------------------------------------------------------------------------- /plugins/digico/fader.ejs: -------------------------------------------------------------------------------- 1 | 2 | <% if(fader && fader.meter>0){ %> 3 | 4 | <%=index%> 5 | 6 | <% }else{ %> 7 | <%=index%> 8 | <% } %> 9 | 10 | <% if(type=="Control_Groups"){ %> 11 | 12 | <%= fader ? fader.name : "-" %> 13 | 14 | <% }else if(type=="Input_Channels"){ %> 15 | 16 | <%= fader ? (fader.Channel_Input ? fader.Channel_Input.name: "") : "" %> 17 | 18 | <% }else if(type=="Group_Outputs"){ %> 19 | 20 | <%= fader ? (fader.Buss_Trim ? fader.Buss_Trim.name: "") : "" %> 21 | 22 | <% }else if(type=="Aux_Outputs"){ %> 23 | 24 | <%= fader ? (fader.Buss_Trim ? fader.Buss_Trim.name: "") : "" %> 25 | 26 | <% } %> 27 | 28 | 29 | 30 | <% if(fader){ %> 31 | <% let percent = (fader.fader-10)*1.5+100 %> 32 | 33 | <% if(fader.fader>-100){ %> 34 | <%= fader ? Math.round(fader.fader*10.0)/10.0 : '-' %>db 35 | <% } %> 36 | 37 | <% }else{ %> 38 | - 39 | <% } %> 40 | 41 | 42 | -------------------------------------------------------------------------------- /plugins/digico/icon.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/digico/icon.afphoto -------------------------------------------------------------------------------- /plugins/digico/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/digico/icon.png -------------------------------------------------------------------------------- /plugins/digico/info.html: -------------------------------------------------------------------------------- 1 | Cue View emulates to be the DiGiCo SD iPad app. 2 |
    3 |
  1. On the SD console open Master ScreenSetupExternal Control
  2. 4 |
  3. Enable External Control by pressing the button at the top of the panel
  4. 5 |
  5. Press the add device button and select DiGiCo Pad
  6. 6 |
  7. Enter a Device Name for Cue View and then enter the IP Address of this computer
  8. 7 |
  9. Enter Send and Receive Port numbers
  10. 8 | 12 |
  11. Tick the Enable column for this device
  12. 13 |
  13. 14 | Press the Load button in the bottom right corner of the panel and select the commands button for the 15 | relevant console 16 |
  14. 17 |
18 | -------------------------------------------------------------------------------- /plugins/digico/main.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | exports.config = { 6 | defaultName: 'DiGiCo SD', 7 | connectionType: 'osc-udp', 8 | remotePort: 8000, 9 | localPort: 8001, 10 | mayChangePorts: true, 11 | heartbeatInterval: 2000, 12 | heartbeatTimeout: 11000, 13 | searchOptions: { 14 | type: 'UDPsocket', 15 | // send "/Console/Name/?" this is what the iPad sends to check connection 16 | searchBuffer: Buffer.from([ 17 | 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, 0x4e, 0x61, 0x6d, 0x65, 0x2f, 0x3f, 0x00, 0x2c, 0x00, 0x00, 18 | 0x00, 19 | ]), 20 | devicePort: 8000, 21 | listenPort: 9000, 22 | validateResponse(msg, info) { 23 | console.log(msg); 24 | console.log(msg.toString()); 25 | return msg.toString().includes('/Console'); 26 | }, 27 | }, 28 | }; 29 | 30 | exports.ready = function ready(_device) { 31 | const device = _device; 32 | device.data = new DiGiCo(); 33 | 34 | device.send('/Console/Name/?'); 35 | device.send('/Console/Session/Filename/?'); 36 | device.send('/Console/Channels/?'); 37 | device.send('/Snapshots/Current_Snapshot/?'); 38 | device.send('/Meters/clear'); 39 | }; 40 | exports.data = function data(_device, oscData) { 41 | const device = _device; 42 | this.deviceInfoUpdate(device, 'status', 'ok'); 43 | 44 | const properties = oscData.address.split('/'); 45 | properties.shift(); 46 | 47 | if (properties[0] !== 'Meters') { 48 | setObjectProperty(device.data, properties, oscData.args); 49 | // console.log(properties); 50 | } 51 | 52 | if (properties[0] === 'Console' && properties[1] === 'Control_Groups') { 53 | for (let i = 1; i <= device.data.Console.Control_Groups; i++) { 54 | device.send(`/Control_Groups/${i}/?`); 55 | } 56 | device.draw(); 57 | } else if (properties[0] === 'Console' && properties[1] === 'Input_Channels') { 58 | for (let i = 1; i <= device.data.Console.Input_Channels; i++) { 59 | device.send(`/Input_Channels/${i}/Channel_Input/name/?`); 60 | device.send(`/Input_Channels/${i}/mute/?`); 61 | device.send(`/Input_Channels/${i}/solo/?`); 62 | device.send(`/Input_Channels/${i}/fader/?`); 63 | device.send(`/Meters/request/1${i}`, [ 64 | { type: 's', value: `/Input_Channels/${i}/Channel_Input/post_meter/left` }, 65 | ]); 66 | } 67 | device.draw(); 68 | } else if (properties[0] === 'Console' && properties[1] === 'Group_Outputs') { 69 | for (let i = 1; i <= device.data.Console.Group_Outputs; i++) { 70 | device.send(`/Group_Outputs/${i}/mute/?`); 71 | device.send(`/Group_Outputs/${i}/solo/?`); 72 | device.send(`/Group_Outputs/${i}/fader/?`); 73 | device.send(`/Group_Outputs/${i}/Buss_Trim/name/?`); 74 | device.send(`/Meters/request/3${i}`, [{ type: 's', value: `/Group_Outputs/${i}/fader_meter/left` }]); 75 | } 76 | device.draw(); 77 | } else if (properties[0] === 'Console' && properties[1] === 'Aux_Outputs') { 78 | for (let i = 1; i <= device.data.Console.Aux_Outputs; i++) { 79 | device.send(`/Aux_Outputs/${i}/mute/?`); 80 | device.send(`/Aux_Outputs/${i}/solo/?`); 81 | device.send(`/Aux_Outputs/${i}/fader/?`); 82 | device.send(`/Aux_Outputs/${i}/Buss_Trim/name/?`); 83 | device.send(`/Meters/request/4${i}`, [{ type: 's', value: `/Aux_Outputs/${i}/fader_meter/left` }]); 84 | } 85 | device.draw(); 86 | } else if (properties[0] === 'Console' && properties[1] === 'Name') { 87 | this.deviceInfoUpdate(device, 'defaultName', device.data.Console.Name); 88 | } else if (['Control_Groups', 'Input_Channels', 'Group_Outputs', 'Aux_Outputs'].includes(properties[0])) { 89 | device.update('fader', { 90 | type: properties[0], 91 | channel: properties[1], 92 | }); 93 | } else if (properties[0] === 'Snapshots') { 94 | device.update('updateSnapshot', { 95 | snapshots: device.data.Snapshots, 96 | }); 97 | } else if (properties[0] === 'Meters' && properties[1] === 'values') { 98 | for (let i = 0; i < oscData.args.length; i += 2) { 99 | const channelType = oscData.args[i].toString()[0]; 100 | const channelNumber = oscData.args[i].toString().substring(1); 101 | const meterValue = BigInt(oscData.args[i + 1]) / -40000n + 100n; 102 | 103 | if (channelType === '1' && device.data.Input_Channels[channelNumber]) { 104 | device.data.Input_Channels[channelNumber].meter = meterValue; 105 | device.update('fader', { 106 | type: 'Input_Channels', 107 | channel: channelNumber, 108 | }); 109 | } else if (channelType === '3' && device.data.Group_Outputs[channelNumber]) { 110 | device.data.Group_Outputs[channelNumber].meter = meterValue; 111 | device.update('fader', { 112 | type: 'Group_Outputs', 113 | channel: channelNumber, 114 | }); 115 | } else if (channelType === '4' && device.data.Aux_Outputs[channelNumber]) { 116 | device.data.Aux_Outputs[channelNumber].meter = meterValue; 117 | device.update('fader', { 118 | type: 'Aux_Outputs', 119 | channel: channelNumber, 120 | }); 121 | } 122 | } 123 | } 124 | }; 125 | 126 | exports.heartbeat = function heartbeat(device) { 127 | device.send('/Console/Name/?'); 128 | console.log(device.data); 129 | 130 | // Need to do a /Meters/clear when quitting Cue View or closing the View 131 | }; 132 | 133 | class DiGiCo { 134 | constructor() { 135 | this.Console = {}; 136 | this.Input_Channels = {}; 137 | this.Aux_Outputs = {}; 138 | this.Control_Groups = {}; 139 | this.Group_Outputs = {}; 140 | this.Matrix_Outputs = {}; 141 | this.Layout = {}; 142 | } 143 | } 144 | 145 | /* 146 | This function takes somethings like this /Console/Session/Filename with a osc args like ['session.ses'] 147 | and turns it into a nested object like. An easy way to set properties converting OSC paths to properties 148 | { 149 | Console: { 150 | Session: { 151 | Filename: 'session.ses' 152 | } 153 | } 154 | } 155 | 156 | */ 157 | function setObjectProperty(_object, _properties, value) { 158 | let object = _object; 159 | const properties = _properties; 160 | 161 | for (let i = 0; i < properties.length - 1; i++) { 162 | const property = properties[i]; 163 | if (object[property] === undefined) { 164 | object[property] = {}; 165 | } 166 | object = object[property]; 167 | } 168 | const property = properties[properties.length - 1]; 169 | if (Array.isArray(value)) { 170 | // if this is an array of length one just set the property to the contents so ['string'] becomes 'string' 171 | if (value.length === 1) { 172 | object[property] = value[0]; 173 | return; 174 | } 175 | } 176 | object[property] = value; 177 | } 178 | 179 | const faderTemplate = _.template(fs.readFileSync(path.join(__dirname, `/fader.ejs`))); 180 | exports.update = function update(device, doc, updateType, data) { 181 | if (updateType === 'fader') { 182 | const $elem = doc.getElementById(`${data.type}-${data.channel}`); 183 | $elem.outerHTML = faderTemplate({ 184 | type: data.type, 185 | fader: device.data[data.type][data.channel], 186 | index: data.channel, 187 | }); 188 | } else if (updateType === 'snapshot') { 189 | const $elem = doc.getElementById(`snapshot`); 190 | $elem.textContent = `${data.snapshots.Current_Snapshot}`; 191 | } 192 | }; 193 | -------------------------------------------------------------------------------- /plugins/digico/styles.css: -------------------------------------------------------------------------------- 1 | .column { 2 | float: left; 3 | margin: 5px; 4 | } 5 | .section { 6 | border: #333 3px solid; 7 | border-bottom: none; 8 | border-top-left-radius: 10px; 9 | border-top-right-radius: 10px; 10 | padding: 10px; 11 | margin-bottom: 30px; 12 | } 13 | .section h3 { 14 | font-weight: 300; 15 | margin-top: 0px; 16 | color: #aaa !important; 17 | } 18 | table { 19 | border-collapse: collapse; 20 | display: block; 21 | } 22 | .fader:nth-child(even) { 23 | background: #1a1a1a; 24 | } 25 | .fader:nth-child(odd) { 26 | background: #1f1f1f; 27 | } 28 | .fader td { 29 | text-align: center; 30 | width: 30px; 31 | height: 20px; 32 | border: black 1px solid; 33 | } 34 | .fader .fader-channel { 35 | color: #777; 36 | font-size: 10px; 37 | } 38 | .fader .fader-name { 39 | width: 80px; 40 | color: black; 41 | font-family: monospace; 42 | } 43 | .fader .lcd-control-group { 44 | background-color: rgb(66, 132, 255); 45 | } 46 | .fader .lcd-channel-input { 47 | background-color: rgb(135, 175, 235); 48 | } 49 | .fader .lcd-group-output { 50 | background-color: rgb(241, 71, 48); 51 | } 52 | .fader .lcd-aux-output { 53 | background-color: rgb(197, 0, 187); 54 | } 55 | .fader .fader-name.state-1 { 56 | background-color: rgb(0, 185, 0); 57 | } 58 | .fader .fader-mute { 59 | width: 10px; 60 | background-color: #444; 61 | } 62 | .fader .fader-mute.state-1 { 63 | background-color: red; 64 | } 65 | .fader .fader-fader { 66 | text-align: left; 67 | width: 70px; 68 | padding: 0px 10px; 69 | color: rgb(68, 68, 68); 70 | } 71 | 72 | #snapshot { 73 | font-size: 36px; 74 | font-weight: bold; 75 | font-family: 'Arial Black', 'Impact'; 76 | } 77 | -------------------------------------------------------------------------------- /plugins/digico/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName %>

3 |

<%= data.Console.Session.Filename %>

4 |
5 | 6 | <% 7 | const fs = require('fs'); 8 | let _ = require('lodash'); 9 | const path = require('path'); 10 | 11 | let faderTemplate = _.template(fs.readFileSync(path.join(__dirname, `/plugins/digico/fader.ejs`))); 12 | %> 13 | 14 |
15 |
16 |

snapshot

17 |
18 | 19 |
20 |

control groups

21 | 22 | <% for(let i=1; i<=data.Console.Control_Groups; i++){ %> 23 | <%= faderTemplate({type: "Control_Groups", fader: data.Control_Groups[i], index: i}) %> 24 | <% } %> 25 |
26 |
27 | 28 |
29 |

group outputs

30 | 31 | <% for(let i=1; i<=data.Console.Group_Outputs; i++){ %> 32 | <%= faderTemplate({type: "Group_Outputs", fader: data.Group_Outputs[i], index: i}) %> 33 | <% } %> 34 |
35 |
36 | 37 |
38 |

aux outputs

39 | 40 | <% for(let i=1; i<=data.Console.Aux_Outputs; i++){ %> 41 | <%= faderTemplate({type: "Aux_Outputs", fader: data.Aux_Outputs[i], index: i}) %> 42 | <% } %> 43 |
44 |
45 |
46 | 47 |
48 |

input channels

49 | 50 | <% for(let i=1; i<=data.Console.Input_Channels; i++){ %> 51 | <%= faderTemplate({type: "Input_Channels", fader: data.Input_Channels[i], index: i}) %> 52 | <% } %> 53 |
54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /plugins/eos/cue.ejs: -------------------------------------------------------------------------------- 1 | 2 | <% for(var partNumber in q){ %> 3 | <% var part = q[partNumber] %> 4 | <%if(part.scene.length>0 && partNumber==0){ %> 5 | 6 | 7 | 8 |
9 | <%= part.scene %> 10 | 11 | 12 | <% } %> 13 | 14 | <% if(isActive){ %> 15 | 16 | <% }else{ %> 17 | 18 | 19 | <% } %> 20 | 21 | <% if(partNumber!=0){ %> 22 | 23 |
P<%= partNumber %>
24 | 25 | <% }else{ %> 26 |
<%= cueNumber %>
27 | <% } %> 28 | 29 | <% if(partNumber !== 0 || part.partCount === 0){ %> 30 | 31 | 32 | <% if(part.uptimeDuration === part.downtimeDuration || part.downtimeDuration === -1){ %> 33 | 34 | <%= part.prettyDuration(part.uptimeDelay) || "" %> 35 | <%= part.prettyDuration(part.uptimeDuration, true) %> 36 | 37 | <% }else{ %> 38 | 39 | <%= part.prettyDuration(part.uptimeDelay) || "" %> 40 | <%= part.prettyDuration(part.uptimeDuration, true) %> 41 | 42 | 43 | <%= part.prettyDuration(part.downtimeDuration, true) %> 44 | 45 | <% } %> 46 | 47 | 48 | <%= part.prettyDuration(part.focusTimeDuration, true) %> 49 | 50 | 51 | <%= part.prettyDuration(part.colorTimeDuration, true) %> 52 | 53 | 54 | <%= part.prettyDuration(part.beamTimeDuration, true) %> 55 | 56 | <%= part.prettyDuration(part.duration) %> 57 | 58 | <% }else{ %> 59 | 60 | 61 | 62 | 63 | 64 | <% } %> 65 | 66 | <%= part.mark %> 67 | <%= part.block %> 68 | <%= part.assert %> 69 | 70 | <%= (part.follow>=0)? "F"+part.prettyDuration(part.follow) : "" %> 71 | <%= (part.hang>=0)? "H"+part.prettyDuration(part.hang) : "" %> 72 | 73 | 74 | <%= (partNumber==0 ? "": "   ") %> 75 | <%= part.label %> 76 | 77 | <%= part.extLinks %> 78 | 79 | <% } %> 80 | 81 | -------------------------------------------------------------------------------- /plugins/eos/cue.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | class Cue { 3 | constructor(args) { 4 | this.uid = args[1]; 5 | this.label = args[2]; 6 | this.uptimeDuration = args[3]; 7 | this.uptimeDelay = args[4]; 8 | this.downtimeDuration = args[5]; 9 | this.downtimeDelay = args[6]; 10 | this.focusTimeDuration = args[7]; 11 | this.focusTimeDelay = args[8]; 12 | this.colorTimeDuration = args[9]; 13 | this.colorTimeDelay = args[10]; 14 | this.beamTimeDuration = args[11]; 15 | this.beamTimeDelay = args[12]; 16 | this.mark = args[16]; 17 | this.block = args[17]; 18 | this.assert = args[18]; 19 | this.follow = args[20]; 20 | this.hang = args[21]; 21 | this.partCount = args[26]; 22 | this.scene = args[28]; 23 | this.duration = Math.max(args[3], args[5], args[7], args[9], args[11]); 24 | } 25 | 26 | prettyDuration(milliseconds, box) { 27 | if (milliseconds === -1) { 28 | return ''; 29 | } 30 | 31 | const num = Math.round(milliseconds / 100) / 10; 32 | if (box) { 33 | return `
${num}
`; 34 | } 35 | 36 | return num; 37 | } 38 | } 39 | 40 | module.exports = Cue; 41 | -------------------------------------------------------------------------------- /plugins/eos/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/eos/icon.png -------------------------------------------------------------------------------- /plugins/eos/info.html: -------------------------------------------------------------------------------- 1 |

Connection Requirements: Eos 3.2 and newer

2 |

Browser: Setup → Device Settings → Network

3 | 7 | 8 |

 

9 | 10 |

Connection Requirements: Eos 3.1 and earlier

11 |

Shell/ECU → Network

12 | 16 | If the "Third Party OSC" option is not available, Eos must be updated to 3.1 or newer. 17 | 18 |

Browser: System Settings → Show Control → OSC

19 | 25 | -------------------------------------------------------------------------------- /plugins/eos/main.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const Cue = require('./cue'); 5 | 6 | exports.config = { 7 | defaultName: 'ETC Eos', 8 | connectionType: 'osc', 9 | defaultPort: 3037, 10 | mayChangePorts: true, 11 | heartbeatInterval: 5000, 12 | heartbeatTimeout: 6000, 13 | searchOptions: { 14 | type: 'TCPport', 15 | searchBuffer: Buffer.from('\xc0/eos/ping\x00\x00\x2c\x00\x00\x00\xc0', 'ascii'), 16 | testPort: 3037, 17 | validateResponse(msg, info) { 18 | return msg.toString().includes('/eos/out'); 19 | }, 20 | }, 21 | fields: [ 22 | { 23 | key: 'cueListFilter', 24 | label: 'Q List', 25 | type: 'numberinput', 26 | value: '', 27 | action(_device) { 28 | const device = _device; 29 | device.data.cueListFilter = device.fields.cueListFilter; 30 | device.draw(); 31 | }, 32 | }, 33 | ], 34 | }; 35 | 36 | exports.ready = function ready(_device) { 37 | const device = _device; 38 | device.data.EOS = new EOS(); 39 | device.templates = { 40 | cue: _.template(fs.readFileSync(path.join(__dirname, `cue.ejs`))), 41 | }; 42 | device.data.cueListFilter = device.fields.cueListFilter; 43 | 44 | device.send('/eos/get/cuelist/count'); 45 | device.send('/eos/get/version'); 46 | device.send('/eos/subscribe', [{ type: 'i', value: 1 }]); 47 | }; 48 | 49 | exports.data = function data(_device, osc) { 50 | const device = _device; 51 | this.deviceInfoUpdate(device, 'status', 'ok'); 52 | 53 | const addressParts = osc.address.split('/'); 54 | addressParts.shift(); 55 | 56 | if (match(addressParts, ['eos', 'out', 'show', 'name'])) { 57 | device.data.EOS.showName = osc.args[0]; 58 | device.data.EOS.cueLists = {}; 59 | this.deviceInfoUpdate(device, 'defaultName', osc.args[0]); 60 | } else if (match(addressParts, ['eos', 'out', 'get', 'cuelist', 'count'])) { 61 | for (let i = 0; i < osc.args[0]; i++) { 62 | device.send(`/eos/get/cuelist/index/${i}`); 63 | } 64 | } else if (match(addressParts, ['eos', 'out', 'get', 'cuelist', '*', 'list', '*', '*'])) { 65 | device.data.EOS.cueLists[addressParts[4]] = {}; 66 | device.send(`/eos/get/cue/${addressParts[4]}/count`); 67 | } else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', 'count'])) { 68 | for (let i = 0; i < osc.args[0]; i++) { 69 | device.send(`/eos/get/cue/${addressParts[4]}/index/${i}`); 70 | } 71 | } else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', '*', '*', 'list', '*', '*'])) { 72 | if (device.data.EOS.cueLists[addressParts[4]] === undefined) { 73 | device.data.EOS.cueLists[addressParts[4]] = {}; 74 | device.send(`/eos/get/cue/${addressParts[4]}/count`); 75 | } 76 | if (device.data.EOS.cueLists[addressParts[4]][addressParts[5]] === undefined) { 77 | device.data.EOS.cueLists[addressParts[4]][addressParts[5]] = {}; 78 | } 79 | device.data.EOS.cueLists[addressParts[4]][addressParts[5]][addressParts[6]] = new Cue(osc.args); 80 | 81 | device.update('cueData', { 82 | cue: device.data.EOS.cueLists[addressParts[4]][addressParts[5]], 83 | cueNumber: addressParts[5], 84 | uid: osc.args[1], 85 | }); 86 | } else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', '*'])) { 87 | // There's no OSC notification of the deletion of a part. It just tells you to update the parent cue and remaining children. 88 | // So: we should fetch all the parts of a cue somehow every time there's an update to a cue. 89 | delete device.data.EOS.cueLists[addressParts[4]][addressParts[5]]; 90 | device.draw(); 91 | } else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', '*', '*', 'actions', 'list', '*', '*'])) { 92 | if (osc.args.length === 3) { 93 | device.data.EOS.cueLists[addressParts[4]][addressParts[5]][0].extLinks = osc.args[2]; 94 | } 95 | } else if (match(addressParts, ['eos', 'out', 'event', 'cue', '*', '*', 'fire'])) { 96 | device.data.EOS.activeCue = addressParts[5]; 97 | device.draw(); 98 | const cues = device.data.EOS.cueLists[addressParts[4]][addressParts[5]]; 99 | if (cues) { 100 | device.update('activeCue', { 101 | uid: cues[0].uid, 102 | }); 103 | } 104 | } else if (match(addressParts, ['eos', 'out', 'notify', 'cue', '*', '*', '*', '*'])) { 105 | const cueList = addressParts[4]; 106 | const cueNumber = osc.args[1]; 107 | device.send(`/eos/get/cue/${cueList}/${cueNumber}`); 108 | } else if (match(addressParts, ['eos', 'out', 'get', 'version'])) { 109 | device.data.EOS.version = osc.args[0]; 110 | } else { 111 | // console.log(osc); 112 | } 113 | }; 114 | 115 | exports.update = function update(device, doc, updateType, data) { 116 | if (updateType === 'cueData') { 117 | const $elem = doc.getElementById(data.uid); 118 | if ($elem) { 119 | $elem.outerHTML = device.templates.cue({ 120 | q: data.cue, 121 | cueNumber: data.cueNumber, 122 | isActive: false, 123 | }); 124 | } else { 125 | device.draw(); 126 | } 127 | } else if (updateType === 'activeCue') { 128 | const $elem = doc.getElementById(data.uid); 129 | $elem.scrollIntoView({ behavior: 'smooth', block: 'center' }); 130 | } 131 | }; 132 | 133 | exports.heartbeat = function heartbeat(device) { 134 | device.send('/eos/ping'); 135 | }; 136 | 137 | function match(testArray, patternArray) { 138 | let out = true; 139 | if (testArray.length !== patternArray.length) { 140 | return false; 141 | } 142 | patternArray.forEach((patternPart, i) => { 143 | if (testArray[i] !== patternPart && patternPart !== '*') { 144 | out = false; 145 | } 146 | }); 147 | return out; 148 | } 149 | class EOS { 150 | constructor() { 151 | this.version = ''; 152 | this.showName = ''; 153 | this.cueLists = {}; 154 | this.activeCue = undefined; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /plugins/eos/styles.css: -------------------------------------------------------------------------------- 1 | table { 2 | background-color: #141414; 3 | border: #404040 1px solid; 4 | border-radius: 4px; 5 | color: #a59baa; 6 | width: 100%; 7 | } 8 | th { 9 | font-size: 14px; 10 | font-weight: normal; 11 | padding: 5px; 12 | } 13 | td { 14 | text-align: center; 15 | padding: 4px 6px; 16 | } 17 | td:first-child { 18 | text-align: left; 19 | } 20 | td.black { 21 | background-color: black; 22 | border-right: #212021 1px solid; 23 | } 24 | td.num { 25 | color: white; 26 | font-weight: 600; 27 | font-size: 16px; 28 | } 29 | 30 | .scene { 31 | background-color: black; 32 | border-top: #141414 2px solid; 33 | border-bottom: #141414 2px solid; 34 | } 35 | .scene hr { 36 | border: #001f10 4px solid; 37 | } 38 | .scene span { 39 | display: table; 40 | margin: 0px auto; 41 | margin-top: -25px; 42 | padding: 4px; 43 | background-color: black; 44 | font-weight: bold; 45 | font-size: 14px; 46 | } 47 | .time { 48 | border: #2f2b35 2px solid; 49 | border-radius: 5px; 50 | } 51 | 52 | tr.active-cue { 53 | color: #c78b07; 54 | } 55 | tr.active-cue td.num div { 56 | margin: 0px; 57 | padding: 1px 6px; 58 | background-color: #c78b07; 59 | border-radius: 4px; 60 | color: black; 61 | } 62 | tr.active-cue .time { 63 | border-color: #c78b07; 64 | } 65 | 66 | .list_name { 67 | color: lightgray; 68 | margin-left: 5px; 69 | margin-bottom: 2px; 70 | } 71 | 72 | .list_container { 73 | margin-bottom: 10px; 74 | } 75 | 76 | @media screen and (min-width: 0px) and (max-width: 750px) { 77 | .hide-medium { 78 | display: none; 79 | } 80 | } 81 | @media screen and (min-width: 0px) and (max-width: 550px) { 82 | .hide-small { 83 | display: none; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /plugins/eos/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName %>

3 |

Eos <%= data.EOS.version %>

4 |
5 | 6 | <% for(var i in data.EOS.cueLists){ %> 7 | <% if(data.cueListFilter == i || data.cueListFilter.length==0){ %> 8 |
9 |
List <%= i %>
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | <% var cues = Object.keys(data.EOS.cueLists[i]).sort(function(a, b){return Number(a)-Number(b)}) %> 28 | 29 | <% for(var j=0; j 30 | <% var q = data.EOS.cueLists[i][cues[j]] %> 31 | 32 | <%= templates.cue({ 33 | q: q, 34 | cues: cues, 35 | cueNumber: cues[j], 36 | isActive: (cues[j]==data.EOS.activeCue+"") 37 | }) %> 38 | 39 | <% } %> 40 |
CueInt UpInt DownFocusColorBeamDurMBAFw/HgLabelExt Links
41 |
42 | <% } %> 43 | 44 | <% } %> 45 | -------------------------------------------------------------------------------- /plugins/lightfactory/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/lightfactory/icon.png -------------------------------------------------------------------------------- /plugins/lightfactory/info.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/lightfactory/info.html -------------------------------------------------------------------------------- /plugins/lightfactory/main.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | defaultName: 'LightFactory', 3 | connectionType: 'TCPsocket', 4 | remotePort: 3100, 5 | mayChangePorts: true, 6 | heartbeatInterval: 5000, 7 | heartbeatTimeout: 10000, 8 | searchOptions: { 9 | type: 'TCPport', 10 | searchBuffer: Buffer.from('\n', 'ascii'), 11 | testPort: 3100, 12 | validateResponse(msg, info) { 13 | return msg.toString().includes('LightFactory'); 14 | }, 15 | }, 16 | }; 17 | 18 | exports.ready = function ready(_device) { 19 | const device = _device; 20 | device.data.cueLists = {}; 21 | device.send('get cue list\n'); 22 | }; 23 | 24 | function cleanMessage(message) { 25 | return message.split('\n').filter((line) => line.trim() !== ''); 26 | } 27 | 28 | // TODO(jwetzell): try to grab notes? 29 | exports.data = function data(_device, _message) { 30 | const message = _message.toString(); 31 | const device = _device; 32 | 33 | if (message.includes('Current Cue List')) { 34 | // NOTE(jwetzell): loading cue list info 35 | const cueListMessage = cleanMessage(message); 36 | if (cueListMessage.includes('') && cueListMessage.includes('')) { 37 | const cueListstartIndex = cueListMessage.indexOf(''); 38 | const cueListFinishedIndex = cueListMessage.indexOf(''); 39 | 40 | for (let i = cueListstartIndex + 1; i < cueListFinishedIndex; i++) { 41 | const cueListName = cueListMessage[i]; 42 | if (device.data.cueLists[cueListName] === undefined) { 43 | device.data.cueLists[cueListName] = { 44 | name: cueListName, 45 | cues: {}, 46 | active: false, 47 | }; 48 | } else { 49 | // NOTE(jwetzell): reset active as it will be computed in a few lines 50 | device.data.cueLists[cueListName].active = false; 51 | } 52 | } 53 | const activeCueListLine = cueListMessage[cueListFinishedIndex + 1]; 54 | if (activeCueListLine) { 55 | const activeCueListLineParts = activeCueListLine.split('Current Cue List'); 56 | const activeCueList = activeCueListLineParts[activeCueListLineParts.length - 1].trim(); 57 | if (activeCueList && device.data.cueLists[activeCueList] !== undefined) { 58 | device.data.cueLists[activeCueList].active = true; 59 | } 60 | } 61 | device.data.cueListToLoad = Object.keys(device.data.cueLists)[0]; 62 | device.send(`get cues ${device.data.cueListToLoad}\n`); 63 | } 64 | } else if (message.includes('Current Live Cue')) { 65 | // NOTE(jwetzell): loading cues for a cue list 66 | const cueList = device.data.cueLists[device.data.cueListToLoad]; 67 | const cueListCuesMessage = cleanMessage(message); 68 | 69 | // NOTE(jwetzell): this is so gross 70 | if (cueListCuesMessage.includes('') && cueListCuesMessage.includes('')) { 71 | const cuesStartIndex = cueListCuesMessage.indexOf(''); 72 | const cuesFinishedIndex = cueListCuesMessage.indexOf(''); 73 | 74 | const validCueNumbers = []; 75 | for (let i = cuesStartIndex + 1; i < cuesFinishedIndex; i++) { 76 | const cueData = cueListCuesMessage[i]; 77 | const cueSplit = cueData.indexOf(' '); 78 | const cueNumber = cueSplit > 0 ? cueData.substring(0, cueSplit).trim() : cueData.trim(); 79 | const cueName = cueSplit > 0 ? cueData.substring(cueSplit + 1).trim() : ''; 80 | cueList.cues[cueNumber] = { 81 | name: cueName, 82 | number: cueNumber, 83 | active: false, 84 | }; 85 | validCueNumbers.push(cueNumber); 86 | } 87 | 88 | const invalidCueNumbers = Object.keys(cueList.cues).filter((cueNumber) => !validCueNumbers.includes(cueNumber)); 89 | 90 | invalidCueNumbers.forEach((cueNumber) => { 91 | delete cueList.cues[cueNumber]; 92 | }); 93 | 94 | const activeCueLine = cueListCuesMessage[cuesFinishedIndex + 1]; 95 | if (activeCueLine) { 96 | const activeCueLineParts = activeCueLine.split('Current Live Cue'); 97 | const activeCue = activeCueLineParts[activeCueLineParts.length - 1].trim(); 98 | if (activeCue && cueList.cues[activeCue] !== undefined) { 99 | cueList.cues[activeCue].active = true; 100 | } 101 | } 102 | } 103 | // NOTE(jwetzell): load next cue list's cues if there is one 104 | const cueListNames = Object.keys(device.data.cueLists); 105 | const cueListIndex = cueListNames.indexOf(device.data.cueListToLoad); 106 | device.data.cueListToLoad = cueListNames.at(cueListIndex + 1); 107 | if (device.data.cueListToLoad !== undefined) { 108 | device.send(`get cues ${device.data.cueListToLoad}\n`); 109 | } 110 | } else if (message.includes('Running cue') || message.includes('Goto cue')) { 111 | const cueUpdateMessage = message 112 | .trim() 113 | .slice(0, -1) 114 | .split(':') 115 | .map((item) => item.trim()); 116 | if (cueUpdateMessage.length === 3) { 117 | const cueListName = cueUpdateMessage[1]; 118 | const updatedCue = cueUpdateMessage[2]; 119 | 120 | const split = updatedCue.indexOf(' '); 121 | 122 | const updatedCueNumber = split > 0 ? updatedCue.substring(0, split).trim() : updatedCue.trim(); 123 | if (device.data.cueLists[cueListName] !== undefined) { 124 | Object.entries(device.data.cueLists[cueListName].cues).forEach(([cueName, cueData]) => { 125 | cueData.active = cueData.number === updatedCueNumber; 126 | }); 127 | } 128 | } 129 | } 130 | device.draw(); 131 | }; 132 | 133 | // TODO(jwetzell): keep alive? refresh cue list info? 134 | exports.heartbeat = function heartbeat(device) { 135 | device.send('get cue list\n'); 136 | }; 137 | -------------------------------------------------------------------------------- /plugins/lightfactory/styles.css: -------------------------------------------------------------------------------- 1 | section { 2 | background-color: #363636; 3 | border: gray 1px solid; 4 | padding: 2px; 5 | padding-top: 0px; 6 | overflow: hidden; 7 | font-size: 8px; 8 | margin-bottom: 10px; 9 | width: 300px; 10 | } 11 | table { 12 | border: #414142 1px solid; 13 | background-color: #272727; 14 | width: 100%; 15 | } 16 | table, 17 | th, 18 | td { 19 | border-collapse: collapse; 20 | color: #ccc; 21 | font-size: 12px; 22 | } 23 | th { 24 | padding: 2px 10px; 25 | border: #282828 1px solid; 26 | background: linear-gradient(#404040, #363636); 27 | text-align: left; 28 | } 29 | td { 30 | text-align: left; 31 | padding: 7px 7px; 32 | font-family: sans-serif; 33 | position: relative; 34 | } 35 | tr { 36 | border-bottom: #363636 1px solid; 37 | } 38 | tr:nth-child(odd) { 39 | background-color: #2a2a2a; 40 | } 41 | tr:nth-child(even) { 42 | background-color: #202020; 43 | } 44 | 45 | .active_indicator { 46 | color: #00ff00; 47 | font-size: 13px; 48 | position: absolute; 49 | top: 4px; 50 | left: -4px; 51 | transform: rotate(-45deg); 52 | } 53 | -------------------------------------------------------------------------------- /plugins/lightfactory/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName %>

3 |
4 | 5 |
6 | 7 | <% Object.entries(data.cueLists).forEach(([cueListName,cueListData]) => { %> 8 | 9 |
10 |

Cue List: <%= cueListData.name %> <%= cueListData.active ? '(Active)': '' %>

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <% Object.entries(cueListData.cues).forEach(([cueName,cueData]) => { %> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <% }); %> 29 |
Cue NoDescription
<%= cueData.active ? '◢': '' %><%= cueData.number %><%= cueData.name %>
30 |
31 | 32 | <% 33 | }); 34 | %> 35 |
-------------------------------------------------------------------------------- /plugins/livestream-studio/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/livestream-studio/icon.png -------------------------------------------------------------------------------- /plugins/livestream-studio/img/tbar_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/livestream-studio/img/tbar_bg.png -------------------------------------------------------------------------------- /plugins/livestream-studio/img/tbar_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/livestream-studio/img/tbar_handle.png -------------------------------------------------------------------------------- /plugins/livestream-studio/info.html: -------------------------------------------------------------------------------- 1 |

Livestream Studio Configuration

2 |
    3 |
  • enable Third-party Controllers under Settings->Hardware Control->Allow Incoming Connections
  • 4 |
  • allow the connection from Cue View the first time it connects under the same settings menu
  • 5 |
6 | -------------------------------------------------------------------------------- /plugins/livestream-studio/main.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | defaultName: 'Livestream Studio', 3 | connectionType: 'TCPsocket', 4 | remotePort: 9923, 5 | mayChangePort: false, 6 | searchOptions: { 7 | type: 'TCPport', 8 | searchBuffer: Buffer.from(''), 9 | testPort: 9923, 10 | validateResponse(msg, info) { 11 | return msg.toString().startsWith('ILCC:'); 12 | }, 13 | }, 14 | }; 15 | 16 | exports.ready = function ready(_device) { 17 | console.log('livestream studio ready'); 18 | const device = _device; 19 | device.draw(); 20 | 21 | setInterval(() => { 22 | device.update('inputs', device.data); 23 | device.update('fadeToBlack', device.data); 24 | }, 1000); 25 | }; 26 | 27 | exports.update = function update(device, _document, updateType, data) { 28 | const document = _document; 29 | if (updateType === 'programInput' || updateType === 'previewInput' || updateType === 'inputs') { 30 | device.data.inputs.forEach((input) => { 31 | if (device.data.program === input.number) { 32 | document.getElementById(`input-${input.number}`).classList.add('lss-red'); 33 | } else { 34 | document.getElementById(`input-${input.number}`).classList.remove('lss-red'); 35 | } 36 | 37 | if (device.data.preview === input.number) { 38 | document.getElementById(`input-${input.number}`).classList.add('lss-green'); 39 | } else { 40 | document.getElementById(`input-${input.number}`).classList.remove('lss-green'); 41 | } 42 | }); 43 | } else if (updateType === 'fadeToBlack') { 44 | const ftbId = 'ftb'; 45 | 46 | if (device.data.fadeToBlack) { 47 | document.getElementById(ftbId).classList.add('lss-ftb-glow'); 48 | } else { 49 | document.getElementById(ftbId).classList.remove('lss-ftb-glow'); 50 | } 51 | } else if (updateType === 'tbar') { 52 | const tbarId = `tbar-div`; 53 | const tbarHandleId = `tbar-handle-div`; 54 | if (document.getElementById(tbarId)) { 55 | document.getElementById(tbarId).style.height = `${device.data.tBar.percent * 1.4 + 10}px`; 56 | } 57 | if (document.getElementById(tbarHandleId)) { 58 | document.getElementById(tbarHandleId).style.bottom = `${device.data.tBar.percent * 1.4 + 10}px`; 59 | } 60 | 61 | if (device.data.tBar.status === 'Automatic') { 62 | document.getElementById(`auto`).classList.add('lss-red'); 63 | } else { 64 | document.getElementById(`auto`).classList.remove('lss-red'); 65 | } 66 | } else if (updateType === 'cut') { 67 | document.getElementById('cut').classList.add('lss-red'); 68 | setTimeout(() => { 69 | document.getElementById('cut').classList.remove('lss-red'); 70 | }, 250); 71 | } 72 | }; 73 | 74 | exports.data = function data(_device, msg) { 75 | const device = _device; 76 | 77 | this.deviceInfoUpdate(device, 'status', 'ok'); 78 | 79 | const packets = msg.toString().split('\n'); 80 | 81 | packets.forEach((packet) => { 82 | const packetParts = packet.split(':'); 83 | const packetType = packetParts[0]; 84 | if (packetType === 'ILC') { 85 | if (device.data.inputs === undefined) { 86 | device.data.inputs = []; 87 | } 88 | device.data.inputs[packetParts[1]] = { 89 | number: Number.parseInt(packetParts[1], 10) + 1, 90 | name: packetParts[2].replaceAll('"', ''), 91 | audio: { 92 | level: parseFloat(packetParts[3]) / 1000, 93 | gain: parseFloat(packetParts[4]) / 1000, 94 | mute: packetParts[5] === '1', 95 | solo: packetParts[6] === '1', 96 | programLock: packetParts[7] === '1', 97 | }, 98 | type: packetParts[8], 99 | }; 100 | device.draw(); 101 | device.update('inputs', device.data); 102 | } else if (packetType === 'ILCC') { 103 | if (device.data.inputCount !== undefined) { 104 | device.data.inputs = []; 105 | } 106 | device.data.inputCount = Number.parseInt(packetParts[1], 10); 107 | } else if (packetType === 'PmIS') { 108 | device.data.program = Number.parseInt(packetParts[1], 10) + 1; 109 | device.update('programInput', device.data); 110 | } else if (packetType === 'PwIS') { 111 | device.data.preview = Number.parseInt(packetParts[1], 10) + 1; 112 | device.update('previewInput', device.data); 113 | } else if (packetType === 'Cut') { 114 | [device.data.preview, device.data.program] = [device.data.program, device.data.preview]; 115 | device.data.tBar.percent = 0; 116 | device.data.tBar.status = 'Stop'; 117 | device.update('cut'); 118 | device.update('inputs', device.data); 119 | device.update('tbar', device.data); 120 | } else if (packetType === 'FOut') { 121 | device.data.fadeToBlack = true; 122 | device.update('fadeToBlack', device.data); 123 | } else if (packetType === 'FIn') { 124 | device.data.fadeToBlack = false; 125 | device.update('fadeToBlack', device.data); 126 | } else if (packetType === 'TrASp' || packetType === 'TrMSp') { 127 | if (device.data.tBar === undefined) { 128 | device.data.tBar = {}; 129 | } 130 | device.data.tBar.status = packetType === 'TrASp' ? 'Automatic' : 'Manual'; 131 | device.data.tBar.percent = Number.parseInt(packetParts[1], 10) / 10; 132 | if (device.data.tBar.percent === 0) { 133 | device.data.tBar.status = 'Stop'; 134 | } 135 | device.update('tbar', device.data); 136 | } else if (packetType === 'TrAStart' || packetType === 'TrAStop') { 137 | if (device.data.tBar === undefined) { 138 | device.data.tBar = {}; 139 | } 140 | device.data.tBar.status = packetType.slice(3); 141 | if (packetType === 'TrAStop') { 142 | device.data.tBar.percent = 0; 143 | } 144 | device.update('tbar', device.data); 145 | } 146 | }); 147 | }; 148 | -------------------------------------------------------------------------------- /plugins/livestream-studio/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #191919; 3 | } 4 | h1 { 5 | font-family: 'Open Sans', sans-serif; 6 | } 7 | h5 { 8 | color: white; 9 | margin: 0px 0px 10px 3px; 10 | font-weight: bold; 11 | width: 100%; 12 | /* font-family: 'Open Sans', sans-serif; */ 13 | } 14 | 15 | .lss-input { 16 | width: 120px; 17 | height: 40px; 18 | background-color: #242424; 19 | color: white; 20 | border: #242424 6px solid; 21 | outline: black 1px solid; 22 | outline-offset: -1px; 23 | } 24 | 25 | .lss-button { 26 | width: 80px; 27 | height: 30px; 28 | background: linear-gradient(#363636, #282828); 29 | border-radius: 5px; 30 | border: black 2px solid; 31 | color: gray; 32 | margin-bottom: 5px; 33 | } 34 | 35 | .source-wrapper { 36 | display: flex; 37 | flex-wrap: wrap; 38 | background-color: #000000; 39 | width: 122px; 40 | border: black 1px solid; 41 | } 42 | 43 | .lss-red { 44 | border-color: #6f1607; 45 | outline-color: #ff0000; 46 | } 47 | .lss-button.lss-red { 48 | color: #6f1607; 49 | } 50 | 51 | .lss-green { 52 | border-color: #395a13; 53 | outline-color: #558522; 54 | } 55 | 56 | .lss-disabled { 57 | color: #3a3a3a; 58 | } 59 | 60 | .lss-ftb-glow { 61 | border: gray 2px solid; 62 | animation: ftb-glow 0.75s infinite alternate; 63 | } 64 | 65 | @keyframes ftb-glow { 66 | 0% { 67 | border-color: #666666; 68 | color: #666666; 69 | } 70 | 100% { 71 | border-color: #fc584b; 72 | color: #fc584b; 73 | } 74 | } 75 | 76 | .source-label { 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | height: 100%; 81 | font-size: 11px; 82 | font-weight: bold; 83 | } 84 | 85 | .inputs-container { 86 | width: 200px; 87 | } 88 | 89 | .transition-container { 90 | display: flex; 91 | flex-wrap: wrap; 92 | justify-content: space-between; 93 | } 94 | 95 | .transition-settings { 96 | display: flex; 97 | flex-direction: column; 98 | justify-content: space-between; 99 | margin-right: 30px; 100 | } 101 | 102 | .tbar-container { 103 | display: flex; 104 | flex-direction: column-reverse; 105 | background-color: red; 106 | border: 2px black solid; 107 | width: 80px; 108 | height: 176px; 109 | position: relative; 110 | margin-bottom: 10px; 111 | } 112 | 113 | .tbar-bg { 114 | position: absolute; 115 | background-image: url('img/tbar_bg.png'); 116 | width: 80px; 117 | height: 176px; 118 | } 119 | .tbar-handle { 120 | position: absolute; 121 | background-image: url('img/tbar_handle.png'); 122 | width: 42px; 123 | height: 20px; 124 | left: 19px; 125 | bottom: 10px; 126 | } 127 | .tbar-div { 128 | width: 100%; 129 | background-color: #7aff58; 130 | overflow: hidden; 131 | } 132 | 133 | .fade-to-black-container { 134 | display: flex; 135 | flex-direction: column; 136 | align-items: flex-end; 137 | } 138 | 139 | .fade-to-black .source-wrapper { 140 | display: flex; 141 | width: 100%; 142 | padding: 5px; 143 | justify-content: center; 144 | } 145 | 146 | .fade-to-black h3 { 147 | text-align: center; 148 | } 149 | 150 | .clear { 151 | background: transparent; 152 | border-color: transparent; 153 | background-color: transparent; 154 | } 155 | 156 | .hide { 157 | display: none; 158 | } 159 | 160 | .no-wrap { 161 | flex-wrap: nowrap; 162 | } 163 | 164 | .show-small { 165 | display: none; 166 | } 167 | 168 | @media screen and (min-width: 0px) and (max-width: 480px) { 169 | .hide-small { 170 | display: none; 171 | } 172 | .show-small { 173 | display: block; 174 | } 175 | } 176 | 177 | .float-left { 178 | float: left; 179 | } 180 | -------------------------------------------------------------------------------- /plugins/livestream-studio/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName %>

3 |
4 | 5 | <% if (data.inputCount) { %> 6 |
7 |
INPUTS
8 |
9 | <% data.inputs.forEach((input)=>{ %> 10 |
11 |
<%= input.name %>
12 |
13 | <% }) %> 14 |
15 |
16 | 17 |
18 |
19 | 45 | 46 |
47 |
TRANSITION
48 |
49 |
CUT
50 |
51 |
52 |
AUTO
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
FTB
61 |
62 |
63 |
64 | 65 |
66 | <% } %> 67 | -------------------------------------------------------------------------------- /plugins/pjlink/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/pjlink/icon.png -------------------------------------------------------------------------------- /plugins/pjlink/info.html: -------------------------------------------------------------------------------- 1 |

Connection Requirements

2 |
    3 |
  • Requires no password on the projector
  • 4 |
5 | -------------------------------------------------------------------------------- /plugins/pjlink/main.js: -------------------------------------------------------------------------------- 1 | const md5 = require('md5'); 2 | 3 | exports.config = { 4 | defaultName: 'PJLink Projector', 5 | connectionType: 'TCPsocket', 6 | remotePort: 4352, 7 | mayChangePorts: false, 8 | heartbeatInterval: 5000, 9 | heartbeatTimeout: 15000, 10 | searchOptions: { 11 | type: 'UDPsocket', 12 | searchBuffer: Buffer.from([0x25, 0x32, 0x53, 0x52, 0x43, 0x48, 0x0d]), 13 | devicePort: 4352, 14 | listenPort: 4352, 15 | validateResponse(msg, info) { 16 | return msg.toString().includes('%2ACKN='); 17 | }, 18 | }, 19 | fields: [ 20 | { 21 | key: 'password', 22 | label: 'Pass', 23 | type: 'textinput', 24 | value: '', 25 | action(device) { 26 | device.plugin.heartbeat(device); 27 | }, 28 | }, 29 | ], 30 | }; 31 | 32 | exports.ready = function ready(device) { 33 | // Power status query 34 | // device.send("%1POWR ?\r"); 35 | }; 36 | 37 | const PJLinkCmds = [ 38 | '%1POWR=', 39 | '%1INPT=', 40 | '%1AVMT=', 41 | '%1ERST=', 42 | '%1LAMP=', 43 | '%1NAME=', 44 | '%1INF1=', 45 | '%1INF2=', 46 | '%2SNUM=', 47 | '%2SVER=', 48 | ]; 49 | 50 | let passwordMD5 = false; 51 | let passwordSeed = false; 52 | 53 | function processPJLink(_device, str, that) { 54 | const arr = str.split('%'); 55 | arr.shift(); 56 | const device = _device; 57 | 58 | arr.forEach((response) => { 59 | const parts = response.split('='); 60 | const cmd = parts[0]; 61 | const value = parts[1]; 62 | 63 | switch (cmd) { 64 | case '1POWR': 65 | device.data.power = value; 66 | break; 67 | 68 | case '1INPT': 69 | device.data.input = value; 70 | break; 71 | 72 | case '1AVMT': 73 | device.data.avmute = value; 74 | break; 75 | 76 | case '1ERST': 77 | device.data.fanError = value[0]; 78 | device.data.lampError = value[1]; 79 | device.data.tempError = value[2]; 80 | device.data.coverError = value[3]; 81 | device.data.filterError = value[4]; 82 | device.data.otherError = value[5]; 83 | break; 84 | 85 | case '1LAMP': 86 | device.data.lamp = value.split(' '); 87 | break; 88 | 89 | case '1NAME': 90 | device.data.name = value; 91 | that.deviceInfoUpdate(device, 'defaultName', device.data.name); 92 | break; 93 | 94 | case '1INF1': 95 | device.data.info1 = value; 96 | break; 97 | 98 | case '1INF2': 99 | device.data.info2 = value; 100 | break; 101 | 102 | case '2SNUM': 103 | device.data.serial = value; 104 | break; 105 | 106 | case '2SVER': 107 | device.data.version = value; 108 | break; 109 | 110 | default: 111 | break; 112 | } 113 | }); 114 | device.draw(); 115 | } 116 | 117 | exports.data = function data(_device, message) { 118 | const device = _device; 119 | this.deviceInfoUpdate(device, 'status', 'ok'); 120 | const msg = message.toString(); 121 | 122 | if (msg.substring(0, 8) === 'PJLINK 1') { 123 | passwordSeed = msg.substring(9, 17); 124 | passwordMD5 = md5(`${passwordSeed}${device.fields.password}`); 125 | device.data.authentication = 'ON'; 126 | 127 | device.send( 128 | `${passwordMD5}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r` 129 | ); 130 | device.draw(); 131 | } else if (msg.substring(0, 8) === 'PJLINK 0') { 132 | device.data.authentication = 'OFF'; 133 | device.draw(); 134 | } else if (msg.startsWith('PJLINK ERRA')) { 135 | device.data.passwordOK = false; 136 | device.draw(); 137 | } 138 | 139 | if (PJLinkCmds.includes(msg.substring(0, 7))) { 140 | processPJLink(device, msg, this); 141 | device.data.passwordOK = true; 142 | } 143 | }; 144 | 145 | exports.heartbeat = function heartbeat(device) { 146 | passwordMD5 = md5(`${passwordSeed}${device.fields.password}`); 147 | if (device.fields.password.length > 0 && device.data.passwordOK) { 148 | device.send( 149 | `${passwordMD5}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r` 150 | ); 151 | } else if (device.fields.password.length > 0) { 152 | device.send(`${passwordMD5}%1POWR ?\r`); 153 | } else if (device.data.passwordOK) { 154 | device.send(`%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r`); 155 | } else { 156 | device.send(`%1POWR ?\r`); 157 | } 158 | 159 | device.draw(); 160 | }; 161 | -------------------------------------------------------------------------------- /plugins/pjlink/styles.css: -------------------------------------------------------------------------------- 1 | .warning { 2 | color: #e9873a; 3 | } 4 | .error { 5 | color: #ed5f5d; 6 | } 7 | .ok { 8 | color: #79b757; 9 | } 10 | table { 11 | margin-bottom: 30px; 12 | width: 350px; 13 | } 14 | -------------------------------------------------------------------------------- /plugins/pjlink/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName %>

3 |
4 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 45 | 46 | 47 | 48 | 58 | 59 | 60 | <% if(data.lamp){ %> 61 | <% for(var i=0; i 62 | 63 | 64 | 71 | 72 | <% } %> 73 | <% } %> 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
Power
9 | <% if(data.power==0){ %>OFF<% } %> 10 | <% if(data.power==1){ %>ON<% } %> 11 | <% if(data.power==2){ %>COOLING DOWN<% } %> 12 | <% if(data.power==3){ %>WARMING UP<% } %> 13 |
AV Mute
18 | <% if(data.avmute==11){ %>Video Mute<% } %> 19 | <% if(data.avmute==21){ %>Audio Mute<% } %> 20 | <% if(data.avmute==31){ %>Video & Audio Mute<% } %> 21 | <% if(data.avmute==30){ %>...<% } %> 22 |
Errors
27 | <% if(data.fanError==1){ %>Fan Warning
<% } %> 28 | <% if(data.fanError==2){ %>Fan Error
<% } %> 29 | 30 | <% if(data.lampError==1){ %>Lamp Warning
<% } %> 31 | <% if(data.lampError==2){ %>Lamp Error
<% } %> 32 | 33 | <% if(data.tempError==1){ %>Temp Warning
<% } %> 34 | <% if(data.tempError==2){ %>Temp Error
<% } %> 35 | 36 | <% if(data.coverError==1){ %>Cover Warning
<% } %> 37 | <% if(data.coverError==2){ %>Cover Error
<% } %> 38 | 39 | <% if(data.filterError==1){ %>Filter Warning
<% } %> 40 | <% if(data.filterError==2){ %>Filter Error
<% } %> 41 | 42 | <% if(data.otherError==1){ %>Other Warning
<% } %> 43 | <% if(data.otherError==2){ %>Other Error
<% } %> 44 |  
Input
49 | <% if(data.input){ %> 50 | <% if(data.input[0]==1){ %>RGB<% } %> 51 | <% if(data.input[0]==2){ %>Video<% } %> 52 | <% if(data.input[0]==3){ %>Digital<% } %> 53 | <% if(data.input[0]==4){ %>Storage<% } %> 54 | <% if(data.input[0]==5){ %>Network<% } %> 55 | <%= data.input[1] %> 56 | <% } %> 57 |
Lamp <%= i/2 %> 65 | <% if(Number(data.lamp[i+1])){ %> 66 |
ON
67 | <% }else{ %> 68 |
OFF
69 | <% } %> 70 |
Name
<%= data.name %>
Info 1
<%= data.info1 %>
Info 2
<%= data.info2 %>
Serial Number
<%= data.serial %>
Version
<%= data.version %>
96 | 97 | 98 | 99 | 109 | 110 |
Authentication 100 | <% if(!data.passwordOK){ %> 101 |
INCORRECT PASSWORD
102 | <% }else if(data.authentication=="ON"){ %> 103 |
ON
104 | <% }else{ %> 105 |
<%= data.authentication %>
106 | <% } %> 107 | 108 |
-------------------------------------------------------------------------------- /plugins/posistagenet/icon.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/posistagenet/icon.afphoto -------------------------------------------------------------------------------- /plugins/posistagenet/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/posistagenet/icon.png -------------------------------------------------------------------------------- /plugins/posistagenet/info.html: -------------------------------------------------------------------------------- 1 |

PosiStageNet requires no configuration.

2 | -------------------------------------------------------------------------------- /plugins/posistagenet/main.js: -------------------------------------------------------------------------------- 1 | const { Decoder } = require('@jwetzell/posistagenet'); 2 | 3 | exports.config = { 4 | defaultName: 'PosiStageNet', 5 | connectionType: 'multicast', 6 | remotePort: 56565, 7 | mayChangePorts: false, 8 | heartbeatInterval: 5000, 9 | heartbeatTimeout: 15000, 10 | searchOptions: { 11 | type: 'multicast', 12 | address: `236.10.10.10`, 13 | port: 56565, 14 | validateResponse(msg, info) { 15 | // TODO(jwetzell): find a way to check that this is a posistagenet message 16 | return true; 17 | }, 18 | }, 19 | }; 20 | 21 | exports.ready = function ready(_device) { 22 | const device = _device; 23 | device.data = {}; 24 | const networkInterfaces = device.getNetworkInterfaces(); 25 | 26 | Object.keys(networkInterfaces).forEach((networkInterfaceID) => { 27 | const networkInterface = networkInterfaces[networkInterfaceID]; 28 | device.connection.addMembership(`236.10.10.10`, networkInterface[0].address); 29 | }); 30 | }; 31 | 32 | exports.data = function data(_device, buf, info) { 33 | const device = _device; 34 | 35 | if (!device.decoders) { 36 | device.decoders = {}; 37 | } 38 | 39 | if (!device.decoders[info.address]) { 40 | device.decoders[info.address] = new Decoder(); 41 | } 42 | 43 | device.decoders[info.address].decode(buf); 44 | 45 | Object.keys(device.decoders).forEach((address) => { 46 | device.data[address] = { 47 | trackers: device.decoders[address].trackers, 48 | system_name: device.decoders[address].system_name, 49 | fields: device.decoders[address].getTrackerFields(), 50 | }; 51 | }); 52 | device.draw(); 53 | }; 54 | 55 | exports.heartbeat = function heartbeat(device) {}; 56 | 57 | exports.update = function update(_device, doc, updateType, updateData) {}; 58 | -------------------------------------------------------------------------------- /plugins/posistagenet/styles.css: -------------------------------------------------------------------------------- 1 | section { 2 | background-color: #2d2d2d; 3 | border: gray 1px solid; 4 | padding: 10px; 5 | padding-top: 0px; 6 | border-radius: 10px; 7 | overflow: hidden; 8 | } 9 | table { 10 | border: #414142 1px solid; 11 | background-color: #272727; 12 | } 13 | table, 14 | th, 15 | td { 16 | border-collapse: collapse; 17 | color: #ccc; 18 | 19 | font-size: 13px; 20 | } 21 | th { 22 | padding: 2px 10px; 23 | } 24 | td { 25 | text-align: center; 26 | padding: 2px 7px; 27 | font-family: 'Courier New', Courier, monospace; 28 | } 29 | .bg-pos { 30 | background-color: #443727; 31 | } 32 | .bg-speed { 33 | background-color: #32422e; 34 | } 35 | .bg-ori { 36 | background-color: #2e4242; 37 | } 38 | .bg-accel { 39 | background-color: #392b3f; 40 | } 41 | .bg-trgtpos { 42 | background-color: #3a2732; 43 | } 44 | .border-right { 45 | border-right: 1px solid #272727; 46 | } 47 | -------------------------------------------------------------------------------- /plugins/posistagenet/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName%>

3 |
4 | 5 |
6 | 7 | <% Object.keys(data).forEach((address) => { 8 | const instanceData = data[address] 9 | %> 10 |
11 |

Server: <%= instanceData.system_name %> (<%= address %>)

12 |

Tracker Count: <%= Object.keys(instanceData.trackers).length %>

13 | 14 | 15 | 16 | 17 | <% if(instanceData.fields.has('tracker_name')) { %> 18 | 19 | <% } %> 20 | 21 | <% if(instanceData.fields.has('pos')) { %> 22 | 23 | <% } %> 24 | 25 | <% if(instanceData.fields.has('speed')) { %> 26 | 27 | <% } %> 28 | 29 | <% if(instanceData.fields.has('ori')) { %> 30 | 31 | <% } %> 32 | 33 | <% if(instanceData.fields.has('status')) { %> 34 | 35 | <% } %> 36 | 37 | <% if(instanceData.fields.has('accel')) { %> 38 | 39 | <% } %> 40 | 41 | <% if(instanceData.fields.has('trgtpos')) { %> 42 | 43 | <% } %> 44 | 45 | <% if(instanceData.fields.has('timestamp')) { %> 46 | 47 | <% } %> 48 | 49 | 50 | 51 | <% if(instanceData.fields.has('tracker_name')) { %> 52 | 53 | <% } %> 54 | 55 | <% if(instanceData.fields.has('pos')) { %> 56 | 57 | 58 | 59 | <% } %> 60 | 61 | <% if(instanceData.fields.has('speed')) { %> 62 | 63 | 64 | 65 | <% } %> 66 | 67 | <% if(instanceData.fields.has('ori')) { %> 68 | 69 | 70 | 71 | <% } %> 72 | 73 | <% if(instanceData.fields.has('status')) { %> 74 | 75 | <% } %> 76 | 77 | <% if(instanceData.fields.has('accel')) { %> 78 | 79 | 80 | 81 | <% } %> 82 | 83 | <% if(instanceData.fields.has('trgtpos')) { %> 84 | 85 | 86 | 87 | <% } %> 88 | 89 | <% if(instanceData.fields.has('timestamp')) { %> 90 | 91 | <% } %> 92 | 93 | 94 | 95 | <% Object.entries(instanceData.trackers).forEach(([trackerId,trackerData]) => { %> 96 | 97 | 98 | 99 | <% if(instanceData.fields.has('tracker_name')) { %> 100 | 101 | <% } %> 102 | 103 | <% if(instanceData.fields.has('pos')) { %> 104 | 105 | 106 | 107 | 108 | <% } %> 109 | 110 | <% if(instanceData.fields.has('speed')) { %> 111 | 112 | 113 | 114 | 115 | <% } %> 116 | 117 | <% if(instanceData.fields.has('ori')) { %> 118 | 119 | 120 | 121 | 122 | <% } %> 123 | 124 | <% if(instanceData.fields.has('status')) { %> 125 | 126 | <% } %> 127 | 128 | <% if(instanceData.fields.has('accel')) { %> 129 | 130 | 131 | 132 | 133 | <% } %> 134 | 135 | <% if(instanceData.fields.has('trgtpos')) { %> 136 | 137 | 138 | 139 | 140 | <% } %> 141 | 142 | <% if(instanceData.fields.has('timestamp')) { %> 143 | 144 | <% } %> 145 | 146 | 147 | 148 | <% }); %> 149 |
POSSPEEDORIACCELTRGTPOS
IDNamexyzxyzxyzvalidityxyzxyztracker_timestamp
<%= trackerId %><%= trackerData.tracker_name?.tracker_name%><%= trackerData.pos?.pos_x?.toFixed(2) %><%= trackerData.pos?.pos_y?.toFixed(2) %><%= trackerData.pos?.pos_z?.toFixed(2) %><%= trackerData.speed?.speed_x?.toFixed(2) %><%= trackerData.speed?.speed_y?.toFixed(2) %><%= trackerData.speed?.speed_z?.toFixed(2) %><%= trackerData.ori?.ori_x?.toFixed(2) %><%= trackerData.ori?.ori_y?.toFixed(2) %><%= trackerData.ori?.ori_z?.toFixed(2) %><%= trackerData.status?.validity?.toFixed(2) %><%= trackerData.accel?.accel_x?.toFixed(2) %><%= trackerData.accel?.accel_y?.toFixed(2) %><%= trackerData.accel?.accel_z?.toFixed(2) %><%= trackerData.trgtpos?.trgtpos_x?.toFixed(2) %><%= trackerData.trgtpos?.trgtpos_y?.toFixed(2) %><%= trackerData.trgtpos?.trgtpos_z?.toFixed(2) %><%= trackerData.timestamp?.tracker_timestamp %>
150 |
151 | <% 152 | }); 153 | %> 154 |
-------------------------------------------------------------------------------- /plugins/qlab/cart.ejs: -------------------------------------------------------------------------------- 1 | <% let cueKeys = allCues[cueList.uniqueID]; %> 2 | 3 |
4 | 5 |

<%= workspace.displayName %> — <%= cueList.listName %>

6 | <% 7 | let width = 100/cueKeys.cartColumns; 8 | let height = 600/(cueKeys.cartRows); 9 | %> 10 | 11 |
12 | 13 | <% for(let r=0; r 14 | <% for(let c=0; c 15 | <% let style = `style="left:${width*c}%; top:${height*r}px; width:${width }%; height:${height}px;"`; %> 16 | 17 |
>
18 | 19 | <% } %> 20 | <% } %> 21 | 22 | 23 | <% for(let i=0; i=cueKeys.cartColumns || row>=cueKeys.cartRows){ 38 | continue; 39 | } 40 | 41 | %> 42 | 43 | <%= tileTemplate({cue: cueList.cues[i], allCues: allCues, workspace: workspace}) %> 44 | 45 | <% } %> 46 | 47 |
48 | 49 |
-------------------------------------------------------------------------------- /plugins/qlab/cue.ejs: -------------------------------------------------------------------------------- 1 | <% let cueKeys = allCues[cue.uniqueID]; %> 2 | <% let playbackPosition = workspace.playbackPosition==cue.uniqueID ? "playback-position" : "" %> 3 | <% let selected = workspace.selected.includes(cue.uniqueID) ? "selected": "" %> 4 | 5 | <% if(cueKeys && cueKeys.parent){ %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | <% if(cueKeys.isBroken){ %> 16 | 17 | 18 | <% }else if(cueKeys.isRunning){ %> 19 | 20 | 21 | <% }else if(cueKeys.isPaused){ %> 22 | 23 | 24 | <% }else if(cueKeys.isLoaded){ %> 25 | 26 | 27 | <% }else if(cueKeys.isFlagged){ %> 28 | 29 | 30 | <% }else{ %> 31 | 32 | <% } %> 33 |
34 | 35 | 36 | 37 | <% if(cueKeys.type == "Group" && cueKeys.mode){ %> 38 | 39 | 40 | <% }else{ %> 41 | 42 | 43 | <% } %> 44 | 45 | 46 | 47 | 48 |
<%= cueKeys.number || " " %>
49 | 50 | 51 | <% let nextCue = cueKeys.cueInWorkspace.parent.cues[cueKeys.cueInWorkspace.sortIndex+1]; %> 52 | 53 | 54 | <% for(let i=0; i 55 | <% let parent = cueKeys.parentKeys[i+1]; %> 56 | <% %> 57 | 58 | <% if(cueKeys.type=="Group" && cueKeys.cues.length==0 && i==cueKeys.parentKeys.length-1){ %> 59 | 60 | 61 | <% }else if(cueKeys.type=="Group" && i==cueKeys.parentKeys.length-1){ %> 62 | 63 | 64 | <%}else if(parent){%> 65 | 66 | <% let parentCues = parent.cueInWorkspace.cues %> 67 | <% let lastCueInMe = parentCues[parentCues.length-1]%> 68 | 69 | <% while(lastCueInMe.type=="Group" && lastCueInMe.cues.length){ 70 | parentCues = lastCueInMe.cues; 71 | lastCueInMe = parentCues[parentCues.length-1]; 72 | } %> 73 | 74 | <% if(lastCueInMe.uniqueID == cueKeys.uniqueID){ %> 75 | 76 | 77 | <% }else{ %> 78 | 79 | 80 | 81 | <% } %> 82 | 83 | 84 | <% }else if(cueKeys.parentKeys[i]){ %> 85 | <% let nextCue2 = cueKeys.parentKeys[i].cues[cueKeys.cueInWorkspace.sortIndex+1]; %> 86 | 87 | <% if(!nextCue2){ %> 88 | 89 | 90 | <% }else{ %> 91 | 92 | 93 | <% } %> 94 | 95 | 96 | <% } %> 97 | 98 | <% } %> 99 | 100 | 101 | 102 | 103 | 104 | <% if(cue.type=="Group"){ %> 105 | 106 | <% if(cue.cues && cue.cues.length==0){ %> 107 | <%= cueKeys.listName %> 108 | 109 | <% }else{ %> 110 | <%= cueKeys.listName %> 111 | 112 | <% } %> 113 | 114 | <% }else if(!nextCue){ %> 115 | <%= cueKeys.listName %> 116 | 117 | <% }else{ %> 118 | <%= cueKeys.listName %> 119 | 120 | <% } %> 121 | 122 | 123 | 124 | 125 | 126 | <% if(cueKeys.currentCueTarget){ %> 127 | <%= allCues[cueKeys.currentCueTarget].number || allCues[cueKeys.currentCueTarget].listName %> 128 | 129 | <% }else{ %> 130 | 131 | 132 | <% } %> 133 | 134 | 135 | 136 | 137 | <% if(cueKeys.preWait){ %> 138 | <%= elapsedTime(cueKeys.preWait, cueKeys.preWaitElapsed, "preWait", cueKeys) %> 139 | 140 | <% }else{ %> 141 | 00:00.00 142 | 143 | <% } %> 144 | 145 | 146 | 147 | 148 | <% let cueTypesWithAction = ["Audio", "Mic", "Video", "Camera", "Text", "Light", "Fade", "Network", "MIDI File", "Timecode", "Wait"]; %> 149 | <% if((cueKeys.type=="Group" && cueKeys.mode==3) || (cueKeys.type=="Group" && cueKeys.mode==6) || cueTypesWithAction.includes(cueKeys.type)){ %> 150 | <%= elapsedTime(cueKeys.duration, cueKeys.actionElapsed, "action", cueKeys) %> 151 | 152 | <% }else{ %> 153 | 154 | 155 | <% } %> 156 | 157 | 158 | 159 | <% if(cueKeys.postWait){ %> 160 | <%= elapsedTime(cueKeys.postWait, cueKeys.postWaitElapsed, "postWait", cueKeys) %> 161 | 162 | <% }else{ %> 163 | 00:00.00 164 | 165 | <% } %> 166 | 167 | 168 | 169 | 170 | <% if(cueKeys.continueMode==2){ %> 171 |
172 | 173 | <% }else if(cueKeys.continueMode==1){ %> 174 |
175 | 176 | <% }else{ %> 177 | 178 | 179 | <% } %> 180 | 181 | 182 | 183 | 184 | 185 | <% }else{ %> 186 | 187 | 188 | 189 | 190 | 191 |
<%= cue.number || " " %>
192 | <%= cue.listName %> 193 | 194 | 00:00.00 195 | 196 | 00:00.00 197 | 198 | 199 | 200 | <% } %> 201 | 202 | <% 203 | function prettyFormatTime(seconds){ 204 | if(!seconds){ 205 | return "00:00.00"; 206 | } 207 | var startIndex = 14; 208 | if(seconds>=3600){ 209 | startIndex = 11; 210 | } 211 | var string = new Date(seconds * 1000).toISOString() 212 | return string.substring(startIndex, string.length-2) 213 | } 214 | 215 | function elapsedTime(def, value, type, cue){ 216 | 217 | if(type=="action" && cue.isPaused){ 218 | value-=cue.preWait; 219 | } 220 | 221 | let border, fill; 222 | if(cue.isPaused){ 223 | border = "#f6e737"; 224 | fill = "rgba(255, 240, 60, 0.7)"; 225 | }else{ 226 | border = "#48ba41"; 227 | fill = "rgba(0, 200, 50, 0.5)"; 228 | } 229 | 230 | 231 | if(value>0){ 232 | let percent = value/def*100; 233 | let bg = `style='background: black; background: linear-gradient(90deg, ${fill} ${percent}%, transparent ${percent}%);`; 234 | 235 | if(cue.isPaused){ 236 | bg+= "outline-color: "+border+";"; 237 | bg+= "color: white;"; 238 | } 239 | bg+="'"; 240 | 241 | return `
${prettyFormatTime(Math.min(def, value))}`; 242 | 243 | }else if(type=="postWait" && cue.continueMode==0){ 244 | return `${prettyFormatTime(def)}`; 245 | 246 | }else{ 247 | return prettyFormatTime(def); 248 | 249 | } 250 | } 251 | %> -------------------------------------------------------------------------------- /plugins/qlab/cuelist.ejs: -------------------------------------------------------------------------------- 1 |

<%= workspace.displayName %> — <%= cueList.listName %>

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | <% for(let i=0; i 31 | <%= displayCueRow(cueList.cues[i]) %> 32 | <% } %> 33 | 34 | 35 |
NumberNameTargetPre-WaitDurationPost-Wait 25 | 26 |
36 | 37 | 38 | <% 39 | function displayCueRow(q){ 40 | let html = rowTemplate({cue: q, allCues: allCues, workspace: workspace}); 41 | 42 | if(q.cues){ 43 | for(let i=0; i -------------------------------------------------------------------------------- /plugins/qlab/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/icon.png -------------------------------------------------------------------------------- /plugins/qlab/img/arm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/arm.png -------------------------------------------------------------------------------- /plugins/qlab/img/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/arrow-down.png -------------------------------------------------------------------------------- /plugins/qlab/img/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/arrow-right.png -------------------------------------------------------------------------------- /plugins/qlab/img/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/audio.png -------------------------------------------------------------------------------- /plugins/qlab/img/auto_continue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/auto_continue.png -------------------------------------------------------------------------------- /plugins/qlab/img/auto_continue_stubby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/auto_continue_stubby.png -------------------------------------------------------------------------------- /plugins/qlab/img/auto_follow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/auto_follow.png -------------------------------------------------------------------------------- /plugins/qlab/img/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/camera.png -------------------------------------------------------------------------------- /plugins/qlab/img/devamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/devamp.png -------------------------------------------------------------------------------- /plugins/qlab/img/disarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/disarm.png -------------------------------------------------------------------------------- /plugins/qlab/img/disarmed-pattern-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/disarmed-pattern-light.png -------------------------------------------------------------------------------- /plugins/qlab/img/disarmed-pattern-light.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/disarmed-pattern-light.tiff -------------------------------------------------------------------------------- /plugins/qlab/img/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/empty.png -------------------------------------------------------------------------------- /plugins/qlab/img/fade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/fade.png -------------------------------------------------------------------------------- /plugins/qlab/img/goto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/goto.png -------------------------------------------------------------------------------- /plugins/qlab/img/group-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/group-arrow.png -------------------------------------------------------------------------------- /plugins/qlab/img/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/group.png -------------------------------------------------------------------------------- /plugins/qlab/img/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/light.png -------------------------------------------------------------------------------- /plugins/qlab/img/load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/load.png -------------------------------------------------------------------------------- /plugins/qlab/img/memo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/memo.png -------------------------------------------------------------------------------- /plugins/qlab/img/mic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/mic.png -------------------------------------------------------------------------------- /plugins/qlab/img/midi-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/midi-file.png -------------------------------------------------------------------------------- /plugins/qlab/img/midi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/midi.png -------------------------------------------------------------------------------- /plugins/qlab/img/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/network.png -------------------------------------------------------------------------------- /plugins/qlab/img/new_group_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/new_group_arrow.png -------------------------------------------------------------------------------- /plugins/qlab/img/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/pause.png -------------------------------------------------------------------------------- /plugins/qlab/img/pause_circled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/pause_circled.png -------------------------------------------------------------------------------- /plugins/qlab/img/play_circled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/play_circled.png -------------------------------------------------------------------------------- /plugins/qlab/img/playhead.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/playhead.afdesign -------------------------------------------------------------------------------- /plugins/qlab/img/playhead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/playhead.png -------------------------------------------------------------------------------- /plugins/qlab/img/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/reset.png -------------------------------------------------------------------------------- /plugins/qlab/img/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/script.png -------------------------------------------------------------------------------- /plugins/qlab/img/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/start.png -------------------------------------------------------------------------------- /plugins/qlab/img/status_broken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_broken.png -------------------------------------------------------------------------------- /plugins/qlab/img/status_broken_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_broken_white.png -------------------------------------------------------------------------------- /plugins/qlab/img/status_flagged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_flagged.png -------------------------------------------------------------------------------- /plugins/qlab/img/status_loaded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_loaded.png -------------------------------------------------------------------------------- /plugins/qlab/img/status_paused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_paused.png -------------------------------------------------------------------------------- /plugins/qlab/img/status_running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_running.png -------------------------------------------------------------------------------- /plugins/qlab/img/status_spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_spinner.gif -------------------------------------------------------------------------------- /plugins/qlab/img/status_spinner.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/status_spinner.psd -------------------------------------------------------------------------------- /plugins/qlab/img/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/stop.png -------------------------------------------------------------------------------- /plugins/qlab/img/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/target.png -------------------------------------------------------------------------------- /plugins/qlab/img/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/text.png -------------------------------------------------------------------------------- /plugins/qlab/img/timecode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/timecode.png -------------------------------------------------------------------------------- /plugins/qlab/img/v5/group-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/v5/group-1.png -------------------------------------------------------------------------------- /plugins/qlab/img/v5/group-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/v5/group-2.png -------------------------------------------------------------------------------- /plugins/qlab/img/v5/group-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/v5/group-3.png -------------------------------------------------------------------------------- /plugins/qlab/img/v5/group-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/v5/group-4.png -------------------------------------------------------------------------------- /plugins/qlab/img/v5/group-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/v5/group-6.png -------------------------------------------------------------------------------- /plugins/qlab/img/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/video.png -------------------------------------------------------------------------------- /plugins/qlab/img/wait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/qlab/img/wait.png -------------------------------------------------------------------------------- /plugins/qlab/info.html: -------------------------------------------------------------------------------- 1 |

Connection Requirements

2 |
    3 |
  • View permission enabled in OSC Access
  • 4 |
  • Provide passcode if necessary
  • 5 |
6 | -------------------------------------------------------------------------------- /plugins/qlab/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-bottom: 200px !important; 3 | } 4 | table { 5 | background-color: #323232; 6 | border: 0px; 7 | color: #b8b8b8; 8 | font-family: sans-serif; 9 | width: 100%; 10 | table-layout: fixed; 11 | margin-bottom: 30px; 12 | } 13 | th { 14 | font-size: 12px; 15 | font-weight: normal; 16 | padding: 0px 4px; 17 | background-color: #2d2c2d; 18 | border: #545454 1px solid; 19 | border-right: none; 20 | } 21 | th.no-border { 22 | border-left: none; 23 | border-right: none; 24 | padding: 0px; 25 | } 26 | 27 | td { 28 | padding: -1px; 29 | height: 25px; 30 | overflow: hidden; 31 | white-space: nowrap; 32 | text-overflow: ellipsis; 33 | } 34 | td img { 35 | padding-top: 2px; 36 | } 37 | 38 | tr:nth-child(even) { 39 | background: #2d2d2d; 40 | } 41 | tr:nth-child(odd) { 42 | background: #262626; 43 | } 44 | 45 | tr:nth-child(odd).q-red { 46 | background: linear-gradient(to top, #262626, #49302f 1px); 47 | } 48 | tr:nth-child(even).q-red { 49 | background: linear-gradient(to top, #2d2c2d, #513635 1px); 50 | } 51 | .q-red td.playhead { 52 | background: linear-gradient(to right, #ff4242 55%, transparent 55%); 53 | } 54 | 55 | tr:nth-child(odd).q-orange { 56 | background: linear-gradient(to top, #262626, #443526 1px); 57 | } 58 | tr:nth-child(even).q-orange { 59 | background: linear-gradient(to top, #2d2c2d, #4a3b2c 1px); 60 | } 61 | .q-orange td.playhead { 62 | background: linear-gradient(to right, #ffa500 55%, transparent 55%); 63 | } 64 | 65 | tr:nth-child(odd).q-green { 66 | background: linear-gradient(to top, #262626, #2d3d27 1px); 67 | } 68 | tr:nth-child(even).q-green { 69 | background: linear-gradient(to top, #2d2c2d, #32422e 1px); 70 | } 71 | .q-green td.playhead { 72 | background: linear-gradient(to right, #01d52f 55%, transparent 55%); 73 | } 74 | 75 | tr:nth-child(odd).q-blue { 76 | background: linear-gradient(to top, #262626, #292d40 1px); 77 | } 78 | tr:nth-child(even).q-blue { 79 | background: linear-gradient(to top, #2d2c2d, #2f3346 1px); 80 | } 81 | .q-blue td.playhead { 82 | background: linear-gradient(to right, #536de0 55%, transparent 55%); 83 | } 84 | 85 | tr:nth-child(odd).q-purple { 86 | background: linear-gradient(to top, #262626, #342639 1px); 87 | } 88 | tr:nth-child(even).q-purple { 89 | background: linear-gradient(to top, #2d2c2d, #382c3f 1px); 90 | } 91 | .q-purple td.playhead { 92 | background: linear-gradient(to right, #a601c0 55%, transparent 55%); 93 | } 94 | 95 | tr.q-armed-false td { 96 | background: url('img/disarmed-pattern-light.png'); 97 | background-attachment: fixed; 98 | } 99 | 100 | tr.playback-position { 101 | background: #444 !important; 102 | color: white; 103 | } 104 | tr.playback-position td.playhead { 105 | padding: 0px; 106 | } 107 | tr.playback-position td.playhead::before { 108 | content: url(img/playhead.png); 109 | } 110 | tr.playback-position .q-gray-text { 111 | color: white !important; 112 | } 113 | tr.selected { 114 | background: #1557da !important; 115 | color: white !important; 116 | } 117 | 118 | .gLeft { 119 | height: 24px; 120 | width: 12px; 121 | border-left: 2px solid; 122 | } 123 | .gTop { 124 | border-top: 2px solid; 125 | } 126 | .gBot { 127 | border-bottom: 2px solid; 128 | } 129 | .gRight { 130 | border-right: 2px solid; 131 | } 132 | .gMode-1.gLeft.gTop { 133 | border-radius: 6px 0px 0px 0px; 134 | } 135 | .gMode-1.gLeft.gBot { 136 | border-radius: 0px 0px 0px 6px; 137 | } 138 | .gMode-1.gRight.gBot { 139 | border-radius: 0px 0px 6px 0px; 140 | } 141 | .gMode-1.gLeft.gTop.gBot { 142 | border-radius: 6px 0px 0px 6px; 143 | } 144 | .gMode-1.gLeft.gTop.gRight { 145 | border-radius: 6px 6px 0px 0px; 146 | } 147 | .gMode-1.gTop.gRight.gBot { 148 | border-radius: 0px 6px 6px 0px; 149 | } 150 | .gMode-1.gLeft.gTop.gRight.gBot { 151 | border-radius: 6px; 152 | } 153 | .gMode-1 { 154 | border-color: #48477f; 155 | } 156 | .gMode-2 { 157 | border-color: #48477f; 158 | } 159 | .gMode-3 { 160 | border-color: #43a424; 161 | } 162 | .gMode-4 { 163 | border-color: #7f26a5; 164 | } 165 | .gMode-6 { 166 | border-color: #ee6a21; 167 | } 168 | .gMode-, 169 | .gMode-0 { 170 | border-color: rgba(0, 0, 0, 0); 171 | } 172 | .group-arrow { 173 | width: 13px; 174 | background-image: url('img/new_group_arrow.png'); 175 | background-position: center; 176 | background-repeat: no-repeat; 177 | } 178 | 179 | .q-time { 180 | font-size: 14px; 181 | text-align: center; 182 | } 183 | .q-target { 184 | text-align: center; 185 | font-size: 12px; 186 | padding-left: 5px; 187 | padding-right: 5px; 188 | } 189 | .q-gray-text { 190 | color: #424242; 191 | } 192 | .q-time-elapsed { 193 | margin: 0px 2px; 194 | outline: #49c042 1px solid; 195 | line-height: 18px; 196 | } 197 | 198 | .cart { 199 | position: relative; 200 | height: 600px; 201 | width: 100%; 202 | background-color: #2c2b2a; 203 | } 204 | .cartCueWrapper { 205 | display: block; 206 | position: absolute; 207 | box-sizing: border-box; 208 | padding: 3px; 209 | } 210 | .cartCueWrapper.selected .cartCue { 211 | border-color: #89b4db; 212 | box-shadow: #89b4db 0px 0px 2px 3px; 213 | } 214 | .cartCue { 215 | height: 100%; 216 | box-sizing: border-box; 217 | 218 | border: 3px solid; 219 | border-radius: 6px; 220 | color: white; 221 | } 222 | .cartCue p { 223 | margin: 10px; 224 | } 225 | .cartCueIcon { 226 | position: absolute; 227 | right: 13px; 228 | top: 13px; 229 | } 230 | div.cartColor-red { 231 | border-color: #e7443a; 232 | background-color: #942f27; 233 | } 234 | div.cartColor-orange { 235 | border-color: #f18f28; 236 | background-color: #a95023; 237 | } 238 | div.cartColor-green { 239 | border-color: #4ebf32; 240 | background-color: #397824; 241 | } 242 | div.cartColor-blue { 243 | border-color: #3a54cf; 244 | background-color: #25378a; 245 | } 246 | div.cartColor-purple { 247 | border-color: #7e25a5; 248 | background-color: #4c2269; 249 | } 250 | div.cartColor-none { 251 | border-color: #95929f; 252 | background-color: #3b3b3b; 253 | } 254 | .cartBlank { 255 | border-color: #1f1f1f; 256 | background-color: #1f1f1f; 257 | } 258 | .cartCueWrapper.playback-position .cartCue { 259 | border-color: #88b3db; 260 | outline: #88b3db 1px solid; 261 | box-shadow: 0px 0px 3px 3px #88b3db, inset 0px 0px 5px #88b3db; 262 | } 263 | 264 | #playhead-information { 265 | width: 80%; 266 | height: 120px; 267 | position: fixed; 268 | bottom: 30px; 269 | left: 10%; 270 | padding: 16px; 271 | box-sizing: border-box; 272 | 273 | background-color: #2e2d2d; 274 | border: #424242 3px solid; 275 | border-radius: 4px; 276 | box-shadow: black 0px 0px 20px 5px; 277 | } 278 | #playhead-information.playhead-active { 279 | border-color: #4cbe34; 280 | } 281 | .playhead-name { 282 | width: 100%; 283 | padding: 6px 12px; 284 | box-sizing: border-box; 285 | margin-bottom: 12px; 286 | 287 | background-color: #434343; 288 | border: #434343 1px solid; 289 | font-size: 18px; 290 | color: white; 291 | border-radius: 3px; 292 | } 293 | .playhead-disarmed { 294 | background: url('img/disarmed-pattern-light.png'); 295 | background-attachment: fixed; 296 | } 297 | #playhead-notes { 298 | width: 100%; 299 | padding: 6px 8px; 300 | box-sizing: border-box; 301 | height: 36px; 302 | 303 | border: #5c5b5b 1px solid; 304 | font-size: 18px; 305 | color: #9a9a99; 306 | border-radius: 3px; 307 | overflow: hidden; 308 | text-overflow: ellipsis; 309 | } 310 | 311 | @media screen and (min-width: 0px) and (max-width: 750px) { 312 | .hide-medium { 313 | display: none; 314 | } 315 | } 316 | @media screen and (min-width: 0px) and (max-width: 550px) { 317 | .hide-small { 318 | display: none; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /plugins/qlab/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName %>

3 |

QLab <%= data.version || "" %>

4 |
5 | 6 | 7 | 8 | <% const workspaceKeys = Object.keys(data.workspaces); %> 9 | 10 | <% if(workspaceKeys.length==0){ %> 11 |

QLab is open but there isn't an open Workspace

12 | <% } %> 13 | 14 | 15 | <% for(let i=0; i 16 | <% const workspace = data.workspaces[workspaceKeys[i]]; %> 17 | 18 | <% if(workspace.permission=="ok"){ %> 19 | 20 | <% for(let j=0; j 21 | <% const cueList = workspace.cueLists[j]; %> 22 | 23 | <% if(cueList.type=="Cue List"){ %> 24 | <%= templates.cuelist({cueList: cueList, allCues: data.cueKeys, rowTemplate: templates.cue, workspace: workspace}) %> 25 | <% }else if(cueList.type=="Cart"){ %> 26 | <%= templates.cart({cueList: cueList, allCues: data.cueKeys, tileTemplate: templates.tile, workspace: workspace}) %> 27 | <% } %> 28 | 29 | <% } %> 30 | 31 | <% }else{ %> 32 | 33 |

<%= workspace.displayName %> — Incorrect Passcode or OSC Access Permissions

34 | 35 |

 

36 | <% } %> 37 | 38 | <% } %> 39 | 40 |
41 |
[no cue on standby]
42 |
43 |
44 | 45 | -------------------------------------------------------------------------------- /plugins/qlab/tile.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | let height = 0; 3 | let width = 0; 4 | let left = 0; 5 | let top = 0; 6 | let cueKeys = allCues[cue.uniqueID]; 7 | let parent = allCues[cue.parent]; 8 | 9 | if(cueKeys.parent){ 10 | width = 100/cueKeys.parent.cartColumns; 11 | height = 600/cueKeys.parent.cartRows; 12 | 13 | top = (cueKeys.cartPosition[0]-1)*height; 14 | left = (cueKeys.cartPosition[1]-1)*width; 15 | } 16 | 17 | let style = `style="left:${left}%; top:${top}px; width:${width }%; height:${height}px;"`; 18 | 19 | %> 20 | 21 |
> 22 | 23 |
24 | <% if(cueKeys.isBroken){ %> 25 | 26 | <% }else if(cueKeys.isRunning){ %> 27 | 28 | <% }else{ %> 29 | 30 | <% } %> 31 |
32 | 33 |
34 |

35 | <%= cueKeys.number %> 36 | <% if(cueKeys.number){ %> • <% } %> 37 | <%= cueKeys.displayName %> 38 |

39 |
40 | 41 |
-------------------------------------------------------------------------------- /plugins/sacn/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/sacn/icon.png -------------------------------------------------------------------------------- /plugins/sacn/info.html: -------------------------------------------------------------------------------- 1 |

sACN requires no configuration.

2 | -------------------------------------------------------------------------------- /plugins/sacn/main.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | exports.config = { 4 | defaultName: 'sACN', 5 | connectionType: 'multicast', 6 | remotePort: 5568, 7 | mayChangePorts: false, 8 | heartbeatInterval: 5000, 9 | heartbeatTimeout: 15000, 10 | searchOptions: { 11 | type: 'multicast', 12 | address: getMulticastGroup(1), 13 | port: 5568, 14 | validateResponse(msg, info) { 15 | return msg.toString('utf8', 4, 13) === 'ASC-E1.17'; 16 | }, 17 | }, 18 | }; 19 | 20 | exports.ready = function ready(device) { 21 | const d = device; 22 | d.data.universes = {}; 23 | d.data.priorities = {}; 24 | d.data.source = 'Unknown Source'; 25 | d.data.orderedUniverses = []; 26 | 27 | const networkInterfaces = d.getNetworkInterfaces(); 28 | 29 | for (let i = 1; i <= 16; i++) { 30 | for (let j = 0; j < Object.keys(networkInterfaces).length; j++) { 31 | const networkInterfaceID = Object.keys(networkInterfaces)[j]; 32 | const networkInterface = networkInterfaces[networkInterfaceID]; 33 | d.connection.addMembership(getMulticastGroup(i), networkInterface[0].address); 34 | } 35 | } 36 | }; 37 | 38 | exports.data = function data(_device, buf) { 39 | const universeIndex = buf.readUInt16BE(113); 40 | const device = _device; 41 | 42 | let universe = device.data.universes[universeIndex]; 43 | let priorities = device.data.priorities[universeIndex]; 44 | 45 | if (!universe) { 46 | device.data.universes[universeIndex] = {}; 47 | universe = device.data.universes[universeIndex]; 48 | } 49 | if (!priorities) { 50 | device.data.priorities[universeIndex] = new Array(512).fill(0); 51 | priorities = device.data.priorities[universeIndex]; 52 | } 53 | 54 | universe.sequence = buf.readUInt8(111); 55 | universe.priority = buf.readUInt8(108); 56 | universe.cid = buf.toString('hex', 22, 38); 57 | universe.slots = buf.slice(126); 58 | universe.startCode = buf.readUInt8(125); 59 | 60 | device.data.source = buf.toString('utf8', 44, 108); 61 | device.displayName = `${device.data.source} sACN`; 62 | device.data.ip = device.addresses[0]; 63 | 64 | if (!_.includes(device.data.orderedUniverses, universeIndex)) { 65 | device.data.orderedUniverses.push(universeIndex); 66 | device.data.orderedUniverses.sort(); 67 | universe.slotElems = []; 68 | universe.slotElemsSet = false; 69 | 70 | if (universe.priority > 0 && universe.startCode === 0) { 71 | device.draw(); 72 | device.update('elementCache'); 73 | } 74 | } 75 | if (universe.priority > 0 && universe.startCode === 0) { 76 | device.update('universeData', { 77 | universeIndex, 78 | universe, 79 | startCode: universe.startCode, 80 | }); 81 | } else { 82 | device.data.priorities[universeIndex] = buf.slice(126); 83 | } 84 | }; 85 | 86 | exports.heartbeat = function heartbeat(device) {}; 87 | 88 | let lastUpdate = Date.now(); 89 | exports.update = function update(_device, doc, updateType, updateData) { 90 | const device = _device; 91 | const data = updateData; 92 | 93 | if (updateType === 'universeData' && data.universe) { 94 | if (Date.now() - lastUpdate > 1000) { 95 | lastUpdate = Date.now(); 96 | device.update('elementCache'); 97 | } 98 | 99 | const $elem = doc.getElementById(`universe-${data.universeIndex}`); 100 | 101 | if ($elem && data.universe.slotElemsSet) { 102 | for (let i = 0; i < 512; i++) { 103 | data.universe.slotElems[i].textContent = data.universe.slots[i]; 104 | } 105 | const $code = doc.getElementById(`universe-${data.universeIndex}-code`); 106 | if (data.startCode === 0xdd) { 107 | $code.textContent = 'Net3'; 108 | } else if (data.startCode === 0x17) { 109 | $code.textContent = 'Text'; 110 | } else if (data.startCode === 0xcf) { 111 | $code.textContent = 'SIP'; 112 | } else if (data.startCode === 0xcc) { 113 | $code.textContent = 'RDM'; 114 | } 115 | } else if (data.universe.startCode === 0) { 116 | device.draw(); 117 | device.update('elementCache'); 118 | } 119 | } else if (updateType === 'elementCache') { 120 | device.data.orderedUniverses.forEach((universeIndex) => { 121 | const universe = device.data.universes[universeIndex]; 122 | 123 | if (doc.getElementById(`${universeIndex}-0`)) { 124 | for (let i = 0; i < 512; i++) { 125 | universe.slotElems[i] = doc.getElementById(`${universeIndex}-${i}`); 126 | universe.slotElems[i].title = `${universeIndex}/${i} ${device.data.priorities[universeIndex][i]}`; 127 | } 128 | universe.slotElemsSet = true; 129 | } 130 | }); 131 | } 132 | }; 133 | 134 | // From https://github.com/hhromic/e131-node/blob/master/lib/e131.js 135 | function getMulticastGroup(universe) { 136 | if (universe < 1 || universe > 63999) { 137 | throw new RangeError('universe should be in the range [1-63999]'); 138 | } 139 | return `239.255.${universe >> 8}.${universe & 0xff}`; 140 | } 141 | -------------------------------------------------------------------------------- /plugins/sacn/styles.css: -------------------------------------------------------------------------------- 1 | table { 2 | margin-bottom: 50px; 3 | background-color: #333; 4 | } 5 | td, 6 | th { 7 | width: 35px; 8 | } 9 | td { 10 | background-color: black; 11 | text-align: center; 12 | border-radius: 3px; 13 | color: white; 14 | font-size: 13px; 15 | } 16 | th { 17 | background-color: #222; 18 | color: #ccc; 19 | font-size: 11px; 20 | } 21 | td.data { 22 | padding: 7px; 23 | font-size: 12px; 24 | text-align: left; 25 | } 26 | td.data em { 27 | font-size: 14px; 28 | color: #ff0033; 29 | } 30 | -------------------------------------------------------------------------------- /plugins/sacn/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= data.source %> sACN (E1.31)

3 |
4 | 5 | 6 |
7 | <% data.orderedUniverses.forEach(universeIndex => { 8 | let universe = data.universes[universeIndex]; 9 | %> 10 | 11 | 12 | 13 | 17 | 21 | 25 | 29 | 33 | 34 | 35 | 36 | 37 | <% for(col=1; col<=16; col++){ %> 38 | 39 | <% } %> 40 | 41 | 42 | <% let slot = 0; %> 43 | <% for(row=0; row<32; row++){ %> 44 | 45 | 46 | <% for(col=0; col<16; col++){ %> 47 | <% slot++%> 48 | 49 | <% } %> 50 | 51 | <% } %> 52 | 53 | 54 |
14 | Universe 15 |
<%= universeIndex %> 16 |
18 | Priority 19 |
<%= universe.priority %> 20 |
22 | CID 23 |
<%= universe.cid %> 24 |
26 | IP 27 |
<%= data.ip %> 28 |
30 | Flavor 31 |
0 32 |
<%= col %>
<%= row*16+1 %><%= universe.slots[slot-1] %>
55 | 56 | <% }); %> 57 | 58 |
-------------------------------------------------------------------------------- /plugins/shure/channel.js: -------------------------------------------------------------------------------- 1 | class Channel { 2 | constructor() { 3 | this.chan_name = '?'; 4 | this.batt_bars = 255; 5 | this.batt_run_time = 65535; 6 | this.audio_gain = 0; 7 | this.audio_lvl = 0; 8 | this.rx_rf_lvl = 0; 9 | this.rf_antenna = 0; 10 | this.tx_type = 0; 11 | this.rx_graph_bars = 0; 12 | } 13 | } 14 | 15 | module.exports = Channel; 16 | -------------------------------------------------------------------------------- /plugins/shure/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/shure/icon.png -------------------------------------------------------------------------------- /plugins/shure/info.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/shure/info.html -------------------------------------------------------------------------------- /plugins/shure/main.js: -------------------------------------------------------------------------------- 1 | const Channel = require('./channel'); 2 | 3 | exports.config = { 4 | defaultName: 'Shure Wireless', 5 | connectionType: 'TCPsocket', 6 | remotePort: 2202, 7 | mayChangePorts: false, 8 | heartbeatInterval: 5000, 9 | heartbeatTimeout: 10000, 10 | searchOptions: { 11 | type: 'TCPport', 12 | searchBuffer: Buffer.from('< GET DEVICE_ID >', 'ascii'), 13 | testPort: 2202, 14 | validateResponse(msg, info) { 15 | return msg.toString().includes('DEVICE_ID'); 16 | }, 17 | }, 18 | }; 19 | 20 | exports.ready = function ready(_device) { 21 | const device = _device; 22 | device.data.channelCount = 0; 23 | device.data.channels = [{}, new Channel(), new Channel(), new Channel(), new Channel()]; 24 | 25 | device.send('< GET 0 ALL >'); 26 | device.send('< SET 0 METER_RATE 00100 >'); 27 | device.send('< SAMPLE 0 AUDIO_LVL>'); 28 | }; 29 | 30 | exports.data = function data(_device, message) { 31 | const device = _device; 32 | let msgStr = message.toString(); 33 | 34 | if (!msgStr.startsWith('< ')) { 35 | return; 36 | } 37 | 38 | // console.log(msgStr); 39 | 40 | msgStr = msgStr.slice(2).slice(0, -1); 41 | const msgs = msgStr.split('><'); 42 | 43 | msgs.forEach((ms, i) => { 44 | const msg = ms.trim(); 45 | const msgParts = msg.split(' '); 46 | const channelNumber = Number(msgParts[1]); 47 | const channel = device.data.channels[channelNumber]; 48 | 49 | if (msgParts[0] === 'REP') { 50 | if (msgParts[2] === 'CHAN_NAME') { 51 | channel.chan_name = msg.substring(17).slice(0, -2).trim(); 52 | if (device.data.channelCount < channelNumber) { 53 | device.data.channelCount = channelNumber; 54 | } 55 | device.draw(); 56 | } else if (msgParts[2] === 'BATT_BARS') { 57 | channel.batt_bars = Number(msgParts[3]); 58 | } else if (msgParts[2] === 'BATT_RUN_TIME') { 59 | channel.batt_run_time = Number(msgParts[3]); 60 | } else if (msgParts[2] === 'AUDIO_GAIN') { 61 | channel.audio_gain = Number(msgParts[3]) - 18; 62 | } else if (msgParts[2] === 'AUDIO_LVL') { 63 | channel.audio_lvl = Number(msgParts[3]); 64 | } else if (msgParts[2] === 'RX_RF_LVL') { 65 | channel.rx_rf_lvl = Number(msgParts[3]) - 128; 66 | } else if (msgParts[2] === 'RF_ANTENNA') { 67 | channel.rf_antenna = msgParts[3]; 68 | } else if (msgParts[2] === 'TX_TYPE') { 69 | channel.tx_type = msgParts[3]; 70 | device.draw(); 71 | } else if (msgParts[1] === 'DEVICE_ID') { 72 | const id = msg.substring(15).slice(0, -1).trim(); 73 | this.deviceInfoUpdate(device, 'defaultName', id); 74 | } else if (msgParts[1] === 'FW_VER') { 75 | device.data.version = msgParts[2].substring(1); 76 | } 77 | } else if (msgParts[0] === 'SAMPLE') { 78 | channel.rf_antenna = msgParts[3]; 79 | channel.rx_rf_lvl = Number(msgParts[4]) - 128; 80 | channel.audio_lvl = Number(msgParts[5]); 81 | if (channelNumber === 4) { 82 | device.update('updateSample', { 83 | channels: device.data.channels, 84 | }); 85 | } 86 | } 87 | }); 88 | }; 89 | 90 | exports.update = function update(device, doc, updateType, data) { 91 | for (let i = 1; i < data.channels.length; i++) { 92 | const channel = data.channels[i]; 93 | const $audio = doc.getElementById(`ch-${i}-audio`); 94 | const $audioText = doc.getElementById(`ch-${i}-audio-text`); 95 | if ($audio) { 96 | $audio.style.height = 90 - channel.audio_lvl * 2; 97 | } 98 | if ($audioText) { 99 | $audioText.textContent = channel.audio_lvl; 100 | } 101 | 102 | const $rfA = doc.getElementById(`ch-${i}-a`); 103 | const $rfB = doc.getElementById(`ch-${i}-b`); 104 | let rfClass = ''; 105 | 106 | if ($rfA) { 107 | if (channel.rf_antenna.charAt(0) === 'A') { 108 | $rfA.style.color = '#53c3c3'; 109 | rfClass = 'color-1'; 110 | } else { 111 | $rfA.style.color = '#333'; 112 | } 113 | } 114 | if ($rfB) { 115 | if (channel.rf_antenna.charAt(1) === 'B') { 116 | $rfB.style.color = '#53c3c3'; 117 | rfClass = 'color-2'; 118 | } else { 119 | $rfB.style.color = '#333'; 120 | } 121 | } 122 | 123 | const $rf = doc.getElementById(`ch-${i}-rf`); 124 | const $rfGraph = doc.getElementById(`ch-${i}-graph`); 125 | const $rfText = doc.getElementById(`ch-${i}-rf-text`); 126 | const rfHeight = 90 - (channel.rx_rf_lvl + 90) * 2; 127 | if ($rf) { 128 | $rf.style.height = rfHeight; 129 | } 130 | if ($rfGraph) { 131 | if ($rfGraph.childElementCount > 115) { 132 | $rfGraph.removeChild($rfGraph.firstElementChild); 133 | } 134 | $rfGraph.insertAdjacentHTML( 135 | 'beforeend', 136 | `
` 137 | ); 138 | channel.rx_graph_bars++; 139 | } 140 | if ($rfText) { 141 | $rfText.textContent = channel.rx_rf_lvl; 142 | } 143 | } 144 | }; 145 | 146 | exports.heartbeat = function heartbeat(device) { 147 | device.send('< GET 0 ALL >'); 148 | device.send('< SET 0 METER_RATE 00100 >'); 149 | device.send('< SAMPLE 0 AUDIO_LVL>'); 150 | }; 151 | -------------------------------------------------------------------------------- /plugins/shure/styles.css: -------------------------------------------------------------------------------- 1 | table.channel { 2 | width: 120px; 3 | height: 320px; 4 | background-color: #1a1a1d; 5 | border: #3b3c42 1px solid; 6 | border-collapse: collapse; 7 | color: white; 8 | text-align: center; 9 | float: left; 10 | margin: 2px; 11 | margin-bottom: 20px; 12 | } 13 | table td { 14 | border: #3b3c42 1px solid; 15 | vertical-align: top; 16 | } 17 | 18 | .chan_name { 19 | width: 100px; 20 | color: #b2ff33 !important; 21 | font-size: 18px; 22 | font-weight: bold; 23 | } 24 | 25 | .bar-wrapper { 26 | width: 20px; 27 | height: 90px; 28 | margin: 0px auto; 29 | margin-top: 5px; 30 | margin-bottom: 5px; 31 | 32 | border-radius: 6px; 33 | outline: #1a1a1d 2px solid; 34 | outline-offset: -1px; 35 | overflow: hidden; 36 | } 37 | .bar-wrapper.green { 38 | background: linear-gradient(0deg, #4742af, #554ed4 30%, #554ed4 80%, #df664d); 39 | } 40 | .bar-wrapper.pink { 41 | background: linear-gradient(0deg, #383943, #4e505d 40%); 42 | } 43 | .bar-wrapper.orange { 44 | background: linear-gradient(0deg, #50c3c3, #5bdfe0 30%, #5bdfe0 80%); 45 | } 46 | .bar { 47 | width: 20px; 48 | background-color: #202124; 49 | outline: rgba(255, 255, 255, 0.1) 1px solid; 50 | } 51 | 52 | .batt-wrapper { 53 | width: 84px; 54 | height: 35px; 55 | margin: 0px auto; 56 | border: #333 2px solid; 57 | border-radius: 5px; 58 | padding: 2px; 59 | margin: 10px auto; 60 | position: relative; 61 | } 62 | .batt-wrapper.green { 63 | border-color: greenyellow; 64 | } 65 | .batt-knob { 66 | background-color: #333; 67 | position: absolute; 68 | right: -5px; 69 | top: 10px; 70 | width: 5px; 71 | height: 16px; 72 | border-radius: 2px; 73 | } 74 | .batt-knob.green { 75 | background-color: #adff2f; 76 | } 77 | .batt-bar { 78 | height: 26px; 79 | background: linear-gradient(0deg, rgb(145, 220, 33), greenyellow 100%); 80 | border-radius: 2px; 81 | color: #1a1a1d; 82 | padding-top: 9px; 83 | } 84 | 85 | .rf-indicator-wrapper { 86 | line-height: 8px; 87 | margin-bottom: 4px; 88 | font-size: 6px; 89 | padding-top: 4px; 90 | letter-spacing: 1px; 91 | } 92 | .rf-indicator { 93 | color: #333; 94 | } 95 | 96 | small { 97 | color: dimgray; 98 | } 99 | small small { 100 | color: #444; 101 | } 102 | 103 | .rf-graph { 104 | height: 80px; 105 | vertical-align: bottom; 106 | text-align: left; 107 | } 108 | .rf-graph-bar { 109 | padding: 0px; 110 | margin: 0px; 111 | width: 1px; 112 | height: 1px; 113 | background: #383943; 114 | display: inline-block; 115 | } 116 | .color-1 { 117 | background: #292662; 118 | border-top: #554ed4 1px solid; 119 | } 120 | .color-2 { 121 | background: #283636; 122 | border-top: #50c3c3 1px solid; 123 | } 124 | -------------------------------------------------------------------------------- /plugins/shure/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName %>

3 |

<%= data.version || "" %>

4 |
5 | 6 | <% for(let i=1; i<=data.channelCount; i++){ %> <% let ch = data.channels[i]; %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 29 | 40 | 48 | 49 | 50 | 53 | 54 | 55 | 68 | 69 | 70 | 74 | 75 |
<%= i %>
<%= ch.chan_name %>
16 |
17 | 18 | 19 |
20 | rf 21 |
22 |
26 |
27 | <%= ch.rx_rf_lvl %>
dBm
28 |
30 |
 
31 | audio 32 |
33 |
37 |
38 | <%= ch.audio_lvl %>
dBFS
39 |
41 |
 
42 | gain 43 |
44 |
45 |
46 | <%= ch.audio_gain %>
dB
47 |
51 | 52 |
56 | <% if(ch.batt_bars==255){ %> 57 |
58 | <% }else{ %> 59 |
60 |
61 | <% if(ch.batt_run_time<=65532){ %> <%= Math.floor(ch.batt_run_time/60) 62 | %>: <%= ch.batt_run_time%60 %> <% } %> 63 |
64 |
65 |
66 | <% } %> 67 |
71 | <% if(ch.tx_type=="UNKN"){ %>  No Transmitter  <% 72 | }else{ %> <%= ch.tx_type %> <% } %> 73 |
76 | 77 | <% } %> 78 | -------------------------------------------------------------------------------- /plugins/watchout/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/watchout/icon.png -------------------------------------------------------------------------------- /plugins/watchout/info.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/watchout/info.html -------------------------------------------------------------------------------- /plugins/watchout/main.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | defaultName: 'Dataton Watchout', 3 | connectionType: 'TCPsocket', 4 | remotePort: 3040, 5 | mayChangePorts: false, 6 | heartbeatInterval: 250, 7 | heartbeatTimeout: 5000, 8 | searchOptions: { 9 | type: 'TCPport', 10 | searchBuffer: Buffer.from('authenticate 1\n', 'ascii'), 11 | testPort: 3040, 12 | validateResponse(msg, info) { 13 | return msg.toString().substring(0, 5) === 'Ready'; 14 | }, 15 | }, 16 | }; 17 | 18 | exports.ready = function ready(device) { 19 | device.send('authenticate 1\n'); 20 | }; 21 | 22 | exports.data = function data(_device, _message) { 23 | const message = _message.toString(); 24 | const device = _device; 25 | 26 | if (message.substring(0, 5) === 'Ready') { 27 | // device.send('getStatus\n'); 28 | } else if (message.substring(0, 5) === 'Reply') { 29 | const arr = message.split(' '); 30 | 31 | device.data.showName = ''; 32 | 33 | let i = 0; 34 | while (arr[i][arr[i].length - 1] !== '"') { 35 | i++; 36 | device.data.showName += `${arr[i]} `; 37 | } 38 | 39 | device.data.showName = device.data.showName.substring(1, device.data.showName.length - 2); 40 | 41 | i--; 42 | device.data.busy = arr[i + 2]; 43 | device.data.health = arr[i + 3]; 44 | device.data.displayOpen = arr[i + 4]; 45 | device.data.showActive = arr[i + 5]; 46 | device.data.programmerOnline = arr[i + 6]; 47 | device.data.position = Number(arr[i + 7]).toFixed(2); 48 | device.data.rate = arr[i + 8]; 49 | device.data.standby = arr[i + 9]; 50 | 51 | this.deviceInfoUpdate(device, 'defaultName', device.data.showName); 52 | device.draw(); 53 | } 54 | }; 55 | 56 | exports.heartbeat = function heartbeat(device) { 57 | device.send('getStatus\n'); 58 | }; 59 | -------------------------------------------------------------------------------- /plugins/watchout/styles.css: -------------------------------------------------------------------------------- 1 | .warning { 2 | color: #e9873a; 3 | } 4 | .error { 5 | color: #ed5f5d; 6 | } 7 | .ok { 8 | color: #79b757; 9 | } 10 | -------------------------------------------------------------------------------- /plugins/watchout/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName %>

3 |
4 | 5 | 6 | 7 | 14 | 15 | 16 | <% if(data.position){ %> 17 | <% var seconds = Math.floor(data.position/1000) %> 18 | <% var minutes = Math.floor(seconds/60) %> 19 | <% var millis = data.position %> 20 | 21 | <% }else{ %> 22 | 23 | <% } %> 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 62 | 63 |
8 | <% if(data.rate=="true"){ %> 9 | Playing 10 | <% }else{ %> 11 | Stopped 12 | <% } %> 13 |

<%= minutes %>:<%= ((seconds%60)+"").padStart(2, "0") %>.<%= millis%1000 %>

00:00.00

Busy
<%= data.busy %>
Display Open
<%= data.displayOpen %>
Active
<%= data.showActive %>
Programmer
<%= data.programmerOnline %>
Health
<%= data.health %>
Standby
<%= data.standby %>
Error 53 | <% if(data.error==1){ %>Operating System Error<% } %> 54 | <% if(data.error==2){ %>QuickTime Error<% } %> 55 | <% if(data.error==3){ %>Rendering API Error<% } %> 56 | <% if(data.error==4){ %>Network Error<% } %> 57 | <% if(data.error==5){ %>File Server Error<% } %> 58 | <% if(data.error==6){ %>Syntax/Parser Error<% } %> 59 | <% if(data.error==7){ %>General Runtime Error<% } %> 60 | <% if(data.error==8){ %>Authentication Error<% } %> 61 |
-------------------------------------------------------------------------------- /plugins/x32/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/x32/icon.png -------------------------------------------------------------------------------- /plugins/x32/info.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/x32/info.html -------------------------------------------------------------------------------- /plugins/x32/main.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | defaultName: 'X32 Mixer', 3 | connectionType: 'osc-udp', 4 | remotePort: 10023, 5 | mayChangePorts: false, 6 | heartbeatInterval: 9000, 7 | heartbeatTimeout: 11000, 8 | searchOptions: { 9 | type: 'UDPsocket', 10 | searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]), 11 | devicePort: 10023, 12 | listenPort: 0, 13 | validateResponse(msg, info) { 14 | return msg.toString().includes('/xinfo'); 15 | }, 16 | }, 17 | }; 18 | 19 | exports.ready = function ready(_device) { 20 | const device = _device; 21 | device.data = new Console(); 22 | device.send('/xinfo'); 23 | 24 | device.send('/batchsubscribe', [ 25 | { type: 's', value: '/ch/meters' }, 26 | { type: 's', value: '/meters/0' }, 27 | { type: 'i', value: 0 }, 28 | { type: 'i', value: 0 }, 29 | { type: 'i', value: 1 }, 30 | ]); 31 | 32 | device.send('/batchsubscribe', [ 33 | { type: 's', value: '/main/meters' }, 34 | { type: 's', value: '/meters/2' }, 35 | { type: 'i', value: 0 }, 36 | { type: 'i', value: 0 }, 37 | { type: 'i', value: 1 }, 38 | ]); 39 | }; 40 | 41 | function parseAddress(msg) { 42 | const addr = msg.split('/'); 43 | addr.shift(); 44 | return addr; 45 | } 46 | 47 | exports.data = function data(_device, oscData) { 48 | this.deviceInfoUpdate(_device, 'status', 'ok'); 49 | 50 | const device = _device; 51 | 52 | if (oscData.address === '/xinfo') { 53 | device.data.info.name = oscData.args[1]; 54 | device.data.info.ip = oscData.args[0]; 55 | device.data.info.firmware = oscData.args[3]; 56 | device.data.info.model = oscData.args[2]; 57 | 58 | this.deviceInfoUpdate(_device, 'defaultName', device.data.info.name); 59 | 60 | device.send('/main/st/config/name'); 61 | 62 | for (let i = 0; i <= 32; i++) { 63 | device.send(`/ch/${i.toString().padStart(2, '0')}/config/name`); 64 | } 65 | device.draw(); 66 | } else if (oscData.address.includes('/ch/meters')) { 67 | const buf = Buffer.from(oscData.args[0]); 68 | 69 | let offset = 4; // skip first 4 bytes they are the length bytes 70 | for (let i = 0; i < 70; i++) { 71 | if (i >= 0 && i < 32) { 72 | // These are channel meters 73 | device.data.inputs.channels[i].meter = Console.getBehringerDB(buf.readFloatLE(offset)); 74 | } 75 | 76 | offset += 4; 77 | } 78 | device.draw(); 79 | } else if (oscData.address.includes('/main/meters')) { 80 | const buf = Buffer.from(oscData.args[0]); 81 | let offset = 4; // skip first 4 bytes they are the length bytes 82 | 83 | for (let i = 0; i < 49; i++) { 84 | if (i === 22) { 85 | // STEREO LEFT METER 86 | device.data.main.stereo.meter[0] = Console.getBehringerDB(buf.readFloatLE(offset)); 87 | } else if (i === 23) { 88 | // STEREO RIGHT METER 89 | device.data.main.stereo.meter[1] = Console.getBehringerDB(buf.readFloatLE(offset)); 90 | } 91 | offset += 4; 92 | } 93 | } else if (oscData.address.includes('/mix/fader')) { 94 | const addr = parseAddress(oscData.address); 95 | 96 | if (addr[0] === 'ch') { 97 | const channel = Number(addr[1]); 98 | device.data.inputs.channels[channel - 1].fader = oscData.args[0]; 99 | device.data.inputs.channels[channel - 1].faderDB = Console.getBehringerDB(oscData.args[0]); 100 | } else if (addr[0] === 'main') { 101 | device.data.main.stereo.fader = oscData.args[0]; 102 | device.data.main.stereo.faderDB = Console.getBehringerDB(oscData.args[0]); 103 | } 104 | 105 | device.draw(); 106 | } else if (oscData.address.includes('/mix/on')) { 107 | const addr = parseAddress(oscData.address); 108 | if (addr[0] === 'ch') { 109 | const channel = Number(addr[1]); 110 | device.data.inputs.channels[channel - 1].mute = oscData.args[0]; 111 | device.send(`/ch/${addr[1]}/mix/fader`); 112 | } else if (addr[0] === 'main') { 113 | device.data.main.stereo.mute = oscData.args[0]; 114 | device.send(`/main/${addr[1]}/mix/fader`); 115 | } 116 | device.draw(); 117 | } else if (oscData.address.includes('/config/name')) { 118 | const addr = parseAddress(oscData.address); 119 | if (addr[0] === 'main') { 120 | if (addr[1] === 'st') { 121 | device.data.main.stereo.name = oscData.args[0]; 122 | if (device.data.main.stereo.name === '') { 123 | device.data.main.stereo.name = 'LR'; 124 | } 125 | device.send(`/main/${addr[1]}/config/color`); 126 | } 127 | } else if (addr[0] === 'ch') { 128 | const channel = Number(addr[1]); 129 | device.data.inputs.channels[channel - 1].name = oscData.args[0]; 130 | device.send(`/ch/${addr[1]}/config/color`); 131 | } 132 | device.draw(); 133 | } else if (oscData.address.includes('/config/color')) { 134 | const addr = parseAddress(oscData.address); 135 | if (addr[0] === 'main') { 136 | device.data.main.stereo.color = oscData.args[0]; 137 | device.send(`/main/${addr[1]}/mix/on`); 138 | } else if (addr[0] === 'ch') { 139 | const channel = Number(addr[1]); 140 | device.data.inputs.channels[channel - 1].color = oscData.args[0]; 141 | device.send(`/ch/${addr[1]}/mix/on`); 142 | } 143 | device.draw(); 144 | } 145 | }; 146 | 147 | exports.heartbeat = function heartbeat(device) { 148 | device.send('/xremote'); 149 | 150 | device.send('/renew', [{ type: 's', value: '/ch/meters' }]); 151 | device.send('/renew', [{ type: 's', value: '/main/meters' }]); 152 | }; 153 | 154 | class Console { 155 | constructor() { 156 | this.inputs = { 157 | channels: new Array(32).fill(0).map(() => ({ 158 | fader: 0, 159 | faderDB: 0, 160 | mute: 0, 161 | name: 'end', 162 | color: undefined, 163 | meter: -90, 164 | })), 165 | }; 166 | 167 | this.main = { 168 | stereo: { 169 | fader: 0, 170 | faderDB: 0, 171 | mute: 0, 172 | name: 'LR', 173 | color: 7, 174 | meter: new Array(2).fill(-90), 175 | }, 176 | }; 177 | 178 | this.info = { 179 | name: '', 180 | ip: '', 181 | firmware: '', 182 | model: '', 183 | }; 184 | } 185 | 186 | static getBehringerDB(level) { 187 | const f = level; 188 | if (f >= 0.5) { 189 | return f * 40 - 30; 190 | } 191 | if (f >= 0.25) { 192 | return f * 80 - 50; 193 | } 194 | if (f >= 0.0625) { 195 | return f * 160 - 70; 196 | } 197 | return f * 480 - 90; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /plugins/x32/styles.css: -------------------------------------------------------------------------------- 1 | td { 2 | padding: 3px 16px !important; 3 | } 4 | tr td:first-child { 5 | text-align: center; 6 | } 7 | 8 | .mute-0, 9 | .mute-1 { 10 | width: 30px; 11 | padding: 4px; 12 | font-size: 14px; 13 | border: black 2px solid; 14 | text-align: center; 15 | border-radius: 10px; 16 | } 17 | .mute-1 { 18 | border-color: gray; 19 | color: gray; 20 | } 21 | .mute-0 { 22 | border-color: #fc3344; 23 | color: #fc3344; 24 | } 25 | .white { 26 | color: white; 27 | } 28 | 29 | .color { 30 | padding: 1px 6px; 31 | border-radius: 10px; 32 | color: black; 33 | text-align: center; 34 | text-overflow: ellipsis; 35 | white-space: nowrap; 36 | overflow: hidden; 37 | } 38 | .color-0 { 39 | /*Black*/ 40 | background-color: #212121; 41 | color: white; 42 | } 43 | .color-1 { 44 | /*Red*/ 45 | background-color: #fc545b; 46 | } 47 | .color-2 { 48 | /*Green*/ 49 | background-color: #65b84d; 50 | } 51 | .color-3 { 52 | /*Yellow*/ 53 | background-color: #fec52e; 54 | } 55 | .color-4 { 56 | /*Blue*/ 57 | background-color: #157efb; 58 | } 59 | .color-5 { 60 | /*Purple*/ 61 | background-color: #a453a5; 62 | } 63 | .color-6 { 64 | /*Teal*/ 65 | background-color: #00dae3; 66 | } 67 | .color-7 { 68 | /*White*/ 69 | background-color: #e0e0e0; 70 | } 71 | .color-8 { 72 | /*Gray*/ 73 | background-color: #8c8c8c; 74 | } 75 | .color-9 { 76 | /*Red*/ 77 | color: #fc545b; 78 | border: #fc545b 1px solid; 79 | } 80 | .color-10 { 81 | /*Green*/ 82 | color: #65b84d; 83 | border: #65b84d 1px solid; 84 | } 85 | .color-11 { 86 | /*Yellow*/ 87 | color: #fec52e; 88 | border: #fec52e 1px solid; 89 | } 90 | .color-12 { 91 | /*Blue*/ 92 | color: #157efb; 93 | border: #157efb 1px solid; 94 | } 95 | .color-13 { 96 | /*Purple*/ 97 | color: #a453a5; 98 | border: #a453a5 1px solid; 99 | } 100 | .color-14 { 101 | /*Teal*/ 102 | color: #00dae3; 103 | border: #00dae3 1px solid; 104 | } 105 | .color-15 { 106 | /*White*/ 107 | color: #e0e0e0; 108 | border: #e0e0e0 1px solid; 109 | } 110 | 111 | input[type='range'] { 112 | -webkit-appearance: none; 113 | margin: 0px; 114 | width: 100%; 115 | } 116 | input[type='range']::-webkit-slider-thumb { 117 | -webkit-appearance: none; 118 | margin-top: -9px; 119 | height: 26px; 120 | width: 8px; 121 | border-radius: 4px; 122 | background: #929292; 123 | } 124 | input[type='range']::-webkit-slider-runnable-track { 125 | width: 100%; 126 | height: 8px; 127 | background: #3b3b3b; 128 | border-radius: 4px; 129 | } 130 | 131 | .infin { 132 | font-size: 20px; 133 | vertical-align: middle; 134 | } 135 | 136 | .meter { 137 | height: 3px; 138 | background: linear-gradient(90deg, rgba(19, 126, 30, 1) 0%, rgba(255, 238, 30, 1) 85%, rgba(255, 0, 0, 1) 100%); 139 | } 140 | 141 | /* need to find a way to match the color of this with the table row */ 142 | .meter-cover { 143 | height: 100%; 144 | background-color: rgb(22, 23, 25); 145 | float: right; 146 | } 147 | 148 | table.cv-table tr:nth-child(odd) td div.meter div.meter-cover { 149 | background-color: #292929; 150 | } 151 | -------------------------------------------------------------------------------- /plugins/x32/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName %>

3 |

<%= data.info.model %>

4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 35 | 36 | <% for(var i=0; i<32; i++){ %> <% if(data.inputs.channels[i].name == "end"){break;} %> 37 | 38 | 39 | 44 | 45 | 46 | 58 | 59 | <% } %> 60 |
ChannelMutedBFader
LR
<%- data.main.stereo.name%>
M
<%= formatAsDB(data.main.stereo.faderDB) %> 20 | <% let style = `style=width:${Math.abs(data.main.stereo.meter[0] - 10)}%`; %> 21 |
22 |
>
23 |
24 | <% style = `style=width:${Math.abs(data.main.stereo.meter[1] - 10)}%`; %> 25 |
26 |
>
27 |
28 | 34 |
<%= i+1 %> 40 |
41 | <%- data.inputs.channels[i].name || i+1 %> 42 |
43 |
M
<%= formatAsDB(data.inputs.channels[i].faderDB) %> 47 | <% let style = `style=width:${Math.abs(data.inputs.channels[i].meter - 10)}%`; %> 48 |
49 |
>
50 |
51 | 57 |
61 | 62 | <% function formatAsDB(val){ if(val==-90){ return '-'; } if(val>0){ 63 | return "+"+val.toFixed(1); } return val.toFixed(1); } %> 64 | -------------------------------------------------------------------------------- /plugins/xair/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/xair/icon.png -------------------------------------------------------------------------------- /plugins/xair/info.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/plugins/xair/info.html -------------------------------------------------------------------------------- /plugins/xair/main.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | defaultName: 'X Air Mixer', 3 | connectionType: 'UDPsocket', 4 | remotePort: 10024, 5 | mayChangePorts: false, 6 | heartbeatInterval: 9000, 7 | heartbeatTimeout: 15000, 8 | searchOptions: { 9 | type: 'UDPsocket', 10 | searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]), 11 | devicePort: 10024, 12 | listenPort: 0, 13 | validateResponse(msg, info) { 14 | return msg.toString().startWith('/xinfo'); 15 | }, 16 | }, 17 | }; 18 | 19 | exports.ready = function ready(device) { 20 | const d = device; 21 | d.data.channelFaders = new Array(32).fill(0); 22 | d.data.channelFadersDB = new Array(32).fill(0); 23 | d.data.channelMutes = new Array(32).fill(0); 24 | d.data.channelNames = new Array(32).fill('end'); 25 | d.data.channelColors = new Array(32); 26 | 27 | d.data.stereoFader = 0; 28 | d.data.stereoFaderDB = 0; 29 | d.data.stereoMute = 0; 30 | 31 | d.send('/xinfo'); 32 | }; 33 | 34 | function parseAddress(msg) { 35 | const addr = msg.split('/'); 36 | addr.shift(); 37 | return addr; 38 | } 39 | 40 | function convertToDBTheBehringerWay(f) { 41 | if (f >= 0.5) { 42 | return f * 40 - 30; 43 | } 44 | if (f >= 0.25) { 45 | return f * 80 - 50; 46 | } 47 | if (f >= 0.0625) { 48 | return f * 160 - 70; 49 | } 50 | return f * 480 - 90; 51 | } 52 | 53 | exports.data = function data(device, buf) { 54 | this.deviceInfoUpdate(device, 'status', 'ok'); 55 | const msg = buf.toString().split('\x00'); 56 | const d = device; 57 | 58 | if (msg[0] === '/xinfo') { 59 | if (msg[7].length > 0) { 60 | d.data.name = msg[7]; 61 | } else { 62 | d.data.name = msg[6]; 63 | } 64 | 65 | d.data.ip = msg[5]; 66 | d.data.firmware = msg[13]; 67 | d.data.model = msg[9]; 68 | this.deviceInfoUpdate(d, 'defaultName', d.data.name); 69 | 70 | d.send('/lr/mix/fader\x00\x00\x00\x00'); 71 | d.send('/lr/mix/on\x00\x00\x00\x00'); 72 | 73 | for (let i = 0; i <= 32; i++) { 74 | d.send(Buffer.from(`/ch/${i.toString().padStart(2, '0')}/config/name\x00\x00\x00\x00`)); 75 | } 76 | d.draw(); 77 | } else if (msg[0] === '/meters/0') { 78 | // console.log(msg) 79 | } else if (msg[0].includes('/mix/fader')) { 80 | const addr = parseAddress(msg[0]); 81 | const channel = Number(addr[1]); 82 | 83 | if (addr[0] === 'ch') { 84 | d.data.channelFaders[channel - 1] = buf.readFloatBE(24); 85 | d.data.channelFadersDB[channel - 1] = convertToDBTheBehringerWay(buf.readFloatBE(24)); 86 | } else if (addr[0] === 'lr') { 87 | d.data.stereoFader = buf.readFloatBE(20); 88 | d.data.stereoFaderDB = convertToDBTheBehringerWay(buf.readFloatBE(20)); 89 | } 90 | 91 | d.draw(); 92 | } else if (msg[0].includes('/mix/on')) { 93 | const addr = parseAddress(msg[0]); 94 | const channel = Number(addr[1]); 95 | 96 | if (addr[0] === 'ch') { 97 | d.data.channelMutes[channel - 1] = buf[23]; 98 | } else if (addr[0] === 'lr') { 99 | d.data.stereoMute = buf[19]; 100 | } 101 | device.draw(); 102 | device.send(`/ch/${addr[1]}/mix/fader\x00\x00\x00\x00`); 103 | } else if (msg[0].includes('/config/name')) { 104 | const addr = parseAddress(msg[0]); 105 | const channel = Number(addr[1]); 106 | d.data.channelNames[channel - 1] = msg[4]; 107 | d.draw(); 108 | d.send(`/ch/${addr[1]}/config/color\x00\x00\x00\x00`); 109 | } else if (msg[0].includes('/config/color')) { 110 | const addr = parseAddress(msg[0]); 111 | const channel = Number(addr[1]); 112 | d.data.channelColors[channel - 1] = buf.readInt8(27); 113 | d.draw(); 114 | d.send(`/ch/${addr[1]}/mix/on\x00\x00\x00\x00`); 115 | } else { 116 | // console.log(msg) 117 | } 118 | // console.log(msg) 119 | }; 120 | 121 | exports.heartbeat = function heartbeat(device) { 122 | device.send('/xremote'); 123 | }; 124 | -------------------------------------------------------------------------------- /plugins/xair/styles.css: -------------------------------------------------------------------------------- 1 | td { 2 | padding: 3px 16px !important; 3 | } 4 | tr td:first-child { 5 | text-align: center; 6 | } 7 | 8 | .mute-0, 9 | .mute-1 { 10 | width: 30px; 11 | padding: 4px; 12 | font-size: 14px; 13 | border: black 2px solid; 14 | text-align: center; 15 | border-radius: 10px; 16 | } 17 | .mute-1 { 18 | border-color: gray; 19 | color: gray; 20 | } 21 | .mute-0 { 22 | border-color: #fc3344; 23 | color: #fc3344; 24 | } 25 | .white { 26 | color: white; 27 | } 28 | 29 | .color { 30 | padding: 1px 6px; 31 | border-radius: 10px; 32 | color: black; 33 | text-align: center; 34 | text-overflow: ellipsis; 35 | white-space: nowrap; 36 | overflow: hidden; 37 | } 38 | .color-0 { 39 | /*Black*/ 40 | background-color: #212121; 41 | color: white; 42 | } 43 | .color-1 { 44 | /*Red*/ 45 | background-color: #fc545b; 46 | } 47 | .color-2 { 48 | /*Green*/ 49 | background-color: #65b84d; 50 | } 51 | .color-3 { 52 | /*Yellow*/ 53 | background-color: #fec52e; 54 | } 55 | .color-4 { 56 | /*Blue*/ 57 | background-color: #157efb; 58 | } 59 | .color-5 { 60 | /*Purple*/ 61 | background-color: #a453a5; 62 | } 63 | .color-6 { 64 | /*Teal*/ 65 | background-color: #00dae3; 66 | } 67 | .color-7 { 68 | /*White*/ 69 | background-color: #e0e0e0; 70 | } 71 | .color-8 { 72 | /*Gray*/ 73 | background-color: #8c8c8c; 74 | } 75 | .color-9 { 76 | /*Red*/ 77 | color: #fc545b; 78 | border: #fc545b 1px solid; 79 | } 80 | .color-10 { 81 | /*Green*/ 82 | color: #65b84d; 83 | border: #65b84d 1px solid; 84 | } 85 | .color-11 { 86 | /*Yellow*/ 87 | color: #fec52e; 88 | border: #fec52e 1px solid; 89 | } 90 | .color-12 { 91 | /*Blue*/ 92 | color: #157efb; 93 | border: #157efb 1px solid; 94 | } 95 | .color-13 { 96 | /*Purple*/ 97 | color: #a453a5; 98 | border: #a453a5 1px solid; 99 | } 100 | .color-14 { 101 | /*Teal*/ 102 | color: #00dae3; 103 | border: #00dae3 1px solid; 104 | } 105 | .color-15 { 106 | /*White*/ 107 | color: #e0e0e0; 108 | border: #e0e0e0 1px solid; 109 | } 110 | 111 | input[type='range'] { 112 | -webkit-appearance: none; 113 | margin: 0px; 114 | width: 100%; 115 | } 116 | input[type='range']::-webkit-slider-thumb { 117 | -webkit-appearance: none; 118 | margin-top: -9px; 119 | height: 26px; 120 | width: 8px; 121 | border-radius: 4px; 122 | background: #929292; 123 | } 124 | input[type='range']::-webkit-slider-runnable-track { 125 | width: 100%; 126 | height: 8px; 127 | background: #3b3b3b; 128 | border-radius: 4px; 129 | } 130 | 131 | .infin { 132 | font-size: 20px; 133 | vertical-align: middle; 134 | } 135 | -------------------------------------------------------------------------------- /plugins/xair/template.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%= listName %>

3 |

<%= data.model %>

4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 27 | 28 | <% for(var i=0; i<32; i++){ %> <% if(data.channelNames[i]=="end"){break;} %> 29 | 30 | 31 | 36 | 37 | 38 | 46 | 47 | <% } %> 48 |
ChannelMutedBFader
LR
MAIN OUT
M
<%= formatAsDB(data.stereoFaderDB) %> 20 | 26 |
<%= i+1 %> 32 |
33 | <%- data.channelNames[i] || i+1 %> 34 |
35 |
M
<%= formatAsDB(data.channelFadersDB[i]) %> 39 | 45 |
49 | 50 | <% function formatAsDB(val){ if(val==-90){ return '-'; } if(val>0){ 51 | return "+"+val.toFixed(1); } return val.toFixed(1); } %> 52 | -------------------------------------------------------------------------------- /preload.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | 3 | const DEVICE = require('./src/device.js'); 4 | const PLUGINS = require('./src/plugins.js'); 5 | const SEARCH = require('./src/search.js'); 6 | const VIEW = require('./src/view.js'); 7 | const SAVESLOTS = require('./src/saveSlots.js'); 8 | 9 | window.addDevice = DEVICE.addDevice; 10 | window.searchAll = SEARCH.searchAll; 11 | 12 | window.init = function init() { 13 | console.log('init!'); 14 | 15 | ipcRenderer.send('enableDeviceDropdown'); 16 | ipcRenderer.send('enableSearchAll'); 17 | 18 | // load autoUpdate setting from storage and send to main process 19 | const autoUpdate = JSON.parse(localStorage.getItem('autoUpdate')); 20 | if (autoUpdate !== undefined && autoUpdate !== null) { 21 | if (autoUpdate) { 22 | ipcRenderer.send('checkForUpdates'); 23 | } 24 | // send message so main process knows the state of autoUpdate 25 | ipcRenderer.send('setAutoUpdate', autoUpdate); 26 | } 27 | 28 | PLUGINS.init(() => { 29 | VIEW.init(); 30 | SAVESLOTS.loadDevices(); 31 | SAVESLOTS.loadSlot(1); 32 | }); 33 | 34 | document.getElementById('search-button').onclick = (e) => { 35 | SEARCH.searchAll(); 36 | }; 37 | 38 | document.getElementById('device-settings-table').onclick = function settingsClick(e) { 39 | e.stopPropagation(); 40 | }; 41 | 42 | document.getElementById('device-settings-name').onchange = function nameChange(e) { 43 | e.stopPropagation(); 44 | DEVICE.changeActiveName(e.target.value); 45 | }; 46 | 47 | document.getElementById('device-settings-plugin-dropdown').onchange = function dropdownChange(e) { 48 | e.stopPropagation(); 49 | DEVICE.changeActiveType(e.target.value); 50 | }; 51 | 52 | document.getElementById('device-settings-ip').onchange = function ipChange(e) { 53 | e.stopPropagation(); 54 | DEVICE.changeActiveIP(e.target.value); 55 | }; 56 | 57 | document.getElementById('device-settings-port').onchange = function portChange(e) { 58 | e.stopPropagation(); 59 | DEVICE.changeActivePort(e.target.value); 60 | }; 61 | 62 | document.getElementById('device-settings-rx-port').onchange = function portChange(e) { 63 | e.stopPropagation(); 64 | DEVICE.changeActiveRxPort(e.target.value); 65 | }; 66 | 67 | document.getElementById('device-settings-pin').onchange = function pinChange(e) { 68 | e.stopPropagation(); 69 | if (e.target.checked) { 70 | VIEW.pinActiveDevice(); 71 | } else { 72 | VIEW.unpinActiveDevice(); 73 | } 74 | }; 75 | 76 | const saveSlots = document.getElementsByClassName('save-slot'); 77 | 78 | for (let i = 0; i < saveSlots.length; i++) { 79 | const saveSlot = saveSlots[i]; 80 | saveSlot.addEventListener('click', (event) => { 81 | // get save slot from button id save-slot-1 = 1 82 | const saveSlotIndex = parseInt(event.target.id.replace('save-slot-', ''), 10); 83 | if (saveSlotIndex) { 84 | SAVESLOTS.loadSlot(saveSlotIndex); 85 | } 86 | }); 87 | } 88 | 89 | document.getElementById('refresh-device-button').onclick = function refreshClick(e) { 90 | e.stopPropagation(); 91 | DEVICE.refreshActive(); 92 | }; 93 | 94 | document.getElementById('device-list').onclick = function listClick(e) { 95 | e.stopPropagation(); 96 | const deviceID = e.srcElement.id; 97 | if (e.srcElement.id !== 'device-list') { 98 | VIEW.switchDevice(deviceID); 99 | } else { 100 | VIEW.switchDevice(undefined); 101 | } 102 | }; 103 | 104 | document.getElementById('add-device-button').onchange = function addDeviceClick(e) { 105 | const newDevice = DEVICE.registerDevice( 106 | { 107 | type: e.target.value, 108 | defaultName: 'New Device', 109 | remotePort: PLUGINS.all[e.target.value].config.remotePort || '', 110 | addresses: [], 111 | }, 112 | 'fromAddButton' 113 | ); 114 | e.target.selectedIndex = 0; 115 | 116 | VIEW.switchDevice(newDevice.id); 117 | SAVESLOTS.saveAll(); 118 | }; 119 | 120 | document.getElementById('network-info-button').onclick = function fooBar(e) { 121 | ipcRenderer.send('openNetworkInfoWindow'); 122 | }; 123 | 124 | document.onkeyup = function keyUp(e) { 125 | if (e.key === 'ArrowUp') { 126 | VIEW.selectPreviousDevice(); 127 | } else if (e.key === 'ArrowDown') { 128 | VIEW.selectNextDevice(); 129 | } else if (e.key === 'Tab') { 130 | if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'SELECT') { 131 | document.getElementById('device-settings-name').select(); 132 | } 133 | } 134 | }; 135 | }; 136 | 137 | ipcRenderer.on('setActiveDevicePinned', (event, message) => { 138 | if (!message) { 139 | VIEW.unpinActiveDevice(); 140 | } else { 141 | VIEW.pinActiveDevice(); 142 | } 143 | }); 144 | 145 | ipcRenderer.on('searchAll', (event, message) => { 146 | window.searchAll(); 147 | }); 148 | 149 | ipcRenderer.on('deleteActive', (event, message) => { 150 | DEVICE.deleteActive(); 151 | VIEW.selectPreviousDevice(); 152 | }); 153 | 154 | ipcRenderer.on('clearSavedData', (event, message) => { 155 | SAVESLOTS.clearSavedData(); 156 | }); 157 | 158 | ipcRenderer.on('toggleSidebar', (event, message) => { 159 | document.getElementById('main').classList.toggle('sidebar-hidden'); 160 | }); 161 | 162 | ipcRenderer.on('loadSlot', (event, slot) => { 163 | if (slot) { 164 | SAVESLOTS.loadSlot(slot); 165 | } 166 | }); 167 | 168 | // message from main process to set autoUpdate state 169 | ipcRenderer.on('setAutoUpdate', (event, autoUpdate) => { 170 | localStorage.setItem('autoUpdate', autoUpdate); 171 | if (autoUpdate) { 172 | ipcRenderer.send('checkForUpdates'); 173 | } 174 | // message to main process that we have updated the state 175 | ipcRenderer.send('setAutoUpdate', autoUpdate); 176 | }); 177 | 178 | function switchClass(element, className) { 179 | try { 180 | document.getElementsByClassName(className)[0].classList.remove(className); 181 | } catch (err) { 182 | // console.log(err) 183 | } 184 | element.classList.add(className); 185 | } 186 | window.switchClass = switchClass; 187 | -------------------------------------------------------------------------------- /src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 1rem; 3 | font-family: 4 | ui-sans-serif, 5 | system-ui, 6 | -apple-system, 7 | BlinkMacSystemFont; 8 | } 9 | *, 10 | a, 11 | button { 12 | cursor: default; 13 | user-select: none; 14 | } 15 | 16 | body { 17 | color: #6e6c6f; 18 | margin: 0px; 19 | user-select: none; 20 | overflow-x: hidden; 21 | } 22 | a { 23 | text-decoration: none; 24 | } 25 | .red { 26 | color: #eb6b5e; 27 | } 28 | .green { 29 | color: #61c650; 30 | } 31 | .left { 32 | float: left; 33 | } 34 | .right { 35 | float: right; 36 | } 37 | 38 | #main.sidebar-hidden { 39 | grid-template-columns: auto; 40 | } 41 | 42 | #main.sidebar-hidden > #device-list-col { 43 | display: none; 44 | } 45 | 46 | #main { 47 | box-sizing: border-box; 48 | display: grid; 49 | width: 100%; 50 | height: 100vh; 51 | grid-template-columns: 270px 1fr; 52 | } 53 | .col { 54 | border-right: rgba(0, 0, 0, 0.5) 1px solid; 55 | height: 100vh !important; 56 | overflow: hidden; 57 | box-sizing: border-box; 58 | position: relative; 59 | } 60 | 61 | /* FIRST COL */ 62 | #device-list-col { 63 | background-color: #2b2b2b; 64 | color: #707070; 65 | } 66 | #view-buttons-bar { 67 | box-sizing: border-box; 68 | text-align: right; 69 | padding-left: 100px; 70 | padding-right: 7px; 71 | -webkit-app-region: drag; 72 | z-index: 1000; 73 | } 74 | #view-buttons-bar .active { 75 | background-color: rgba(255, 255, 255, 0.3); 76 | color: white; 77 | border: #61c650; 78 | border-style: solid; 79 | } 80 | #view-buttons-bar button { 81 | background-color: rgba(255, 255, 255, 0.07); 82 | width: 26px; 83 | border-radius: 10px; 84 | font-size: 0.8rem; 85 | } 86 | #view-buttons-bar button:hover { 87 | background-color: rgba(255, 255, 255, 0.25); 88 | } 89 | 90 | #device-list { 91 | box-sizing: border-box; 92 | width: 100%; 93 | height: calc(100% - 387px); 94 | padding: 0px 10px; 95 | overflow-x: hidden; 96 | overflow-y: scroll; 97 | } 98 | #device-list div { 99 | pointer-events: none; 100 | } 101 | #device-list .device { 102 | box-sizing: border-box; 103 | clear: both; 104 | display: block; 105 | width: 100%; 106 | height: 40px; 107 | padding: 10px 5px; 108 | overflow: hidden; 109 | border-radius: 5px; 110 | } 111 | #device-list .device.active-device { 112 | /*background-color: #0e5ccd;*/ 113 | /*background-color: #c9961f;*/ 114 | background-color: rgba(255, 255, 255, 0.2); 115 | } 116 | #device-list .status { 117 | float: left; 118 | width: 25px; 119 | font-size: 18px; 120 | } 121 | #device-list .type { 122 | float: left; 123 | width: 30px; 124 | color: #f6bd26; 125 | } 126 | #device-list .name { 127 | position: relative; 128 | left: 0px; 129 | color: white; 130 | font-weight: 600; 131 | 132 | text-overflow: ellipsis; 133 | white-space: nowrap; 134 | overflow: hidden; 135 | } 136 | #device-list h3.init { 137 | margin: 0px auto; 138 | padding-top: 200px; 139 | text-align: center; 140 | font-weight: 300; 141 | font-size: 16px; 142 | width: 210px; 143 | pointer-events: none; 144 | } 145 | #device-inspector { 146 | position: absolute; 147 | bottom: 0px; 148 | width: 100%; 149 | } 150 | #device-tools { 151 | /* position: absolute; 152 | bottom: 210px; 153 | left: 0px;*/ 154 | width: 262px; 155 | height: 30px; 156 | padding-right: 10px; 157 | /*background: linear-gradient(#2f2f2f 0%, #2c2c2c 100%);*/ 158 | background-color: rgba(0, 0, 0, 0.2); 159 | text-align: center; 160 | border-top: rgba(0, 0, 0, 0.3) 1px solid; 161 | /*border-bottom: #3a3a3a 1px solid;*/ 162 | } 163 | #device-tools select { 164 | text-align: center; 165 | } 166 | 167 | #device-settings { 168 | height: 300px; 169 | padding: 5px; 170 | color: white; 171 | font-weight: 300; 172 | width: 100%; 173 | background-color: rgba(0, 0, 0, 0.2); 174 | } 175 | #device-settings th { 176 | text-align: right; 177 | padding-right: 3px; 178 | color: white; 179 | font-weight: normal; 180 | } 181 | #device-settings td { 182 | padding: 6px; 183 | } 184 | #device-settings-table { 185 | display: none; 186 | } 187 | #device-settings h3 { 188 | padding-top: 90px; 189 | text-align: center; 190 | font-weight: 300; 191 | } 192 | #device-settings #device-settings-name, 193 | #device-settings #device-settings-ip, 194 | #device-settings #device-settings-plugin-dropdown { 195 | width: 170px; 196 | } 197 | #device-settings #device-settings-port, 198 | #device-settings #device-settings-rx-port { 199 | width: 70px; 200 | } 201 | #network-indicator-dot { 202 | position: absolute; 203 | left: 250px; 204 | bottom: 8px; 205 | background: #00ff00; 206 | width: 8px; 207 | height: 8px; 208 | border-radius: 4px; 209 | } 210 | 211 | /* SECOND COL */ 212 | #all-devices { 213 | display: grid; 214 | grid-template-columns: repeat(4, 1fr); 215 | background-color: rgba(0, 0, 0, 0.3); 216 | } 217 | .device-pin { 218 | position: absolute; 219 | right: 45px; 220 | top: 4px; 221 | height: 20px; 222 | padding: inherit; 223 | } 224 | .device-traffic-signal { 225 | position: absolute; 226 | right: 15px; 227 | top: 4px; 228 | height: 20px; 229 | padding: inherit; 230 | opacity: 0.3; 231 | transition: opacity 0.1s ease-in-out; 232 | } 233 | .device-wrapper { 234 | box-sizing: border-box; 235 | border: black 1px solid; 236 | padding: 2px; 237 | } 238 | .draw-area { 239 | height: 100%; 240 | overflow-y: scroll; 241 | width: 100%; 242 | height: 100%; 243 | border: 0; 244 | } 245 | .active-device-outline { 246 | padding: 0px; 247 | border: #fdea08 3px solid; 248 | } 249 | 250 | input { 251 | display: block; 252 | box-sizing: content-box; 253 | background-color: rgba(0, 0, 0, 0.2); 254 | height: 28px; 255 | padding: 2px 6px; 256 | margin: 0px; 257 | border: rgba(255, 255, 255, 0.1) 1px solid; 258 | border-radius: 4px; 259 | color: #fff; 260 | font-size: 16px; 261 | font-weight: 500; 262 | cursor: text; 263 | } 264 | select { 265 | display: block; 266 | box-sizing: content-box; 267 | background-color: rgba(255, 255, 255, 0.15); 268 | height: 28px; 269 | padding: 2px 6px; 270 | margin: 0px; 271 | border: rgba(255, 255, 255, 0.1) 1px solid; 272 | border-radius: 4px; 273 | color: #fff; 274 | font-size: 16px; 275 | font-weight: 500; 276 | } 277 | select option { 278 | /* these styles don't affect macOS but are important for Windows! */ 279 | text-align: left; 280 | background-color: #333333; 281 | } 282 | input:focus, 283 | select:focus { 284 | outline: #fdea08 2px solid; 285 | color: white; 286 | } 287 | input[type='checkbox'] { 288 | position: relative; 289 | height: 14px; 290 | width: 14px; 291 | padding: 2px; 292 | -webkit-appearance: none; 293 | cursor: default; 294 | } 295 | input[type='checkbox']:checked { 296 | background-color: #616064; 297 | } 298 | input[type='checkbox']:checked:after { 299 | content: '\2713'; 300 | font-size: 18px; 301 | position: absolute; 302 | top: -1px; 303 | left: 1px; 304 | color: white; 305 | } 306 | input::-webkit-outer-spin-button, 307 | input::-webkit-inner-spin-button { 308 | -webkit-appearance: none; 309 | margin: 0; 310 | } 311 | input:disabled { 312 | color: gray; 313 | cursor: not-allowed; 314 | } 315 | button, 316 | select.button { 317 | box-sizing: content-box; 318 | width: 40px; 319 | height: 25px; 320 | margin-top: 7px; 321 | margin-bottom: 7px; 322 | margin-left: 4px; 323 | border: none; 324 | outline: none; 325 | padding: 0px; 326 | 327 | color: white; 328 | background: rgba(255, 255, 255, 0.15); 329 | border-radius: 4px; 330 | font-size: 1rem; 331 | user-select: none; 332 | } 333 | button:hover { 334 | background-color: rgba(255, 255, 255, 0.2); 335 | } 336 | button:focus { 337 | outline: none; 338 | } 339 | button:disabled { 340 | background: rgba(255, 255, 255, 0.03); 341 | } 342 | button img { 343 | height: 18px; 344 | } 345 | 346 | select.button:focus { 347 | outline: none; 348 | } 349 | 350 | @font-face { 351 | font-family: 'Material Icons'; 352 | font-style: normal; 353 | font-weight: 400; 354 | src: 355 | local('Material Icons'), 356 | local('MaterialIcons-Regular'), 357 | url(../font/MaterialIcons-Regular.ttf) format('truetype'); 358 | } 359 | .material-icons { 360 | font-family: 'Material Icons'; 361 | font-weight: normal; 362 | font-style: normal; 363 | font-size: 24px; /* Preferred icon size */ 364 | display: inline-block; 365 | line-height: 1; 366 | text-transform: none; 367 | letter-spacing: normal; 368 | word-wrap: normal; 369 | white-space: nowrap; 370 | direction: ltr; 371 | 372 | /* Support for all WebKit browsers. */ 373 | -webkit-font-smoothing: antialiased; 374 | /* Support for Safari and Chrome. */ 375 | text-rendering: optimizeLegibility; 376 | 377 | /* Support for Firefox. */ 378 | -moz-osx-font-smoothing: grayscale; 379 | 380 | /* Support for IE. */ 381 | font-feature-settings: 'liga'; 382 | } 383 | 384 | /* only allow select on input fields */ 385 | body :not(input):not(select):not(textarea) { 386 | user-select: none; 387 | } 388 | 389 | ::-webkit-scrollbar { 390 | /* background-color: black; */ 391 | width: 12px; 392 | } 393 | ::-webkit-scrollbar-track, 394 | ::-webkit-scrollbar-corner { 395 | background-color: rgba(0, 0, 0, 0.1); 396 | } 397 | ::-webkit-scrollbar-thumb { 398 | background-color: #6b6b6b; 399 | border-radius: 16px; 400 | border: 3px solid #2b2b2b; 401 | } 402 | ::-webkit-scrollbar-button { 403 | display: none; 404 | } 405 | -------------------------------------------------------------------------------- /src/assets/css/plugin_default.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui; 3 | margin: 0px; 4 | margin-top: 40px; 5 | user-select: none; 6 | visibility: visible !important; 7 | max-width: 100%; 8 | padding: 10px; 9 | } 10 | header { 11 | position: fixed; 12 | top: 0px; 13 | left: 0px; 14 | height: 33px; 15 | width: 100%; 16 | box-sizing: border-box; 17 | padding-right: 34px; 18 | overflow: hidden; 19 | background-color: #111111; 20 | border-bottom: black 1px solid; 21 | -webkit-user-select: none; 22 | -webkit-app-region: drag; 23 | } 24 | h1, 25 | h2, 26 | h3, 27 | h4, 28 | h5, 29 | h6 { 30 | color: white; 31 | } 32 | 33 | header h1, 34 | header h2 { 35 | margin: 4px; 36 | margin-left: 20px; 37 | font-weight: normal; 38 | font-size: 18px; 39 | float: left; 40 | height: 24px; 41 | overflow: hidden; 42 | } 43 | header h2 { 44 | color: #b6b6b6; 45 | } 46 | h3 { 47 | font-weight: 300; 48 | } 49 | table.cv-table { 50 | border: none; 51 | table-layout: fixed; 52 | border-collapse: collapse; 53 | color: #dddddd; 54 | font-size: 15px; 55 | } 56 | table.cv-table th { 57 | padding: 6px 16px; 58 | color: #969696; 59 | text-align: left; 60 | border-bottom: #555 1px solid; 61 | border-bottom: #555 1px solid; 62 | font-weight: normal; 63 | background-color: #1e1e1e; 64 | } 65 | table.cv-table tr:nth-child(odd) { 66 | background-color: #292929; 67 | } 68 | table.cv-table tr td:first-child { 69 | padding-left: 10px; 70 | border-top-left-radius: 10px; 71 | border-bottom-left-radius: 10px; 72 | } 73 | table.cv-table tr td:last-child { 74 | padding-right: 10px; 75 | border-top-right-radius: 10px; 76 | border-bottom-right-radius: 10px; 77 | } 78 | table.cv-table td { 79 | padding: 7px 16px; 80 | } 81 | 82 | .not-responding { 83 | color: white; 84 | } 85 | .not-responding em { 86 | background-color: #3f3f3f; 87 | padding: 2px 10px; 88 | display: inline-block; 89 | border-radius: 5px; 90 | } 91 | .not-responding hr { 92 | margin-top: 100px; 93 | border-color: #333; 94 | } 95 | 96 | .device-info { 97 | color: #aaa; 98 | } 99 | .device-info h4, 100 | .device-info h2 { 101 | color: #bbb; 102 | } 103 | .device-info em { 104 | background-color: #3f3f3f; 105 | padding: 2px 5px; 106 | display: inline-block; 107 | border-radius: 5px; 108 | color: #fdea08; 109 | margin: 1px; 110 | } 111 | button { 112 | display: block; 113 | box-sizing: content-box; 114 | background-color: rgba(255, 255, 255, 0.15); 115 | height: 28px; 116 | padding: 2px 20px; 117 | margin: 0px; 118 | border: rgba(255, 255, 255, 0.1) 1px solid; 119 | border-radius: 4px; 120 | color: #fff; 121 | font-size: 16px; 122 | font-weight: 500; 123 | } 124 | -------------------------------------------------------------------------------- /src/assets/font/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/font/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /src/assets/img/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/icon.icns -------------------------------------------------------------------------------- /src/assets/img/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/icon.ico -------------------------------------------------------------------------------- /src/assets/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/icon.png -------------------------------------------------------------------------------- /src/assets/img/outline_add_box_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_add_box_white_18dp.png -------------------------------------------------------------------------------- /src/assets/img/outline_add_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_add_white_18dp.png -------------------------------------------------------------------------------- /src/assets/img/outline_broken_image_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_broken_image_white_18dp.png -------------------------------------------------------------------------------- /src/assets/img/outline_clear_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_clear_white_18dp.png -------------------------------------------------------------------------------- /src/assets/img/outline_done_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_done_white_18dp.png -------------------------------------------------------------------------------- /src/assets/img/outline_info_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_info_white_18dp.png -------------------------------------------------------------------------------- /src/assets/img/outline_link_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_link_white_18dp.png -------------------------------------------------------------------------------- /src/assets/img/outline_push_pin_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_push_pin_white_18dp.png -------------------------------------------------------------------------------- /src/assets/img/outline_refresh_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_refresh_white_18dp.png -------------------------------------------------------------------------------- /src/assets/img/outline_search_white_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stagehacks/Cue-View/d26de5448d26d81cf3b43a89088054a2f4ff2672/src/assets/img/outline_search_white_18dp.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | window.init(); 2 | -------------------------------------------------------------------------------- /src/plugins.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | /* eslint-disable import/no-dynamic-require */ 3 | const fs = require('fs'); 4 | const _ = require('lodash'); 5 | const path = require('path'); 6 | 7 | const DEVICE = require('./device.js'); 8 | const VIEW = require('./view.js'); 9 | 10 | const allPlugins = {}; 11 | module.exports.all = allPlugins; 12 | 13 | module.exports.init = function init(callback) { 14 | const pluginDirectoryPath = path.normalize(path.join(__dirname, `../plugins`)); 15 | 16 | console.log(`Loading plugin files... ${pluginDirectoryPath}`); 17 | 18 | fs.readdir(pluginDirectoryPath, (err, files) => { 19 | files.forEach((pluginDir) => { 20 | if (pluginDir[0] !== '.') { 21 | allPlugins[pluginDir] = require(path.join(pluginDirectoryPath, `/${pluginDir}/main.js`)); 22 | 23 | const plugin = allPlugins[pluginDir]; 24 | 25 | plugin.deviceInfoUpdate = function deviceInfoUpdate(device, param, value) { 26 | DEVICE.infoUpdate(device, param, value); 27 | }; 28 | plugin.draw = (device) => { 29 | VIEW.draw(device); 30 | }; 31 | 32 | plugin.template = _.template( 33 | fs.readFileSync(path.join(pluginDirectoryPath, `/${pluginDir}/template.ejs`), 'utf8') 34 | ); 35 | 36 | plugin.info = _.template(fs.readFileSync(path.join(pluginDirectoryPath, `/${pluginDir}/info.html`), 'utf8')); 37 | 38 | plugin.heartbeatTimeout = plugin.config.heartbeatTimeout; 39 | plugin.heartbeatInterval = plugin.config.heartbeatInterval; 40 | 41 | console.log(`${pluginDir} loaded`); 42 | } 43 | }); 44 | 45 | callback(); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/saveSlots.js: -------------------------------------------------------------------------------- 1 | const VIEW = require('./view.js'); 2 | const DEVICE = require('./device.js'); 3 | 4 | let activeSlot = false; 5 | let savedSlots = [[], [], [], []]; 6 | let savedDevices = []; 7 | 8 | const storedSlots = localStorage.getItem('savedSlots'); 9 | if (storedSlots) { 10 | savedSlots = JSON.parse(storedSlots); 11 | // can probably be removed but I've noticed some older versions left some [null] slots in the savedSlots 12 | savedSlots = savedSlots.map((savedSlot) => { 13 | if (savedSlot.length === 1 && savedSlot[0] === null) { 14 | return []; 15 | } 16 | return savedSlot; 17 | }); 18 | } 19 | 20 | const storedDevices = localStorage.getItem('savedDevices'); 21 | if (storedDevices) { 22 | savedDevices = JSON.parse(storedDevices); 23 | } 24 | 25 | function loadSlot(slotIndex) { 26 | VIEW.toggleSlotButtons(slotIndex); 27 | activeSlot = slotIndex; 28 | 29 | Object.keys(DEVICE.all).forEach((d) => { 30 | DEVICE.changePinIndex(DEVICE.all[d], false); 31 | }); 32 | VIEW.resetPinned(); 33 | 34 | Object.keys(savedSlots[slotIndex]).forEach((slot) => { 35 | const savedDevice = savedSlots[slotIndex][slot]; 36 | 37 | Object.keys(DEVICE.all).forEach((d) => { 38 | const device = DEVICE.all[d]; 39 | 40 | if (device.id === savedDevice.id) { 41 | VIEW.pinDevice(device); 42 | VIEW.switchDevice(device.id); 43 | } else if ( 44 | device.addresses[0] === savedDevice.addresses[0] && 45 | device.type === savedDevice.type && 46 | savedDevice.addresses[0] !== undefined 47 | ) { 48 | VIEW.pinDevice(device); 49 | VIEW.switchDevice(device.id); 50 | } 51 | }); 52 | }); 53 | } 54 | module.exports.loadSlot = loadSlot; 55 | 56 | module.exports.loadDevices = function loadDevices() { 57 | console.log(`Loading ${savedDevices.length} saved devices...`); 58 | 59 | for (let i = 0; i < savedDevices.length; i++) { 60 | DEVICE.registerDevice( 61 | { 62 | type: savedDevices[i].type, 63 | displayName: savedDevices[i].displayName, 64 | defaultName: savedDevices[i].defaultName, 65 | remotePort: savedDevices[i].remotePort, 66 | localPort: savedDevices[i].localPort, 67 | addresses: savedDevices[i].addresses, 68 | id: savedDevices[i].id, 69 | fields: savedDevices[i].fields, 70 | }, 71 | 'fromSave' 72 | ); 73 | } 74 | }; 75 | 76 | module.exports.saveAll = function saveAll() { 77 | console.log('Saving...'); 78 | const currentPins = VIEW.getPinnedDevices(); 79 | 80 | savedSlots[activeSlot] = []; 81 | for (let i = 0; i < currentPins.length; i++) { 82 | if (currentPins[i]) { 83 | // only include saved devices 84 | if (currentPins[i].id in DEVICE.all) { 85 | savedSlots[activeSlot][i] = { 86 | addresses: currentPins[i].addresses, 87 | type: currentPins[i].type, 88 | id: currentPins[i].id, 89 | }; 90 | } 91 | } 92 | } 93 | 94 | // can probably be removed but I've noticed some older versions left some [null] slots in the savedSlots 95 | savedSlots = savedSlots.map((savedSlot) => { 96 | if (savedSlot.length === 1 && savedSlot[0] === null) { 97 | return []; 98 | } 99 | return savedSlot; 100 | }); 101 | 102 | localStorage.setItem('savedSlots', JSON.stringify(savedSlots)); 103 | console.log(`Saved ${currentPins.length} pinned devices to slot ${activeSlot}!`); 104 | 105 | savedDevices = []; 106 | let i = 0; 107 | Object.keys(DEVICE.all).forEach((d) => { 108 | const device = DEVICE.all[d]; 109 | savedDevices[i] = { 110 | addresses: device.addresses, 111 | type: device.type, 112 | displayName: device.displayName, 113 | defaultName: device.defaultName, 114 | remotePort: device.remotePort, 115 | localPort: device.localPort, 116 | id: device.id, 117 | fields: device.fields, 118 | }; 119 | i++; 120 | }); 121 | 122 | localStorage.setItem('savedDevices', JSON.stringify(savedDevices)); 123 | console.log(`Saved ${savedDevices.length} devices to storage!`); 124 | }; 125 | 126 | module.exports.removeDevice = function removeDevice(_device) { 127 | // remove devices from local savedDevices 128 | savedDevices = savedDevices.filter((device) => device.id !== _device.id); 129 | 130 | // remove devices from all saved slots 131 | for (let i = 1; i < savedSlots.length; i++) { 132 | savedSlots[i] = savedSlots[i].filter((device) => device.id !== _device.id); 133 | } 134 | 135 | // things might have changed so run a save 136 | this.saveAll(); 137 | }; 138 | 139 | module.exports.reloadActiveSlot = function reloadActiveSlot() { 140 | loadSlot(activeSlot); 141 | }; 142 | 143 | module.exports.clearSavedData = function clearSavedData() { 144 | localStorage.removeItem('savedSlots'); 145 | localStorage.removeItem('savedDevices'); 146 | }; 147 | -------------------------------------------------------------------------------- /src/search.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | const dgram = require('dgram'); 3 | const bonjour = require('bonjour')(); 4 | const net = require('net'); 5 | const os = require('os'); 6 | const ip = require('ip'); 7 | 8 | const { Netmask } = require('netmask'); 9 | const DEVICE = require('./device.js'); 10 | const PLUGINS = require('./plugins.js'); 11 | 12 | let searching = false; 13 | let allServers = false; 14 | let validInterfaces = {}; 15 | 16 | function getServers() { 17 | const interfaces = os.networkInterfaces(); 18 | const result = []; 19 | validInterfaces = {}; 20 | 21 | Object.keys(interfaces).forEach((key) => { 22 | const addresses = interfaces[key]; 23 | 24 | for (let i = addresses.length; i--; ) { 25 | const address = addresses[i]; 26 | if (address.family === 'IPv4' && !address.internal && address.address.substring(0, 3) !== '169') { 27 | let subnet = ip.subnet(address.address, address.netmask); 28 | let current = ip.toLong(subnet.firstAddress); 29 | let last = ip.toLong(subnet.lastAddress) - 1; 30 | address.searchTruncated = false; 31 | 32 | if (last - current > 2296) { 33 | subnet = ip.subnet(address.address, '255.255.248.0'); 34 | last = ip.toLong(subnet.lastAddress) - 1; 35 | address.searchTruncated = true; 36 | } 37 | 38 | // console.log(`range ${subnet.firstAddress} - ${subnet.lastAddress}`); 39 | 40 | address.broadcastAddress = subnet.broadcastAddress; 41 | address.firstSearchAddress = subnet.firstAddress; 42 | address.lastSearchAddress = subnet.lastAddress; 43 | 44 | if (!validInterfaces[key]) { 45 | validInterfaces[key] = []; 46 | } 47 | validInterfaces[key].push(address); 48 | 49 | while (current++ < last) result.push(ip.fromLong(current)); 50 | } 51 | } 52 | }); 53 | return result; 54 | } 55 | function getNetworkInterfaces() { 56 | getServers(); 57 | return validInterfaces; 58 | } 59 | module.exports.getNetworkInterfaces = getNetworkInterfaces; 60 | 61 | const searchSockets = []; 62 | function searchAll() { 63 | if (searching) { 64 | return; 65 | } 66 | searching = true; 67 | ipcRenderer.send('disableSearchAll', ''); 68 | document.getElementById('search-button').style.opacity = 0.2; 69 | console.log('Searching...'); 70 | allServers = getServers(); 71 | let TCPFlag = true; 72 | if (allServers.length > 2296) { 73 | alert( 74 | 'Unable to search for TCP devices - subnet too large!\n\nCue View requires subnet 255.255.248.0 (/21) or smaller.' 75 | ); 76 | TCPFlag = false; 77 | } 78 | 79 | Object.keys(PLUGINS.all).forEach((pluginType) => { 80 | const plugin = PLUGINS.all[pluginType]; 81 | 82 | try { 83 | const searchType = plugin.config.searchOptions.type; 84 | 85 | if (searchType === 'TCPport') { 86 | if (TCPFlag) { 87 | searchTCP(pluginType, plugin.config); 88 | } 89 | } else if (searchType === 'Bonjour') { 90 | searchBonjour(pluginType, plugin.config); 91 | } else if (searchType === 'UDPsocket') { 92 | searchUDP(pluginType, plugin.config); 93 | } else if (searchType === 'multicast') { 94 | searchMulticast(pluginType, plugin.config); 95 | } else if (searchType === 'UDPScan') { 96 | searchUDPScan(pluginType, plugin.config); 97 | } 98 | } catch (err) { 99 | console.error(`Unable to search for plugin ${pluginType}`); 100 | } 101 | }); 102 | 103 | setTimeout(() => { 104 | searching = false; 105 | document.getElementById('search-button').style.opacity = ''; 106 | 107 | for (let i = 0; i < searchSockets.length; i++) { 108 | try { 109 | searchSockets[i].close(); 110 | } catch (err) { 111 | // 112 | } 113 | } 114 | 115 | ipcRenderer.send('enableSearchAll', ''); 116 | }, 10000); 117 | } 118 | module.exports.searchAll = searchAll; 119 | 120 | function searchBonjour(pluginType, pluginConfig) { 121 | bonjour.find({ type: pluginConfig.searchOptions.bonjourName }, (e) => { 122 | const validAddresses = []; 123 | e.addresses.forEach((address) => { 124 | if (!address.includes(':')) { 125 | validAddresses.push(address); 126 | } 127 | }); 128 | 129 | DEVICE.registerDevice( 130 | { 131 | type: pluginType, 132 | defaultName: e.name, 133 | remotePort: e.port, 134 | addresses: validAddresses, 135 | }, 136 | 'fromSearch' 137 | ); 138 | }); 139 | } 140 | 141 | function searchTCP(pluginType, pluginConfig) { 142 | for (let i = 0; i < allServers.length; i++) { 143 | TCPtest(allServers[i], pluginType, pluginConfig); 144 | } 145 | } 146 | 147 | function TCPtest(ipAddr, pluginType, pluginConfig) { 148 | const client = net.createConnection(pluginConfig.searchOptions.testPort, ipAddr, () => { 149 | client.write(pluginConfig.searchOptions.searchBuffer); 150 | }); 151 | client.on('data', (data) => { 152 | if (pluginConfig.searchOptions.validateResponse(data)) { 153 | client.end( 154 | '', 155 | 'utf8', 156 | DEVICE.registerDevice( 157 | { 158 | type: pluginType, 159 | defaultName: pluginConfig.defaultName, 160 | remotePort: pluginConfig.remotePort, 161 | addresses: [ipAddr], 162 | }, 163 | 'fromSearch' 164 | ) 165 | ); 166 | } 167 | }); 168 | client.on('error', (err) => { 169 | // no device here 170 | }); 171 | } 172 | 173 | function searchUDPScan(pluginType, pluginConfig) { 174 | for (let i = 0; i < Object.keys(validInterfaces).length; i++) { 175 | const interfaceID = Object.keys(validInterfaces)[i]; 176 | const interfaceObj = validInterfaces[interfaceID]; 177 | interfaceObj.forEach((netInterface) => { 178 | const udpSocket = dgram.createSocket('udp4'); 179 | udpSocket.bind(pluginConfig.searchOptions.listenPort, netInterface.address); 180 | 181 | udpSocket.on('message', (msg, info) => { 182 | if (pluginConfig.searchOptions.validateResponse(msg, info, DEVICE.all)) { 183 | udpSocket.close(); 184 | DEVICE.registerDevice( 185 | { 186 | type: pluginType, 187 | defaultName: pluginConfig.defaultName, 188 | port: pluginConfig.defaultPort, 189 | addresses: [info.address], 190 | }, 191 | 'fromSearch' 192 | ); 193 | } 194 | }); 195 | const interfaceBlock = new Netmask(netInterface.cidr); 196 | interfaceBlock.forEach((address, long, index) => { 197 | udpSocket.send(pluginConfig.searchOptions.searchBuffer, pluginConfig.searchOptions.devicePort, address); 198 | }); 199 | }); 200 | } 201 | } 202 | 203 | function searchUDP(pluginType, pluginConfig) { 204 | for (let i = 0; i < Object.keys(validInterfaces).length; i++) { 205 | const interfaceID = Object.keys(validInterfaces)[i]; 206 | const interfaceObj = validInterfaces[interfaceID]; 207 | 208 | const j = searchSockets.push(dgram.createSocket('udp4')) - 1; 209 | 210 | searchSockets[j].bind(pluginConfig.searchOptions.listenPort, () => { 211 | searchSockets[j].on('message', (msg, info) => { 212 | if (pluginConfig.searchOptions.validateResponse(msg, info, DEVICE.all)) { 213 | searchSockets[j].close(); 214 | DEVICE.registerDevice( 215 | { 216 | type: pluginType, 217 | defaultName: pluginConfig.defaultName, 218 | remotePort: pluginConfig.remotePort, 219 | addresses: [info.address], 220 | }, 221 | 'fromSearch' 222 | ); 223 | } 224 | }); 225 | }); 226 | 227 | searchSockets[j].on('listening', () => { 228 | searchSockets[j].setBroadcast(true); 229 | searchSockets[j].send( 230 | pluginConfig.searchOptions.searchBuffer, 231 | pluginConfig.searchOptions.devicePort, 232 | interfaceObj[0].broadcastAddress, 233 | (err) => { 234 | // console.log(err); 235 | } 236 | ); 237 | }); 238 | } 239 | } 240 | 241 | function searchMulticast(pluginType, pluginConfig) { 242 | const socket = dgram.createSocket('udp4'); 243 | socket.on('message', (msg, info) => { 244 | if (pluginConfig.searchOptions.validateResponse(msg, info)) { 245 | socket.close(() => { 246 | DEVICE.registerDevice( 247 | { 248 | type: pluginType, 249 | defaultName: pluginConfig.defaultName, 250 | remotePort: pluginConfig.remotePort, 251 | addresses: [info.address], 252 | }, 253 | 'fromSearch' 254 | ); 255 | }); 256 | } 257 | }); 258 | 259 | socket.bind(pluginConfig.searchOptions.port, () => { 260 | for (let i = 0; i < Object.keys(validInterfaces).length; i++) { 261 | const interfaceID = Object.keys(validInterfaces)[i]; 262 | const interfaceObj = validInterfaces[interfaceID]; 263 | 264 | socket.addMembership(pluginConfig.searchOptions.address, interfaceObj[0].address); 265 | } 266 | }); 267 | } 268 | --------------------------------------------------------------------------------