Merge branch 'main' into plugin-sennheiser-wireless

This commit is contained in:
2023-05-11 06:48:52 -05:00
60 changed files with 9056 additions and 1709 deletions
+31
View File
@@ -0,0 +1,31 @@
---
name: Bug report
about: Let us know what isn't working
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows 10]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
+7 -2
View File
@@ -1,7 +1,7 @@
name: release
on:
workflow_dispatch:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -13,28 +13,33 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run release
build-windows:
needs: build-linux
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run release
build-macos:
needs: build-windows
runs-on: macos-latest
env:
CSC_LINK: ${{ secrets.MACOS_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MACOS_CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run release
+1 -1
View File
@@ -1 +1 @@
v18.12.1
v18.13.0
+6 -4
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<title>Cue View</title>
<link rel="stylesheet" href="src/index.css" />
<link rel="stylesheet" href="src/assets/css/index.css" />
</head>
<body>
<div id="main">
@@ -22,12 +22,14 @@
<div id="device-tools">
<select class="left button" id="add-device-button" title="Add Device..."></select>
<button class="left" id="search-button" title="Search network for devices">
<img src="src/img/outline_search_white_18dp.png" />
<img src="src/assets/img/outline_search_white_18dp.png" />
</button>
<button class="left" id="refresh-device-button" title="Refresh Device" disabled>
<img src="src/img/outline_refresh_white_18dp.png" />
<img src="src/assets/img/outline_refresh_white_18dp.png" />
</button>
<button class="right" id="network-info-button">
<img src="src/assets/img/outline_info_white_18dp.png" />
</button>
<button class="right" id="network-info-button"><img src="src/img/outline_info_white_18dp.png" /></button>
</div>
<div id="device-settings">
+48 -20
View File
@@ -1,9 +1,11 @@
const { app, BrowserWindow, Menu, ipcMain, nativeTheme, dialog } = require('electron');
const { app, BrowserWindow, Menu, ipcMain, nativeTheme, dialog, shell } = require('electron');
const { autoUpdater } = require('electron-updater');
const path = require('path');
const packageInfo = require('./package.json');
const isMac = process.platform === 'darwin';
const isWin = process.platform === 'win32';
const isLinux = process.platform === 'linux';
let autoUpdate = false;
let manualUpdateCheck = false;
@@ -146,7 +148,6 @@ const windowMac = {
transparent: true,
frame: false,
show: false,
// backgroundColor: "#333333",
vibrancy: 'window',
visualEffectState: 'followWindow',
webPreferences: {
@@ -167,6 +168,21 @@ const windowWin = {
},
};
const networkInfoMac = {
width: 700,
height: 350,
// transparent: true,
frame: true,
show: false,
vibrancy: 'window',
visualEffectState: 'followWindow',
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload: path.join(__dirname, 'networkInterfaces.js'),
},
};
const networkInfoWin = {
width: 700,
height: 350,
@@ -192,6 +208,10 @@ const createWindow = () => {
mainWindow = new BrowserWindow(windowWin);
}
if (isLinux) {
mainWindow.setIcon(path.join(__dirname, 'src', 'assets', 'img', 'icon.png'));
}
mainWindow.loadFile('index.html');
mainWindow.on('ready-to-show', () => {
@@ -240,7 +260,24 @@ ipcMain.on('setDevicePin', (event, arg) => {
menuObj.getMenuItemById('devicePin').checked = arg;
});
// Autoupdate logic
ipcMain.on('openNetworkInfoWindow', (event, arg) => {
openNetworkInfoWindow();
});
function openNetworkInfoWindow() {
if (!networkInfoWindow || (networkInfoWindow && networkInfoWindow.isDestroyed())) {
if (isMac) {
networkInfoWindow = new BrowserWindow(networkInfoMac);
} else {
networkInfoWindow = new BrowserWindow(networkInfoWin);
networkInfoWindow.removeMenu();
}
networkInfoWindow.loadFile('networkInterfaces.html');
}
networkInfoWindow.show();
}
// ONLY Autoupdate logic below
ipcMain.on('checkForUpdates', (event, arg) => {
autoUpdater.checkForUpdates();
});
@@ -254,18 +291,6 @@ ipcMain.on('setAutoUpdate', (event, _autoUpdate) => {
Menu.setApplicationMenu(menuObj);
});
ipcMain.on('openNetworkInfoWindow', (event, arg) => {
openNetworkInfoWindow();
});
function openNetworkInfoWindow() {
if (!networkInfoWindow || (networkInfoWindow && networkInfoWindow.isDestroyed())) {
networkInfoWindow = new BrowserWindow(networkInfoWin);
networkInfoWindow.loadFile('networkInterfaces.html');
}
networkInfoWindow.show();
}
// this can be set to true to bypass the download update dialog and skip straight to install prompt
autoUpdater.autoDownload = false;
@@ -283,7 +308,7 @@ autoUpdater.on('update-available', (updateInfo) => {
if (isMac) {
dialogOpts = {
type: 'info',
buttons: ['Download', 'Cancel'],
buttons: ['Download', 'Cancel', 'View Release Notes'],
title: 'Update Available',
message: title,
detail: msg,
@@ -291,16 +316,19 @@ autoUpdater.on('update-available', (updateInfo) => {
} else {
dialogOpts = {
type: 'info',
buttons: ['Download', 'Cancel'],
buttons: ['Download', 'Cancel', 'View Release Notes'],
title: 'Update Available',
message: msg,
};
}
dialog.showMessageBox(mainWindow, dialogOpts).then((returnValue) => {
// download was clicked
if (returnValue.response === 0) {
// download was clicked
autoUpdater.downloadUpdate();
} else if (returnValue.response === 2) {
// view release notes clicked
shell.openExternal(`${packageInfo.repository}/releases/tag/v${updateInfo.version}`);
}
});
});
@@ -315,7 +343,7 @@ autoUpdater.on('update-downloaded', (event) => {
dialogOpts = {
type: 'info',
buttons: ['Install', 'Later'],
title: 'Update Available',
title: 'Update Downloaded',
message: title,
detail: msg,
};
@@ -323,7 +351,7 @@ autoUpdater.on('update-downloaded', (event) => {
dialogOpts = {
type: 'info',
buttons: ['Install', 'Later'],
title: 'Update Available',
title: 'Update Downloaded',
message: msg,
};
}
+1 -1
View File
@@ -7,7 +7,7 @@
color: white;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont;
user-select: none;
background-color: rgba(0, 0, 0, 0.3);
background-color: transparent;
}
table {
width: 100%;
+10 -6
View File
@@ -7,11 +7,13 @@ window.init = function init() {
for (let i = 0; i < Object.keys(networkInterfaces).length; i++) {
const interfaceID = Object.keys(networkInterfaces)[i];
const interfaceObj = networkInterfaces[interfaceID];
console.log(interfaceObj);
html += `<tr><td><span class="if-${interfaceID.substring(0, 2)}">${interfaceID}</span></td>`;
html += `<td>${interfaceObj[0].address}</td>`;
html += `<td>${interfaceObj[0].netmask}</td>`;
html += `
<tr>
<td><span class="if-${interfaceID.substring(0, 2)}">${interfaceID}</span></td>
<td>${interfaceObj[0].address}</td>
<td>${interfaceObj[0].netmask}</td>
`;
if (interfaceObj[0].searchTruncated) {
html += `<td class='red'>`;
@@ -19,8 +21,10 @@ window.init = function init() {
html += `<td class='green'>`;
}
html += `${interfaceObj[0].firstSearchAddress} - ${interfaceObj[0].lastSearchAddress}</td>`;
html += `</tr>`;
html += `
${interfaceObj[0].firstSearchAddress} - ${interfaceObj[0].lastSearchAddress}</td>
</tr>
`;
document.getElementById('network-interfaces').innerHTML = html;
}
+7831 -1313
View File
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -1,7 +1,7 @@
{
"name": "cue-view",
"productName": "Cue View",
"version": "0.9.8-pre",
"version": "0.9.9-pre",
"description": "A dashboard for everything in your show",
"main": "main.js",
"scripts": {
@@ -21,31 +21,31 @@
"repository": "https://github.com/stagehacks/Cue-View",
"devDependencies": {
"@electron/notarize": "^1.2.3",
"electron": "^21.3.0",
"electron-builder": "^23.6.0",
"eslint": "^8.27.0",
"electron": "^22.2.0",
"electron-builder": "^24.4.0",
"eslint": "^8.40.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"prettier": "^2.7.1"
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"prettier": "^2.8.8"
},
"dependencies": {
"atem-connection": "^3.1.3",
"bonjour": "^3.5.0",
"electron-updater": "^5.3.0",
"lodash": "^4.17.20",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"netmask": "^2.0.2",
"osc": "^2.4.3",
"osc": "^2.4.4",
"uuid": "^9.0.0"
},
"build": {
"appId": "com.stagehacks.cueview",
"icon": "./src/img/",
"icon": "src/assets/img/",
"artifactName": "${name}.${os}.v${version}.${ext}",
"afterSign": "notarize.js",
"mac": {
"category": "Utilities",
"icon": "./src/img/icon.icns",
"icon": "src/assets/img/icon.icns",
"hardenedRuntime": true,
"electronLanguages": [
"en"
@@ -56,7 +56,7 @@
},
"win": {
"target": "NSIS",
"icon": "./src/img/icon.ico",
"icon": "src/assets/img/icon.ico",
"publish": [
"github"
]
+6 -7
View File
@@ -1,16 +1,15 @@
const _ = require('lodash');
exports.config = {
defaultName: 'Art-Net',
connectionType: 'UDPsocket',
heartbeatInterval: 10000,
defaultPort: 6454,
mayChangePort: false,
heartbeatInterval: 5000,
heartbeatTimeout: 15000,
searchOptions: {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x00]),
devicePort: 6454,
listenPort: 6454,
mayChangePort: false,
validateResponse(msg, info, devices) {
return msg.toString('utf8', 0, 7) === 'Art-Net';
},
@@ -43,7 +42,7 @@ exports.data = function data(_device, buf) {
universe.slots = buf.slice(18);
device.data.ip = device.addresses[0];
if (!_.includes(device.data.orderedUniverses, universeIndex)) {
if (!device.data.orderedUniverses.includes(universeIndex)) {
device.data.orderedUniverses.push(universeIndex);
device.data.orderedUniverses.sort();
universe.slotElems = [];
@@ -77,10 +76,10 @@ exports.update = function update(_device, _document, updateType, updateData) {
if ($elem && data.universe.slotElemsSet) {
for (let i = 0; i < 512; i++) {
data.universe.slotElems[i].innerText = data.universe.slots[i];
data.universe.slotElems[i].textContent = data.universe.slots[i];
}
document.getElementById(`universe-${data.universeIndex}-sequence`).innerText = data.universe.sequence;
document.getElementById(`universe-${data.universeIndex}-sequence`).textContent = data.universe.sequence;
} else {
device.draw();
device.update('elementCache');
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+11
View File
@@ -0,0 +1,11 @@
<h3>ATEM Configuration</h3>
<ul>
<li>Set the IP Address, Subnet Mask, and Gateway of the ATEM using the <em>ATEM Setup utility</em></li>
<li>The ATEM must be connected to a computer using a USB cable to change its Network settings</li>
</ul>
<h3>ATEM Software Download</h3>
<button href="https://www.blackmagicdesign.com/support/family/atem-live-production-switchers">
Blackmagic Support
</button>
(Look for <em>ATEM Switchers Update</em>)
+240
View File
@@ -0,0 +1,240 @@
const { TransitionStyle, TransitionSelection } = require('atem-connection/dist/enums');
let timerFrameRate;
exports.config = {
defaultName: 'ATEM',
connectionType: 'atem',
defaultPort: 9910,
mayChangePort: false,
searchOptions: {
type: 'UDPScan',
searchBuffer: Buffer.from([
0x10, 0x14, 0x53, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
]),
listenPort: 0,
devicePort: 9910,
validateResponse(msg, info) {
// This is a tad bit hacky but works from what I can see.
return Buffer.compare(msg.slice(0, 4), Buffer.from([16, 20, 83, 171])) === 0;
},
},
};
exports.ready = function ready(_device) {
console.log('atem ready');
const device = _device;
device.data = device.connection.state;
device.draw();
// TODO: this is a little hacky but it works? gotta be a way to hook into device.draw() to trigger update
setInterval(() => {
device.update('inputs', device.data);
device.update('fadeToBlack', device.data);
device.update(`transitionPosition`, device.data);
device.update('downstreamKeyers', device.data);
device.update('upstreamKeyers', device.data);
device.update('transitionProperties', device.data);
device.update('deviceInfo', device.data);
}, 1000);
};
exports.update = function update(device, _document, updateType, data) {
const document = _document;
if (updateType.includes('transitionPosition')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const mixEffect = data.video.mixEffects[i];
const tbarId = `me-${i}-tbar-div`;
const tbarHandleId = `me-${i}-tbar-handle-div`;
if (document.getElementById(tbarId)) {
document.getElementById(tbarId).style.height = `${mixEffect.transitionPosition.handlePosition / 100}%`;
}
if (document.getElementById(tbarHandleId)) {
document.getElementById(tbarHandleId).style.bottom = `${mixEffect.transitionPosition.handlePosition / 100}%`;
}
document.getElementById(`me-${i}-transition-rate`).textContent = framesToTime(
mixEffect.transitionPosition.remainingFrames
);
if (mixEffect.transitionPosition.inTransition) {
document.getElementById(`me-${i}-auto`).classList.add('atem-red');
} else {
document.getElementById(`me-${i}-auto`).classList.remove('atem-red');
}
}
} else if (updateType.includes('downstreamKeyers')) {
for (let i = 0; i < data.video.downstreamKeyers.length; i++) {
const dsk = data.video.downstreamKeyers[i];
if (dsk.isAuto) {
document.getElementById(`dsk-${i}-auto`).classList.add('atem-red');
} else {
document.getElementById(`dsk-${i}-auto`).classList.remove('atem-red');
}
if (dsk.onAir) {
document.getElementById(`dsk-${i}-onair`).classList.add('atem-red');
} else {
document.getElementById(`dsk-${i}-onair`).classList.remove('atem-red');
}
if (dsk.properties.tie) {
document.getElementById(`dsk-${i}-tie`).classList.add('atem-yellow');
} else {
document.getElementById(`dsk-${i}-tie`).classList.remove('atem-yellow');
}
document.getElementById(`dsk-${i}-rate`).textContent = framesToTime(dsk.remainingFrames);
}
} else if (updateType.includes('fadeToBlack')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const fadeToBlack = data.video.mixEffects[i].fadeToBlack;
const ftbRateId = `me-${i}-ftb-rate`;
const ftbId = `me-${i}-ftb`;
document.getElementById(ftbRateId).textContent = framesToTime(fadeToBlack.remainingFrames);
if (fadeToBlack.isFullyBlack) {
if (document.getElementById(ftbId).classList.contains('atem-red')) {
document.getElementById(ftbId).classList.remove('atem-red');
} else {
document.getElementById(ftbId).classList.add('atem-red');
}
} else if (fadeToBlack.inTransition) {
document.getElementById(ftbId).classList.add('atem-red');
} else {
document.getElementById(ftbId).classList.remove('atem-red');
}
}
} else if (
updateType.includes('programInput') ||
updateType.includes('previewInput') ||
updateType.includes('inputs')
) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const mixEffect = data.video.mixEffects[0];
Object.keys(device.data.inputs)
.map((key) => Number(key))
.forEach((inputId) => {
if (mixEffect.programInput === inputId) {
document.getElementById(`me-${i}-program-input-${inputId}`).classList.add('atem-red');
document.getElementById(`me-${i}-input-${inputId}`).classList.add('atem-red');
} else {
document.getElementById(`me-${i}-program-input-${inputId}`).classList.remove('atem-red');
document.getElementById(`me-${i}-input-${inputId}`).classList.remove('atem-red');
}
if (mixEffect.previewInput === inputId) {
document.getElementById(`me-${i}-preview-input-${inputId}`).classList.add('atem-green');
document.getElementById(`me-${i}-input-${inputId}`).classList.add('atem-green');
} else {
document.getElementById(`me-${i}-preview-input-${inputId}`).classList.remove('atem-green');
document.getElementById(`me-${i}-input-${inputId}`).classList.remove('atem-green');
}
});
}
} else if (updateType.includes('transitionProperties')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const transitionProperties = data.video.mixEffects[i].transitionProperties;
Object.keys(TransitionStyle)
.filter((key) => !isNaN(Number(key)))
.map((key) => Number(key))
.forEach((style) => {
if (style === transitionProperties.style) {
document.getElementById(`me-${i}-transition-style-${style}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-style-${style}`).classList.remove('atem-yellow');
}
if (style === transitionProperties.nextStyle) {
document.getElementById(`me-${i}-transition-style-${style}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-style-${style}`).classList.remove('atem-yellow');
}
});
Object.keys(TransitionSelection)
.filter((key) => !isNaN(Number(key)))
.map((key) => Number(key))
.forEach((selection) => {
if (transitionProperties.selection.includes(selection)) {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.remove('atem-yellow');
}
if (transitionProperties.nextSelection.includes(selection)) {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.remove('atem-yellow');
}
});
}
} else if (updateType.includes('upstreamKeyers')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const upstreamKeyers = data.video.mixEffects[i].upstreamKeyers;
upstreamKeyers.forEach((upstreamKeyer) => {
if (upstreamKeyer.onAir) {
document.getElementById(`me-${i}-key-${upstreamKeyer.upstreamKeyerId + 1}-onair`).classList.add('atem-red');
} else {
document
.getElementById(`me-${i}-key-${upstreamKeyer.upstreamKeyerId + 1}-onair`)
.classList.remove('atem-red');
}
});
}
} else if (updateType.includes('transitionPreview')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const mixEffect = data.video.mixEffects[i];
if (mixEffect.transitionPreview) {
document.getElementById(`me-${i}-transition-preview`).classList.add('atem-red');
} else {
document.getElementById(`me-${i}-transition-preview`).classList.remove('atem-red');
}
}
} else if (updateType.includes('deviceInfo')) {
// videoModes pulled from https://github.com/nrkno/sofie-atem-connection/blob/master/src/enums/index.ts#L238
if ([27, 26, 23, 25, 19, 13, 11, 7, 5].includes(data.settings.videoMode)) {
timerFrameRate = 30;
} else if ([24, 22, 18, 16, 12, 10, 6, 4].includes(data.settings.videoMode)) {
timerFrameRate = 25;
} else if ([21, 20, 15, 14, 9, 8].includes(data.settings.videoMode)) {
timerFrameRate = 24;
} else {
timerFrameRate = 30;
}
if (!device.displayName) {
this.deviceInfoUpdate(device, 'displayName', data.info.productIdentifier);
}
} else {
console.log('unhandled update');
console.log(updateType);
console.log(device.data);
}
};
exports.data = function data(_device, msg) {
const device = _device;
this.deviceInfoUpdate(device, 'status', 'ok');
device.data = msg.state;
msg.pathToChange.forEach((path) => {
device.update(path, device.data);
});
};
function framesToTime(total) {
if (timerFrameRate) {
const seconds = Math.floor(total / timerFrameRate);
const frames = total - seconds * timerFrameRate;
let framesString = String(frames);
if (frames < 10) {
framesString = `0${framesString}`;
} else if (framesString === '0') {
framesString = '00';
}
return `${seconds}:${framesString}`;
}
return '0:00';
}
+203
View File
@@ -0,0 +1,203 @@
body {
background-color: #282828;
}
h1 {
font-family: 'Open Sans', sans-serif;
}
h3 {
color: #6d6d6d !important;
margin: 30px 0px 10px 9px;
font-size: 0.9em;
font-weight: 400;
font-family: 'Open Sans', sans-serif;
}
.me-label {
margin-top: 0;
margin-bottom: 2px;
}
.atem-input {
width: 52px;
height: 52px;
background-image: url('img/button_white.png');
}
.source-wrapper {
display: flex;
flex-wrap: wrap;
background-color: #1f1f1f;
border: #1a1a1a 2px solid;
border-radius: 8px;
max-width: 416px;
padding: 12px;
}
.atem-red {
background-image: url('img/button_red.png');
box-shadow: 0px 0px 10px 1px #ff0000;
z-index: 10000;
}
.atem-green {
background-image: url('img/button_green.png');
box-shadow: 0px 0px 10px 1px #06c300;
z-index: 10000;
}
.atem-yellow {
background-image: url('img/button_yellow.png');
box-shadow: 0px 0px 10px 1px #c7ca00;
z-index: 10000;
}
.atem-disabled {
background-image: url('img/button_off.png');
color: #3a3a3a;
}
.atem-gray {
color: #575757;
}
.source-label {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 11px;
font-weight: bold;
}
.transition-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.transition-settings {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-right: 30px;
}
.tbar-container {
display: flex;
flex-direction: column-reverse;
background-color: #1f1f1f;
border: 2px solid;
border-color: #1a1a1a;
border-radius: 6px;
width: 88px;
height: 429px;
position: relative;
margin-top: 58px;
margin-right: 60px;
}
.tbar-bg {
position: absolute;
background-image: url('img/tbar_bg.png');
width: 88px;
height: 429px;
}
.tbar-handle {
position: absolute;
background-image: url('img/tbar_handle.png');
width: 126px;
height: 50px;
left: -3px;
}
.tbar-div {
width: 100%;
background-color: #7aff58;
overflow: hidden;
}
.dsk {
width: 100%;
}
.dsk .source-wrapper {
display: flex;
flex-direction: column;
align-content: center;
width: 100%;
padding: 5px;
}
.dsk h3 {
text-align: center;
}
.fade-to-black-container {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-right: 30px;
}
.fade-to-black .source-wrapper {
display: flex;
width: 100%;
padding: 5px;
}
.fade-to-black h3 {
text-align: center;
}
.rate-heading {
text-align: center;
font-size: small;
margin-bottom: 4px;
}
.dsk-rate,
.ftb-rate,
.transition-rate {
display: flex;
justify-content: center;
flex-direction: column;
}
.dsk-rate-label,
.ftb-rate-label,
.transition-rate-label {
color: rgb(235, 110, 0);
background-color: black;
border-radius: 3px;
padding: 6px 2px;
text-align: center;
}
.clear {
background: transparent;
border-color: transparent;
background-color: transparent;
}
.hide {
display: none;
}
.no-wrap {
flex-wrap: nowrap;
}
.show-small {
display: none;
}
@media screen and (min-width: 0px) and (max-width: 480px) {
.hide-small {
display: none;
}
.show-small {
display: block;
}
}
.float-left {
float: left;
}
+189
View File
@@ -0,0 +1,189 @@
<header>
<h1><%= listName %></h1>
</header>
<% if (data.video.mixEffects) { %>
<% Object.entries(data.video.mixEffects).forEach(([meIndex, state])=>{ %>
<div class="mixeffect_wrapper" id="me-<%= meIndex %>">
<h1 class="atem-gray me-label">ME<%= Number(meIndex) + 1 %></h1>
<div class="inputs-container show-small">
<h3 class="atem-gray">Preview/Program</h3>
<div class="source-wrapper">
<% Object.values(data.inputs).sort((a,b)=>a.internalPortType<b.internalPortType).forEach((input)=>{ %>
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
<div id="me-<%= meIndex %>-input-<%= input.inputId %>" class="atem-input">
<div class="source-label"><%= input.shortName %></div>
</div>
<% } else {%>
<div id="me-<%= meIndex %>-input-<%= input.inputId %>" class="atem-input atem-disabled">
<div class="source-label"><%= input.shortName %></div>
</div>
<% }%>
<% }) %>
</div>
</div>
<div class="float-left" style="margin-right: 30px;">
<div class="program-container hide-small">
<h3 class="atem-gray">Program</h3>
<div class="source-wrapper">
<% Object.values(data.inputs).sort((a,b)=>a.internalPortType<b.internalPortType).forEach((input)=>{ %>
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
<div id="me-<%= meIndex %>-program-input-<%= input.inputId %>" class="atem-input">
<div class="source-label"><%= input.shortName %></div>
</div>
<% } else {%>
<div id="me-<%= meIndex %>-program-input-<%= input.inputId %>" class="atem-input atem-disabled">
<div class="source-label"><%= input.shortName %></div>
</div>
<% }%>
<% }) %>
</div>
</div>
<div class="preview hide-small">
<h3 class="atem-gray">Preview</h3>
<div class="source-wrapper">
<% Object.values(data.inputs).forEach((input)=>{ %>
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
<div id="me-<%= meIndex %>-preview-input-<%= input.inputId %>" class="atem-input">
<div class="source-label"><%= input.shortName %></div>
</div>
<% } else {%>
<div id="me-<%= meIndex %>-preview-input-<%= input.inputId %>" class="atem-input atem-disabled">
<div class="source-label"><%= input.shortName %></div>
</div>
<% }%>
<% }) %>
</div>
</div>
</div>
<div class="transition-container float-left">
<div class="transition-settings">
<div class="next-transition">
<h3>Next Transition</h3>
<div class="source-wrapper" style="width: 260px;">
<div class="atem-input clear"></div>
<div id="me-<%= meIndex %>-key-1-onair" class="atem-input">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-key-2-onair" class="atem-input <% if(data.video.downstreamKeyers.length<2){ %> atem-disabled <% } %>">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-key-3-onair" class="atem-input <% if(data.video.downstreamKeyers.length<3){ %> atem-disabled <% } %>">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-key-4-onair" class="atem-input <% if(data.video.downstreamKeyers.length<4){ %> atem-disabled <% } %>">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-1" class="atem-input">
<div class="source-label">BKGD</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-2" class="atem-input">
<div class="source-label">KEY 1</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-4" class="atem-input <% if(data.video.downstreamKeyers.length<2){ %> atem-disabled <% } %>">
<div class="source-label">KEY 2</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-8" class="atem-input <% if(data.video.downstreamKeyers.length<3){ %> atem-disabled <% } %>">
<div class="source-label">KEY 3</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-16" class="atem-input <% if(data.video.downstreamKeyers.length<4){ %> atem-disabled <% } %>">
<div class="source-label">KEY 4</div>
</div>
</div>
</div>
<div class="transition-style">
<h3>Transition Style</h3>
<div class="source-wrapper" style="width: 260px;">
<div id="me-<%= meIndex %>-transition-style-0" class="atem-input">
<div class="source-label">MIX</div>
</div>
<div id="me-<%= meIndex %>-transition-style-1" class="atem-input">
<div class="source-label">DIP</div>
</div>
<div id="me-<%= meIndex %>-transition-style-2" class="atem-input">
<div class="source-label">WIPE</div>
</div>
<div id="me-<%= meIndex %>-transition-style-4" class="atem-input">
<div class="source-label">STING</div>
</div>
<div id="me-<%= meIndex %>-transition-style-3" class="atem-input">
<div class="source-label">DVE</div>
</div>
<div id="me-<%= meIndex %>-transition-preview" class="atem-input">
<div class="source-label" style="flex-direction: column;">
<div>PREV</div> <div>TRANS</div>
</div>
</div>
<div class="atem-input clear"></div>
<div id="me-<%= meIndex %>-cut" class="atem-input">
<div class="source-label">CUT</div>
</div>
<div id="me-<%= meIndex %>-auto" class="atem-input">
<div class="source-label">AUTO</div>
</div>
<div class="transition-rate atem-input clear">
<div class="atem-gray rate-heading">Rate</div>
<div id="me-<%= meIndex %>-transition-rate" class="transition-rate-label">&nbsp;&nbsp;</div>
</div>
</div>
</div>
</div>
<div class="tbar-container">
<div class="tbar-bg"></div>
<div id="me-<%= meIndex %>-tbar-handle-div" class="tbar-handle"></div>
<div id="me-<%= meIndex %>-tbar-div" class="tbar-div" ></div>
</div>
<div class="fade-to-black-container">
<% if (data.video.downstreamKeyers) { %>
<% Object.entries(data.video.downstreamKeyers).forEach(([dskIndex, state])=>{ %>
<div class="dsk">
<h3 class="atem-gray">DSK<%= Number(dskIndex) + 1 %></h3>
<div class="source-wrapper">
<div id="dsk-<%= dskIndex %>-tie" class="atem-input">
<div class="source-label">TIE</div>
</div>
<div class="dsk-rate atem-input clear">
<div class="atem-gray rate-heading">Rate</div>
<div id="dsk-<%= dskIndex %>-rate" class="dsk-rate-label">&nbsp;&nbsp;</div>
</div>
<div id="dsk-<%= dskIndex %>-onair" class="atem-input">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="dsk-<%= dskIndex %>-auto" class="atem-input">
<div class="source-label">AUTO</div>
</div>
</div>
</div>
<% }) %>
<% } %>
<div class="fade-to-black">
<h3 class="atem-gray">Fade to Black</h3>
<div class="source-wrapper">
<div class="ftb-rate atem-input clear">
<div class="atem-gray rate-heading">Rate</div>
<div id="me-<%= meIndex %>-ftb-rate" class="ftb-rate-label">&nbsp;&nbsp;</div>
</div>
<div id="me-<%= meIndex %>-ftb" class="atem-input">
<div class="source-label">FTB</div>
</div>
</div>
</div>
</div>
</div>
</div>
<% }) %>
<% } %>
+3 -5
View File
@@ -2,17 +2,15 @@
<h4>Shell/ECU &rarr; Network</h4>
<ul>
<li>
Enable <em>&#10004; TCP OSC</em>. The TCP format dropdown does not matter.
</li>
<li>Enable <em>&#10004; TCP OSC</em>. The TCP format should be "TCP Format for OSC 1.1 (SLIP)".</li>
<li>Enable <em>Third Party OSC</em></li>
</ul>
If the "Third Party OSC" option is not available, Eos must be updated to 3.1 or
newer.
If the "Third Party OSC" option is not available, Eos must be updated to 3.1 or newer.
<h4>System Settings &rarr; Show Control &rarr; OSC</h4>
<ul>
<li>Enable <em>&#10004; OSC RX</em></li>
<li>Enable <em>&#10004; OSC TX</em></li>
<li>All other fields may be left to defaults or blank.</li>
<li>If a custom port is desired specify it in under OSC TCP Server Ports.</li>
</ul>
+14 -8
View File
@@ -6,12 +6,14 @@ const Cue = require('./cue');
exports.config = {
defaultName: 'ETC Eos',
connectionType: 'osc',
defaultPort: 3032,
mayChangePort: false,
defaultPort: 3037,
mayChangePort: true,
heartbeatInterval: 5000,
heartbeatTimeout: 6000,
searchOptions: {
type: 'TCPport',
searchBuffer: Buffer.from('\xc0/eos/ping\x00\x00\x2c\x00\x00\x00\xc0', 'ascii'),
testPort: 3032,
testPort: 3037,
validateResponse(msg, info) {
return msg.toString().includes('/eos/out');
},
@@ -21,6 +23,9 @@ exports.config = {
exports.ready = function ready(_device) {
const device = _device;
device.data.EOS = new EOS();
device.templates = {
cue: _.template(fs.readFileSync(path.join(__dirname, `cue.ejs`))),
};
device.send('/eos/get/cuelist/count');
device.send('/eos/get/version');
device.send('/eos/subscribe', [{ type: 'i', value: 1 }]);
@@ -28,6 +33,7 @@ exports.ready = function ready(_device) {
exports.data = function data(_device, osc) {
const device = _device;
this.deviceInfoUpdate(device, 'status', 'ok');
const addressParts = osc.address.split('/');
addressParts.shift();
@@ -48,7 +54,10 @@ exports.data = function data(_device, osc) {
device.send(`/eos/get/cue/${addressParts[4]}/index/${i}`);
}
} else if (match(addressParts, ['eos', 'out', 'get', 'cue', '*', '*', '*', 'list', '*', '*'])) {
this.deviceInfoUpdate(device, 'status', 'ok');
if (device.data.EOS.cueLists[addressParts[4]] === undefined) {
device.data.EOS.cueLists[addressParts[4]] = {};
device.send(`/eos/get/cue/${addressParts[4]}/count`);
}
if (device.data.EOS.cueLists[addressParts[4]][addressParts[5]] === undefined) {
device.data.EOS.cueLists[addressParts[4]][addressParts[5]] = {};
}
@@ -85,14 +94,11 @@ exports.data = function data(_device, osc) {
}
};
const cueTemplate = _.template(fs.readFileSync(path.join(__dirname, `cue.ejs`)));
exports.update = function update(device, doc, updateType, data) {
if (updateType === 'cueData') {
const $elem = doc.getElementById(data.uid);
if ($elem) {
$elem.outerHTML = cueTemplate({
$elem.outerHTML = device.templates.cue({
q: data.cue,
cueNumber: data.cueNumber,
isActive: false,
+11
View File
@@ -3,6 +3,7 @@ table {
border: #404040 1px solid;
border-radius: 4px;
color: #a59baa;
width: 100%;
}
th {
font-size: 14px;
@@ -62,6 +63,16 @@ tr.active-cue .time {
border-color: #c78b07;
}
.list_name {
color: lightgray;
margin-left: 5px;
margin-bottom: 2px;
}
.list_container {
margin-bottom: 10px;
}
@media screen and (min-width: 0px) and (max-width: 750px) {
.hide-medium {
display: none;
+31 -36
View File
@@ -3,45 +3,40 @@
<h2>Eos <%= data.version %></h2>
</header>
<%
const fs = require('fs');
const path = require('path');
let cueTemplate = _.template(fs.readFileSync(path.join(__dirname, `/plugins/eos/cue.ejs`)));
%>
<% for(var i in data.EOS.cueLists){ %>
<strong>List <%= i %></strong>
<table cellspacing="0">
<tr>
<th width="70px">Cue</th>
<th width="30px">Int Up</th>
<th width="30px">Int Down</th>
<th width="40px" class="hide-medium">Focus</th>
<th width="40px" class="hide-medium">Color</th>
<th width="40px" class="hide-medium">Beam</th>
<th width="40px" class="hide-small">Dur</th>
<th width="20px" class="hide-small">M</th>
<th width="20px" class="hide-small">B</th>
<th width="20px" class="hide-small">A</th>
<th width="70px" class="hide-small">Fw/Hg</th>
<th width="150px">Label</th>
<th width="10px" class="hide-medium">Ext Links</th>
</tr>
<div class="list_container">
<div class="list_name">List <%= i %></div>
<table cellspacing="0">
<tr>
<th width="70px">Cue</th>
<th width="30px">Int Up</th>
<th width="30px">Int Down</th>
<th width="40px" class="hide-medium">Focus</th>
<th width="40px" class="hide-medium">Color</th>
<th width="40px" class="hide-medium">Beam</th>
<th width="40px" class="hide-small">Dur</th>
<th width="20px" class="hide-small">M</th>
<th width="20px" class="hide-small">B</th>
<th width="20px" class="hide-small">A</th>
<th width="70px" class="hide-small">Fw/Hg</th>
<th width="150px">Label</th>
<th width="10px" class="hide-medium">Ext Links</th>
</tr>
<% var cues = Object.keys(data.EOS.cueLists[i]).sort(function(a, b){return Number(a)-Number(b)}) %>
<% var cues = Object.keys(data.EOS.cueLists[i]).sort(function(a, b){return Number(a)-Number(b)}) %>
<% for(var j=0; j<cues.length; j++){ %>
<% var q = data.EOS.cueLists[i][cues[j]] %>
<%= cueTemplate({
q: q,
cues: cues,
cueNumber: cues[j],
isActive: (cues[j]==data.EOS.activeCue+"")
}) %>
<% for(var j=0; j<cues.length; j++){ %>
<% var q = data.EOS.cueLists[i][cues[j]] %>
<%= templates.cue({
q: q,
cues: cues,
cueNumber: cues[j],
isActive: (cues[j]==data.EOS.activeCue+"")
}) %>
<% } %>
</table>
<% } %>
</table>
</div>
<% } %>
+2 -2
View File
@@ -3,10 +3,10 @@ const md5 = require('md5');
exports.config = {
defaultName: 'PJLink Projector',
connectionType: 'TCPsocket',
heartbeatInterval: 5000,
heartbeatTimeout: 15000,
defaultPort: 4352,
mayChangePort: false,
heartbeatInterval: 5000,
heartbeatTimeout: 15000,
searchOptions: {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x25, 0x32, 0x53, 0x52, 0x43, 0x48, 0x0d]),
+2 -4
View File
@@ -40,13 +40,11 @@
function displayCueRow(q){
let html = "";
html+= rowTemplate({cue: allCues[q.uniqueID], allCues: allCues, workspace: workspace});
let html = rowTemplate({cue: allCues[q.uniqueID], allCues: allCues, workspace: workspace});
if(q.cues){
q.cues.forEach(q =>{
html+=displayCueRow(q);
html += displayCueRow(q);
})
}
return html;
+64 -37
View File
@@ -5,9 +5,10 @@ const path = require('path');
exports.config = {
defaultName: 'QLab',
connectionType: 'osc',
heartbeatInterval: 50,
heartbeatTimeout: 2000,
defaultPort: 53000,
mayChangePort: true,
heartbeatInterval: 100,
heartbeatTimeout: 5000,
searchOptions: {
type: 'Bonjour',
bonjourName: 'qlab',
@@ -35,11 +36,14 @@ const valuesForKeysString =
'"mode","parent","cartRows","cartColumns","cartPosition","displayName","preWaitElapsed",' +
'"actionElapsed","postWaitElapsed","isPaused"]';
const cueTemplate = _.template(fs.readFileSync(path.join(__dirname, `cue.ejs`)));
const tileTemplate = _.template(fs.readFileSync(path.join(__dirname, `tile.ejs`)));
const cartTemplate = _.template(fs.readFileSync(path.join(__dirname, `cart.ejs`)));
exports.ready = function ready(device) {
exports.ready = function ready(_device) {
const device = _device;
device.templates = {
cue: _.template(fs.readFileSync(path.join(__dirname, `cue.ejs`))),
tile: _.template(fs.readFileSync(path.join(__dirname, `tile.ejs`))),
cart: _.template(fs.readFileSync(path.join(__dirname, `cart.ejs`))),
cuelist: _.template(fs.readFileSync(path.join(__dirname, `cuelist.ejs`))),
};
device.send(`/version`);
device.send('/workspaces');
};
@@ -213,45 +217,55 @@ exports.data = function data(_device, oscData) {
if (workspace) {
const cue = workspace.cues[oscArgs.address.substring(55, 91)];
cue.preWaitElapsed = oscArgs.data;
lastElapsedUpdate = Date.now();
if (cue) {
cue.preWaitElapsed = oscArgs.data;
lastElapsedUpdate = Date.now();
device.update('updateCueData', {
cue,
allCues: workspace.cues,
workspace,
});
device.update('updateCueData', {
cue,
allCues: workspace.cues,
workspace,
});
}
}
}
} else if (match(oscAddressParts, ['reply', 'cue_id', '*', 'actionElapsed'])) {
const oscArgs = JSON.parse(oscData.args[0]);
if (oscArgs.status !== 'error') {
const workspace = device.data.workspaces[oscArgs.workspace_id];
const cue = workspace.cues[oscArgs.address.substring(55, 91)];
if (workspace) {
const cue = workspace.cues[oscArgs.address.substring(55, 91)];
cue.actionElapsed = oscArgs.data;
lastElapsedUpdate = Date.now();
if (cue) {
cue.actionElapsed = oscArgs.data;
lastElapsedUpdate = Date.now();
device.update('updateCueData', {
cue,
allCues: workspace.cues,
workspace,
});
device.update('updateCueData', {
cue,
allCues: workspace.cues,
workspace,
});
}
}
}
} else if (match(oscAddressParts, ['reply', 'cue_id', '*', 'postWaitElapsed'])) {
const oscArgs = JSON.parse(oscData.args[0]);
if (oscArgs.status !== 'error') {
const workspace = device.data.workspaces[oscArgs.workspace_id];
const cue = workspace.cues[oscArgs.address.substring(55, 91)];
if (workspace) {
const cue = workspace.cues[oscArgs.address.substring(55, 91)];
cue.postWaitElapsed = oscArgs.data;
lastElapsedUpdate = Date.now();
if (cue) {
cue.postWaitElapsed = oscArgs.data;
lastElapsedUpdate = Date.now();
device.update('updateCueData', {
cue,
allCues: workspace.cues,
workspace,
});
device.update('updateCueData', {
cue,
allCues: workspace.cues,
workspace,
});
}
}
}
} else if (match(oscAddressParts, ['update', 'workspace', '*', 'cue_id', '*'])) {
const workspace = device.data.workspaces[oscAddressParts[2]];
@@ -276,7 +290,6 @@ exports.data = function data(_device, oscData) {
} else if (match(oscAddressParts, ['update', 'workspace', '*', 'dashboard'])) {
// this workspace might be new, let's check
if (device.data.workspaces[oscAddressParts[2]] === undefined) {
console.log('new workspace!');
device.send('/workspaces');
}
} else if (match(oscAddressParts, ['update', 'workspace', '*', 'cueList', '*', 'playbackPosition'])) {
@@ -292,7 +305,12 @@ exports.data = function data(_device, oscData) {
}
} else if (match(oscAddressParts, ['update', 'workspace', '*', 'disconnect'])) {
delete device.data.workspaces[oscAddressParts[2]];
if (Object.keys(device.data.workspaces).length === 0) {
device.data.permission = 'no workspaces';
}
device.draw();
} else if (match(oscAddressParts, ['reply', 'thump'])) {
// intermittent connection check even if nothing's happening
} else {
// console.log(address)
}
@@ -306,8 +324,8 @@ exports.update = function update(device, doc, updateType, data) {
if (data.cue.type === 'Cue List') {
$elem.outerHTML = `<h3>${data.workspace.displayName} &mdash; ${data.cue.name}</h3>`;
} else if (data.cue.type === 'Cart') {
$elem.outerHTML = cartTemplate({
tileTemplate,
$elem.outerHTML = device.templates.cart({
tileTemplate: device.templates.tile,
cueList: data.cue,
allCues: data.workspace.cues,
});
@@ -315,12 +333,12 @@ exports.update = function update(device, doc, updateType, data) {
// checking that the parent cue is a cart cue
const parentCue = data.workspace.cues[data.cue.parent];
if (parentCue && parentCue.type === 'Cart') {
$elem.outerHTML = tileTemplate(data);
$elem.outerHTML = device.templates.tile(data);
} else {
$elem.outerHTML = cueTemplate(data);
$elem.outerHTML = device.templates.cue(data);
}
} else {
$elem.outerHTML = cueTemplate(data);
$elem.outerHTML = device.templates.cue(data);
}
}
} else if (updateType === 'updatePlaybackPosition') {
@@ -381,7 +399,16 @@ exports.heartbeat = function heartbeat(device) {
interval = 1;
}
if (heartbeatCount % interval === 0) {
if (heartbeatCount % 20 === 0 && device.data.workspaces && Object.keys(device.data.workspaces).length === 0) {
device.send(`/version`);
device.send('/workspaces');
}
if (heartbeatCount % 16 === 0) {
device.send(`/thump`);
}
if (heartbeatCount % interval === 0 && device.data.workspaces && Object.keys(device.data.workspaces).length > 0) {
device.send(`/cue_id/active/preWaitElapsed`);
device.send(`/cue_id/active/actionElapsed`);
device.send(`/cue_id/active/postWaitElapsed`);
+2 -12
View File
@@ -3,16 +3,6 @@
<h2>QLab <%= data.version || "" %></h2>
</header>
<%
const fs = require('fs');
let _ = require('lodash');
const path = require('path');
let cueTemplate = _.template(fs.readFileSync(path.join(__dirname, `/plugins/qlab/cue.ejs`)));
let tileTemplate = _.template(fs.readFileSync(path.join(__dirname, `/plugins/qlab/tile.ejs`)));
let cartTemplate = _.template(fs.readFileSync(path.join(__dirname, `/plugins/qlab/cart.ejs`)));
let listTemplate = _.template(fs.readFileSync(path.join(__dirname, `/plugins/qlab/cuelist.ejs`)));
%>
<% if(data.permission=="ok"){
for(const workspace_id in data.workspaces){
@@ -21,9 +11,9 @@
const ql = workspace.cueLists[cueList_id]; %>
<% if(ql.type=="Cue List"){ %>
<%= listTemplate({cueList: ql, allCues: workspace.cues, rowTemplate: cueTemplate, workspace: workspace}) %>
<%= templates.cuelist({cueList: ql, allCues: workspace.cues, rowTemplate: templates.cue, workspace: workspace}) %>
<% }else if(ql.type=="Cart"){ %>
<%= cartTemplate({cueList: ql, allCues: workspace.cues, tileTemplate: tileTemplate}) %>
<%= templates.cart({cueList: ql, allCues: workspace.cues, tileTemplate: templates.tile}) %>
<% } %>
<% }}}else if(data.permission=="no workspaces"){ %>
+41 -28
View File
@@ -3,9 +3,10 @@ const _ = require('lodash');
exports.config = {
defaultName: 'sACN',
connectionType: 'multicast',
heartbeatInterval: 5000,
defaultPort: 5568,
mayChangePort: false,
heartbeatInterval: 5000,
heartbeatTimeout: 15000,
searchOptions: {
type: 'multicast',
address: getMulticastGroup(1),
@@ -19,12 +20,12 @@ exports.config = {
exports.ready = function ready(device) {
const d = device;
d.data.universes = {};
d.data.priorities = {};
d.data.source = 'Unknown Source';
d.data.orderedUniverses = [];
const networkInterfaces = d.getNetworkInterfaces();
// device.draw();
for (let i = 1; i <= 16; i++) {
for (let j = 0; j < Object.keys(networkInterfaces).length; j++) {
const networkInterfaceID = Object.keys(networkInterfaces)[j];
@@ -39,11 +40,16 @@ exports.data = function data(_device, buf) {
const device = _device;
let universe = device.data.universes[universeIndex];
let priorities = device.data.priorities[universeIndex];
if (!universe) {
device.data.universes[universeIndex] = {};
universe = device.data.universes[universeIndex];
}
if (!priorities) {
device.data.priorities[universeIndex] = new Array(512).fill(0);
priorities = device.data.priorities[universeIndex];
}
universe.sequence = buf.readUInt8(111);
universe.priority = buf.readUInt8(108);
@@ -64,15 +70,20 @@ exports.data = function data(_device, buf) {
universe.slotElems = [];
universe.slotElemsSet = false;
device.draw();
device.update('elementCache');
if (universe.priority > 0) {
device.draw();
device.update('elementCache');
}
}
if (universe.priority > 0) {
device.update('universeData', {
universeIndex,
universe,
startCode: universe.startCode,
});
} else {
device.data.priorities[universeIndex] = buf.slice(126);
}
device.update('universeData', {
universeIndex,
universe,
startCode: universe.startCode,
});
};
exports.heartbeat = function heartbeat(device) {};
@@ -91,21 +102,18 @@ exports.update = function update(_device, doc, updateType, updateData) {
const $elem = doc.getElementById(`universe-${data.universeIndex}`);
if ($elem && data.universe.slotElemsSet) {
if (data.universe.priority > 0) {
for (let i = 0; i < 512; i++) {
data.universe.slotElems[i].innerText = data.universe.slots[i];
}
const $code = doc.getElementById(`universe-${data.universeIndex}-code`);
if (data.startCode === 0xdd) {
$code.innerText = 'Net3';
} else if (data.startCode === 0x17) {
$code.innerText = 'Text';
} else if (data.startCode === 0xcf) {
$code.innerText = 'SIP';
} else if (data.startCode === 0xcc) {
$code.innerText = 'RDM';
}
for (let i = 0; i < 512; i++) {
data.universe.slotElems[i].textContent = data.universe.slots[i];
}
const $code = doc.getElementById(`universe-${data.universeIndex}-code`);
if (data.startCode === 0xdd) {
$code.textContent = 'Net3';
} else if (data.startCode === 0x17) {
$code.textContent = 'Text';
} else if (data.startCode === 0xcf) {
$code.textContent = 'SIP';
} else if (data.startCode === 0xcc) {
$code.textContent = 'RDM';
}
} else {
device.draw();
@@ -113,10 +121,15 @@ exports.update = function update(_device, doc, updateType, updateData) {
}
} else if (updateType === 'elementCache') {
device.data.orderedUniverses.forEach((universeIndex) => {
for (let i = 0; i < 512; i++) {
device.data.universes[universeIndex].slotElems[i] = doc.getElementById(`${universeIndex}-${i}`);
const universe = device.data.universes[universeIndex];
if (doc.getElementById(`${universeIndex}-0`)) {
for (let i = 0; i < 512; i++) {
universe.slotElems[i] = doc.getElementById(`${universeIndex}-${i}`);
universe.slotElems[i].title = `${universeIndex}/${i} ${device.data.priorities[universeIndex][i]}`;
}
universe.slotElemsSet = true;
}
device.data.universes[universeIndex].slotElemsSet = true;
});
}
};
+2 -2
View File
@@ -3,10 +3,10 @@ const Channel = require('./channel');
exports.config = {
defaultName: 'Shure Wireless',
connectionType: 'TCPsocket',
heartbeatInterval: 5000,
heartbeatTimeout: 10000,
defaultPort: 2202,
mayChangePort: false,
heartbeatInterval: 5000,
heartbeatTimeout: 10000,
searchOptions: {
type: 'TCPport',
searchBuffer: Buffer.from('< GET DEVICE_ID >', 'ascii'),
+2 -1
View File
@@ -1,9 +1,10 @@
exports.config = {
defaultName: 'Dataton Watchout',
connectionType: 'TCPsocket',
heartbeatInterval: 250,
defaultPort: 3040,
mayChangePort: false,
heartbeatInterval: 250,
heartbeatTimeout: 5000,
searchOptions: {
type: 'TCPport',
searchBuffer: Buffer.from('authenticate 1\n', 'ascii'),
+44 -46
View File
@@ -1,24 +1,25 @@
exports.config = {
defaultName: 'X32 Mixer',
connectionType: 'osc-udp',
heartbeatInterval: 9000,
defaultPort: 10023,
mayChangePort: false,
heartbeatInterval: 9000,
heartbeatTimeout: 11000,
searchOptions: {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]),
devicePort: 10023,
listenPort: 0,
validateResponse(msg, info) {
return msg.toString().includes('/xinfo') === 0;
return msg.toString().includes('/xinfo');
},
},
};
exports.ready = function ready(device) {
const d = device;
d.data.X32 = new Console();
d.send('/xinfo');
exports.ready = function ready(_device) {
const device = _device;
device.data = new Console();
device.send('/xinfo');
device.send('/batchsubscribe', [
{ type: 's', value: '/ch/meters' },
@@ -43,25 +44,25 @@ function parseAddress(msg) {
return addr;
}
exports.data = function data(device, oscData) {
this.deviceInfoUpdate(device, 'status', 'ok');
exports.data = function data(_device, oscData) {
this.deviceInfoUpdate(_device, 'status', 'ok');
const d = device;
const device = _device;
if (oscData.address === '/xinfo') {
d.data.X32.info.name = oscData.args[1];
d.data.X32.info.ip = oscData.args[0];
d.data.X32.info.firmware = oscData.args[3];
d.data.X32.info.model = oscData.args[2];
device.data.info.name = oscData.args[1];
device.data.info.ip = oscData.args[0];
device.data.info.firmware = oscData.args[3];
device.data.info.model = oscData.args[2];
this.deviceInfoUpdate(device, 'defaultName', d.data.X32.info.name);
this.deviceInfoUpdate(_device, 'defaultName', device.data.info.name);
d.send('/main/st/config/name');
device.send('/main/st/config/name');
for (let i = 0; i <= 32; i++) {
d.send(`/ch/${i.toString().padStart(2, '0')}/config/name`);
device.send(`/ch/${i.toString().padStart(2, '0')}/config/name`);
}
d.draw();
device.draw();
} else if (oscData.address.includes('/ch/meters')) {
const buf = Buffer.from(oscData.args[0]);
@@ -69,12 +70,12 @@ exports.data = function data(device, oscData) {
for (let i = 0; i < 70; i++) {
if (i >= 0 && i < 32) {
// These are channel meters
d.data.X32.inputs.channels[i].meter = Console.getBehringerDB(buf.readFloatLE(offset));
device.data.inputs.channels[i].meter = Console.getBehringerDB(buf.readFloatLE(offset));
}
offset += 4;
}
d.draw();
device.draw();
} else if (oscData.address.includes('/main/meters')) {
const buf = Buffer.from(oscData.args[0]);
let offset = 4; // skip first 4 bytes they are the length bytes
@@ -82,10 +83,10 @@ exports.data = function data(device, oscData) {
for (let i = 0; i < 49; i++) {
if (i === 22) {
// STEREO LEFT METER
d.data.X32.main.stereo.meter[0] = Console.getBehringerDB(buf.readFloatLE(offset));
device.data.main.stereo.meter[0] = Console.getBehringerDB(buf.readFloatLE(offset));
} else if (i === 23) {
// STEREO RIGHT METER
d.data.X32.main.stereo.meter[1] = Console.getBehringerDB(buf.readFloatLE(offset));
device.data.main.stereo.meter[1] = Console.getBehringerDB(buf.readFloatLE(offset));
}
offset += 4;
}
@@ -94,56 +95,53 @@ exports.data = function data(device, oscData) {
if (addr[0] === 'ch') {
const channel = Number(addr[1]);
d.data.X32.inputs.channels[channel - 1].fader = oscData.args[0];
d.data.X32.inputs.channels[channel - 1].faderDB = Console.getBehringerDB(oscData.args[0]);
device.data.inputs.channels[channel - 1].fader = oscData.args[0];
device.data.inputs.channels[channel - 1].faderDB = Console.getBehringerDB(oscData.args[0]);
} else if (addr[0] === 'main') {
d.data.X32.main.stereo.fader = oscData.args[0];
d.data.X32.main.stereo.faderDB = Console.getBehringerDB(oscData.args[0]);
device.data.main.stereo.fader = oscData.args[0];
device.data.main.stereo.faderDB = Console.getBehringerDB(oscData.args[0]);
}
d.draw();
device.draw();
} else if (oscData.address.includes('/mix/on')) {
const addr = parseAddress(oscData.address);
if (addr[0] === 'ch') {
const channel = Number(addr[1]);
d.data.X32.inputs.channels[channel - 1].mute = oscData.args[0];
d.send(`/ch/${addr[1]}/mix/fader`);
device.data.inputs.channels[channel - 1].mute = oscData.args[0];
device.send(`/ch/${addr[1]}/mix/fader`);
} else if (addr[0] === 'main') {
d.data.X32.main.stereo.mute = oscData.args[0];
d.send(`/main/${addr[1]}/mix/fader`);
device.data.main.stereo.mute = oscData.args[0];
device.send(`/main/${addr[1]}/mix/fader`);
}
d.draw();
device.draw();
} else if (oscData.address.includes('/config/name')) {
const addr = parseAddress(oscData.address);
if (addr[0] === 'main') {
if (addr[1] === 'st') {
d.data.X32.main.stereo.name = oscData.args[0];
if (d.data.X32.main.stereo.name === '') {
d.data.X32.main.stereo.name = 'LR';
device.data.main.stereo.name = oscData.args[0];
if (device.data.main.stereo.name === '') {
device.data.main.stereo.name = 'LR';
}
d.send(`/main/${addr[1]}/config/color`);
device.send(`/main/${addr[1]}/config/color`);
}
} else if (addr[0] === 'ch') {
const channel = Number(addr[1]);
d.data.X32.inputs.channels[channel - 1].name = oscData.args[0];
d.send(`/ch/${addr[1]}/config/color`);
device.data.inputs.channels[channel - 1].name = oscData.args[0];
device.send(`/ch/${addr[1]}/config/color`);
}
d.draw();
device.draw();
} else if (oscData.address.includes('/config/color')) {
const addr = parseAddress(oscData.address);
if (addr[0] === 'main') {
d.data.X32.main.stereo.color = oscData.args[0];
d.send(`/main/${addr[1]}/mix/on`);
device.data.main.stereo.color = oscData.args[0];
device.send(`/main/${addr[1]}/mix/on`);
} else if (addr[0] === 'ch') {
const channel = Number(addr[1]);
d.data.X32.inputs.channels[channel - 1].color = oscData.args[0];
d.send(`/ch/${addr[1]}/mix/on`);
device.data.inputs.channels[channel - 1].color = oscData.args[0];
device.send(`/ch/${addr[1]}/mix/on`);
}
d.draw();
} else {
// console.log(oscData);
device.draw();
}
// console.log(msg)
};
exports.heartbeat = function heartbeat(device) {
+1 -7
View File
@@ -30,7 +30,6 @@ tr td:first-child {
padding: 1px 6px;
border-radius: 10px;
color: black;
font-family: PlexMono !important;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
@@ -136,12 +135,7 @@ input[type='range']::-webkit-slider-runnable-track {
.meter {
height: 3px;
background: linear-gradient(
90deg,
rgba(19, 126, 30, 1) 0%,
rgba(255, 238, 30, 1) 85%,
rgba(255, 0, 0, 1) 100%
);
background: linear-gradient(90deg, rgba(19, 126, 30, 1) 0%, rgba(255, 238, 30, 1) 85%, rgba(255, 0, 0, 1) 100%);
}
/* need to find a way to match the color of this with the table row */
+14 -14
View File
@@ -1,6 +1,6 @@
<header>
<h1><%= listName %></h1>
<h2><%= data.model %></h2>
<h2><%= data.info.model %></h2>
</header>
<table class="cv-table">
@@ -13,15 +13,15 @@
</tr>
<tr>
<td width="40px">LR</td>
<td width="100px"><div class="color color-<%= data.X32.main.stereo.color %>"><%- data.X32.main.stereo.name%></div></td>
<td><div class="fader-mute mute-<%=data.X32.main.stereo.mute%>">M</div></td>
<td><%= formatAsDB(data.X32.main.stereo.faderDB) %></td>
<td width="100px"><div class="color color-<%= data.main.stereo.color %>"><%- data.main.stereo.name%></div></td>
<td><div class="fader-mute mute-<%=data.main.stereo.mute%>">M</div></td>
<td><%= formatAsDB(data.main.stereo.faderDB) %></td>
<td>
<% let style = `style=width:${Math.abs(data.X32.main.stereo.meter[0] - 10)}%`; %>
<% let style = `style=width:${Math.abs(data.main.stereo.meter[0] - 10)}%`; %>
<div class="meter">
<div class="meter-cover" <%= style %>></div>
</div>
<% style = `style=width:${Math.abs(data.X32.main.stereo.meter[1] - 10)}%`; %>
<% style = `style=width:${Math.abs(data.main.stereo.meter[1] - 10)}%`; %>
<div class="meter">
<div class="meter-cover" <%= style %>></div>
</div>
@@ -29,22 +29,22 @@
type="range"
min="0"
max="100"
value="<%= data.X32.main.stereo.fader*100 %>"
value="<%= data.main.stereo.fader*100 %>"
disabled />
</td>
</tr>
<% for(var i=0; i<32; i++){ %> <% if(data.X32.inputs.channels[i].name == "end"){break;} %>
<% for(var i=0; i<32; i++){ %> <% if(data.inputs.channels[i].name == "end"){break;} %>
<tr>
<td><%= i+1 %></td>
<td>
<div class="color color-<%= data.X32.inputs.channels[i].color %>">
<%- data.X32.inputs.channels[i].name || i+1 %>
<div class="color color-<%= data.inputs.channels[i].color %>">
<%- data.inputs.channels[i].name || i+1 %>
</div>
</td>
<td><div class="fader-mute mute-<%=data.X32.inputs.channels[i].mute%>">M</div></td>
<td><%= formatAsDB(data.X32.inputs.channels[i].faderDB) %></td>
<td><div class="fader-mute mute-<%=data.inputs.channels[i].mute%>">M</div></td>
<td><%= formatAsDB(data.inputs.channels[i].faderDB) %></td>
<td>
<% let style = `style=width:${Math.abs(data.X32.inputs.channels[i].meter - 10)}%`; %>
<% let style = `style=width:${Math.abs(data.inputs.channels[i].meter - 10)}%`; %>
<div class="meter">
<div class="meter-cover" <%= style %>></div>
</div>
@@ -52,7 +52,7 @@
type="range"
min="0"
max="100"
value="<%= data.X32.inputs.channels[i].fader*100 %>"
value="<%= data.inputs.channels[i].fader*100 %>"
disabled />
</td>
</tr>
+2 -5
View File
@@ -1,9 +1,10 @@
exports.config = {
defaultName: 'X Air Mixer',
connectionType: 'UDPsocket',
heartbeatInterval: 10000,
defaultPort: 10024,
mayChangePort: false,
heartbeatInterval: 9000,
heartbeatTimeout: 15000,
searchOptions: {
type: 'UDPsocket',
searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]),
@@ -28,10 +29,6 @@ exports.ready = function ready(device) {
d.data.stereoMute = 0;
d.send('/xinfo');
// device.send(Buffer.from("\x2f\x62\x61\x74\x63\x68\x73\x75\x62\x73\x63\x72\x69\x62\x65\x00\x2c\x73\x73\x69\x69\x69\x00\x00\x6d\x65\x74\x65\x72\x73\x2f\x30\x00\x00\x00\x00\x2f\x6d\x65\x74\x65\x72\x73\x2f\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"));
// device.send(Buffer.from("/batchsubscribe\x00,ssiii\x00\x00meters/0\x00\x00\x00\x00/meters/0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"));
// device.send(Buffer.from("/subscribe\x00,si\x00/-stat/solosw/01\x001"));
};
function parseAddress(msg) {
+10 -6
View File
@@ -95,14 +95,18 @@ window.init = function init() {
};
document.getElementById('add-device-button').onchange = function addDeviceClick(e) {
DEVICE.registerDevice({
type: e.target.value,
defaultName: 'New Device',
port: undefined,
addresses: [],
});
const newDevice = DEVICE.registerDevice(
{
type: e.target.value,
defaultName: 'New Device',
port: undefined,
addresses: [],
},
'fromAddButton'
);
e.target.selectedIndex = 0;
VIEW.switchDevice(newDevice.id);
SAVESLOTS.saveAll();
};
+2 -2
View File
@@ -317,8 +317,8 @@ select.button:focus {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('img/Material Icons'), local('img/MaterialIcons-Regular'),
url(img/MaterialIcons-Regular.ttf) format('truetype');
src: local('Material Icons'), local('MaterialIcons-Regular'),
url(../font/MaterialIcons-Regular.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';

Before

Width:  |  Height:  |  Size: 401 KiB

After

Width:  |  Height:  |  Size: 401 KiB

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Before

Width:  |  Height:  |  Size: 224 B

After

Width:  |  Height:  |  Size: 224 B

Before

Width:  |  Height:  |  Size: 138 B

After

Width:  |  Height:  |  Size: 138 B

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

Before

Width:  |  Height:  |  Size: 212 B

After

Width:  |  Height:  |  Size: 212 B

Before

Width:  |  Height:  |  Size: 171 B

After

Width:  |  Height:  |  Size: 171 B

Before

Width:  |  Height:  |  Size: 210 B

After

Width:  |  Height:  |  Size: 210 B

Before

Width:  |  Height:  |  Size: 236 B

After

Width:  |  Height:  |  Size: 236 B

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 415 B

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 383 B

+55 -38
View File
@@ -2,8 +2,7 @@ const { v4: uuid } = require('uuid');
const osc = require('osc');
const net = require('net');
const udp = require('dgram');
const _ = require('lodash');
const { Atem } = require('atem-connection');
const PLUGINS = require('./plugins.js');
const VIEW = require('./view.js');
const SAVESLOTS = require('./saveSlots.js');
@@ -12,7 +11,7 @@ const SEARCH = require('./search.js');
const devices = {};
module.exports.all = devices;
function registerDevice(newDevice) {
function registerDevice(newDevice, discoveryMethod) {
if (PLUGINS.all[newDevice.type] === undefined) {
console.error(`Plugin for device ${newDevice.type} does not exist.`);
return true;
@@ -23,21 +22,11 @@ function registerDevice(newDevice) {
initElements[i].style.display = 'none';
}
// only register device if it hasn't already been added
if (newDevice.addresses.length > 0) {
const existing = _.find(devices, (e) => {
const typeMatch = e.type === newDevice.type;
const addressMatch = JSON.stringify(e.addresses) === JSON.stringify(newDevice.addresses);
return typeMatch && addressMatch;
});
if (existing) {
return false;
}
// prevent duplicate devices from being added via search
if (isDeviceAlreadyAdded(newDevice) && discoveryMethod === 'fromSearch') {
return false;
}
// console.log("Registered new "+newDevice.type)
const id = newDevice.id || uuid();
devices[id] = {
id,
@@ -49,11 +38,14 @@ function registerDevice(newDevice) {
port: newDevice.port,
addresses: newDevice.addresses,
data: {},
templates: {},
fields: newDevice.fields || {},
pinIndex: false,
lastDrawn: 0,
lastHeartbeat: 0,
lastMessage: 0,
heartbeatInterval: PLUGINS.all[newDevice.type].heartbeatInterval,
heartbeatTimeout: PLUGINS.all[newDevice.type].heartbeatTimeout,
draw() {
VIEW.draw(this);
},
@@ -79,7 +71,7 @@ function registerDevice(newDevice) {
VIEW.addDeviceToList(devices[id]);
initDeviceConnection(id);
return true;
return devices[id];
}
module.exports.registerDevice = registerDevice;
@@ -177,6 +169,7 @@ function initDeviceConnection(id) {
device.connection.on('message', (msg, info) => {
plugins[type].data(device, msg);
device.lastMessage = Date.now();
infoUpdate(device, 'status', 'ok');
});
});
@@ -194,13 +187,33 @@ function initDeviceConnection(id) {
device.connection.on('message', (msg, info) => {
plugins[type].data(device, msg);
device.lastMessage = Date.now();
infoUpdate(device, 'status', 'ok');
});
});
device.send = (data) => {};
} else if (plugins[type].config.connectionType === 'atem') {
device.connection = new Atem({
// this gets around the no workers nodejs error
disableMultithreaded: true,
});
device.connection.connect(device.addresses[0]);
device.connection.on('connected', () => {
infoUpdate(device, 'status', 'ok');
plugins[type].ready(device);
});
device.connection.on('stateChanged', (state, pathToChange) => {
device.lastMessage = Date.now();
plugins[type].data(device, {
pathToChange,
state,
});
infoUpdate(device, 'status', 'ok');
});
}
// device.plugin = plugins[type];
return true;
}
@@ -213,6 +226,9 @@ module.exports.deleteActive = function deleteActive() {
);
if (choice) {
if (device.plugin.connectionType === 'TCPsocket') {
device.connection.destroy();
}
VIEW.removeDeviceFromList(device);
delete devices[device.id];
SAVESLOTS.removeDevice(device);
@@ -235,7 +251,6 @@ module.exports.changeActiveType = function changeActiveType(newType) {
initDeviceConnection(device.id);
VIEW.draw(device);
VIEW.updateFields();
// SAVESLOTS.saveAll();
};
module.exports.changeActiveIP = function changeActiveIP(newIP) {
@@ -271,7 +286,6 @@ module.exports.changeActivePinIndex = function changeActivePinIndex(newPin) {
module.exports.changePinIndex = function changePinIndex(device, newPin) {
const d = device;
d.pinIndex = newPin;
// SAVESLOTS.saveAll();
};
module.exports.refreshActive = function refreshActive() {
const device = VIEW.getActiveDevice();
@@ -311,23 +325,26 @@ function heartbeat() {
d.lastHeartbeat = Date.now();
}
});
// for (let i in devices) {
// const device = devices[i];
// if (Date.now() >= device.lastHeartbeat + device.heartbeatInterval) {
// if (device.status == 'broken') {
// initDeviceConnection(i);
// } else if (Date.now() - device.lastMessage > device.heartbeatTimeout) {
// infoUpdate(device, 'status', 'broken');
// } else {
// if (device.port != undefined && device.addresses.length > 0) {
// PLUGINS.all[device.type].heartbeat(device);
// } else {
// // console.error("Invalid IP/Port on device "+device.name)
// }
// }
// device.lastHeartbeat = Date.now();
// }
// }
}
setInterval(heartbeat, 100);
function isDeviceAlreadyAdded(newDevice) {
let deviceAlreadyAdded = false;
if (newDevice.addresses.length === 0) {
return false;
}
for (let i = 0; i < Object.keys(devices).length; i++) {
const device = devices[Object.keys(devices)[i]];
const typeMatch = device.type === newDevice.type;
const addressMatch = JSON.stringify(device.addresses) === JSON.stringify(newDevice.addresses);
const idMatch = device.id === newDevice.id;
if (typeMatch && addressMatch && !idMatch) {
deviceAlreadyAdded = true;
break;
}
}
return deviceAlreadyAdded;
}
module.exports.isDeviceAlreadyAdded = isDeviceAlreadyAdded;
+2 -10
View File
@@ -35,17 +35,9 @@ module.exports.init = function init(callback) {
plugin.info = _.template(fs.readFileSync(path.join(pluginDirectoryPath, `/${pluginDir}/info.html`), 'utf8'));
if (plugin.config.heartbeatTimeout) {
plugin.heartbeatTimeout = plugin.config.heartbeatInterval * 1.5;
} else {
plugin.heartbeatTimeout = 10000;
}
plugin.heartbeatTimeout = plugin.config.heartbeatTimeout;
plugin.heartbeatInterval = plugin.config.heartbeatInterval;
if (plugin.config.heartbeatInterval) {
plugin.heartbeatInterval = Math.max(50, plugin.config.heartbeatInterval);
} else {
plugin.heartbeatInterval = 5000;
}
console.log(`${pluginDir} loaded`);
}
});
+13 -12
View File
@@ -57,15 +57,18 @@ module.exports.loadDevices = function loadDevices() {
console.log(`Loading ${savedDevices.length} saved devices...`);
for (let i = 0; i < savedDevices.length; i++) {
DEVICE.registerDevice({
type: savedDevices[i].type,
displayName: savedDevices[i].displayName,
defaultName: savedDevices[i].defaultName,
port: savedDevices[i].port,
addresses: savedDevices[i].addresses,
id: savedDevices[i].id,
fields: savedDevices[i].fields,
});
DEVICE.registerDevice(
{
type: savedDevices[i].type,
displayName: savedDevices[i].displayName,
defaultName: savedDevices[i].defaultName,
port: savedDevices[i].port,
addresses: savedDevices[i].addresses,
id: savedDevices[i].id,
fields: savedDevices[i].fields,
},
'fromSave'
);
}
};
@@ -96,9 +99,7 @@ module.exports.saveAll = function saveAll() {
});
localStorage.setItem('savedSlots', JSON.stringify(savedSlots));
console.log(
`Saved ${currentPins.length} pinned devices to slot ${activeSlot}!`
);
console.log(`Saved ${currentPins.length} pinned devices to slot ${activeSlot}!`);
savedDevices = [];
let i = 0;
+69 -24
View File
@@ -5,6 +5,7 @@ const net = require('net');
const os = require('os');
const ip = require('ip');
const { Netmask } = require('netmask');
const DEVICE = require('./device.js');
const PLUGINS = require('./plugins.js');
@@ -91,6 +92,8 @@ function searchAll() {
searchUDP(pluginType, plugin.config);
} else if (searchType === 'multicast') {
searchMulticast(pluginType, plugin.config);
} else if (searchType === 'UDPScan') {
searchUDPScan(pluginType, plugin.config);
}
} catch (err) {
console.error(`Unable to search for plugin ${pluginType}`);
@@ -123,12 +126,15 @@ function searchBonjour(pluginType, pluginConfig) {
}
});
DEVICE.registerDevice({
type: pluginType,
defaultName: e.name,
port: e.port,
addresses: validAddresses,
});
DEVICE.registerDevice(
{
type: pluginType,
defaultName: e.name,
port: e.port,
addresses: validAddresses,
},
'fromSearch'
);
});
}
@@ -147,12 +153,15 @@ function TCPtest(ipAddr, pluginType, pluginConfig) {
client.end(
'',
'utf8',
DEVICE.registerDevice({
type: pluginType,
defaultName: pluginConfig.defaultName,
port: pluginConfig.defaultPort,
addresses: [ipAddr],
})
DEVICE.registerDevice(
{
type: pluginType,
defaultName: pluginConfig.defaultName,
port: pluginConfig.defaultPort,
addresses: [ipAddr],
},
'fromSearch'
)
);
}
});
@@ -161,6 +170,36 @@ function TCPtest(ipAddr, pluginType, pluginConfig) {
});
}
function searchUDPScan(pluginType, pluginConfig) {
for (let i = 0; i < Object.keys(validInterfaces).length; i++) {
const interfaceID = Object.keys(validInterfaces)[i];
const interfaceObj = validInterfaces[interfaceID];
interfaceObj.forEach((netInterface) => {
const udpSocket = dgram.createSocket('udp4');
udpSocket.bind(pluginConfig.searchOptions.listenPort, netInterface.address);
udpSocket.on('message', (msg, info) => {
if (pluginConfig.searchOptions.validateResponse(msg, info, DEVICE.all)) {
udpSocket.close();
DEVICE.registerDevice(
{
type: pluginType,
defaultName: pluginConfig.defaultName,
port: pluginConfig.defaultPort,
addresses: [info.address],
},
'fromSearch'
);
}
});
const interfaceBlock = new Netmask(netInterface.cidr);
interfaceBlock.forEach((address, long, index) => {
udpSocket.send(pluginConfig.searchOptions.searchBuffer, pluginConfig.searchOptions.devicePort, address);
});
});
}
}
function searchUDP(pluginType, pluginConfig) {
for (let i = 0; i < Object.keys(validInterfaces).length; i++) {
const interfaceID = Object.keys(validInterfaces)[i];
@@ -172,12 +211,15 @@ function searchUDP(pluginType, pluginConfig) {
searchSockets[j].on('message', (msg, info) => {
if (pluginConfig.searchOptions.validateResponse(msg, info, DEVICE.all)) {
searchSockets[j].close();
DEVICE.registerDevice({
type: pluginType,
defaultName: pluginConfig.defaultName,
port: pluginConfig.defaultPort,
addresses: [info.address],
});
DEVICE.registerDevice(
{
type: pluginType,
defaultName: pluginConfig.defaultName,
port: pluginConfig.defaultPort,
addresses: [info.address],
},
'fromSearch'
);
}
});
});
@@ -201,12 +243,15 @@ function searchMulticast(pluginType, pluginConfig) {
socket.on('message', (msg, info) => {
if (pluginConfig.searchOptions.validateResponse(msg, info)) {
socket.close(() => {
DEVICE.registerDevice({
type: pluginType,
defaultName: pluginConfig.defaultName,
port: pluginConfig.defaultPort,
addresses: [info.address],
});
DEVICE.registerDevice(
{
type: pluginType,
defaultName: pluginConfig.defaultName,
port: pluginConfig.defaultPort,
addresses: [info.address],
},
'fromSearch'
);
});
}
});
+50 -43
View File
@@ -2,7 +2,6 @@ const { ipcRenderer } = require('electron');
const DEVICE = require('./device.js');
const PLUGINS = require('./plugins.js');
const { saveAll } = require('./saveSlots.js');
// const _ = require('lodash/function');
const pinnedDevices = [];
module.exports.pinnedDevices = pinnedDevices;
@@ -22,31 +21,34 @@ function drawDeviceFrame(id) {
const d = DEVICE.all[id];
let str = '<html><head>';
str += `<link href='./plugins/${d.type}/styles.css' rel='stylesheet' type='text/css'>`;
const str = `
<html>
<head>
<link href='./plugins/${d.type}/styles.css' rel='stylesheet' type='text/css'>
// scrollbar styles are inline to prevent the styles flickering in
str += '<style>';
str += '::-webkit-scrollbar {background-color: black;width: 12px;}';
str += '::-webkit-scrollbar-track, ::-webkit-scrollbar-corner {background-color: #2b2b2b;}';
str += '::-webkit-scrollbar-thumb {background-color: #6b6b6b;border-radius: 16px;border: 3px solid #2b2b2b;}';
str += '::-webkit-scrollbar-button {display:none;}';
str += 'body{visibility: hidden;}';
str += '</style>';
str += "<link href='src/defaultPlugin.css' rel='stylesheet' type='text/css'>";
str += '</head><body>';
str += generateBodyHTML(d);
// str += "<script src='node_modules/lodash/core.min.js'></script>";
// str += "<script src='./plugins/" +d.type +"/update.js'></script>";
str += '</body></html>';
<!--scrollbar styles are inline to prevent the styles flickering in-->
<style>
::-webkit-scrollbar {background-color: black;width: 12px;}
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner {background-color: #2b2b2b;}
::-webkit-scrollbar-thumb {background-color: #6b6b6b;border-radius: 16px;border: 3px solid #2b2b2b;}
::-webkit-scrollbar-button {display:none;}
body{visibility: hidden;}
</style>
<link href='src/assets/css/plugin_default.css' rel='stylesheet' type='text/css'>
</head>
<body>
${generateBodyHTML(d)}
</body>
</html>
`;
$deviceDrawArea.setAttribute('class', `${d.type} draw-area`);
$deviceDrawArea.contentWindow.document.open();
$deviceDrawArea.contentWindow.document.write(str);
$deviceDrawArea.contentWindow.document.close();
$deviceDrawArea.contentWindow.document.onclick = function (e) {
switchDevice(d.id);
};
if (d.pinIndex) {
$devicePinned.style.display = 'block';
@@ -58,29 +60,34 @@ function drawDeviceFrame(id) {
}
function generateBodyHTML(d) {
let str = '';
if (d.status === 'ok') {
try {
str += PLUGINS.all[d.type].template({
return PLUGINS.all[d.type].template({
templates: d.templates,
data: d.data,
listName: d.displayName || d.defaultName,
});
} catch (err) {
console.log(err);
str += '<h3>Plugin Template Error</h3>';
return '<h3>Plugin Template Error</h3>';
}
} else {
str += `<header><h1>${d.displayName || d.defaultName}</h1></header>`;
str += "<div class='not-responding'>";
str += `<h2><em>${d.type}</em> is not responding to requests for data.</h2>`;
str += `<h3>IP <em>${d.addresses[0]}</em></h3>`;
str += `<h3>Port <em>${d.port}</em></h3>`;
str += '<hr></div>';
str += `<div class="device-info">${PLUGINS.all[d.type].info()}<div>`;
}
return `
<header>
<h1>${d.displayName || d.defaultName}</h1>
</header>
return str;
<div class='not-responding'>
<h2><em>${d.type}</em> is not responding to requests for data.</h2>
<h3>IP <em>${d.addresses[0]}</em></h3>
<h3>Port <em>${d.port}</em></h3>
<hr>
</div>
<div class="device-info">
${PLUGINS.all[d.type].info()}
</div>
`;
}
}
module.exports.draw = function draw(device) {
@@ -115,8 +122,10 @@ module.exports.addDeviceToList = function addDeviceToList(device) {
html += "<div class='status material-icons red'>clear</div>";
}
html += `<div class='type'><img height='18px' src='plugins/${d.type}/icon.png'></div>`;
html += `<div class='name'>${d.displayName || d.defaultName}</div>`;
html += `
<div class='type'><img height='18px' src='plugins/${d.type}/icon.png'></div>
<div class='name'>${d.displayName || d.defaultName}</div>
`;
const elem = document.getElementById(d.id);
if (elem == null) {
@@ -151,23 +160,19 @@ function switchDevice(id) {
document.getElementById('all-devices').style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
if (id === undefined) {
// document.getElementById('refresh-device-button').style.opacity = 0.2;
document.getElementById('refresh-device-button').disabled = true;
document.getElementById('device-settings-table').style.display = 'none';
return;
}
document.getElementById('refresh-device-button').disabled = false;
// document.getElementById('refresh-device-button').style.opacity = 1;
const i = DEVICE.all[id].id;
let $deviceWrapper = document.getElementById(`device-${i}`);
const $deviceWrapper = document.getElementById(`device-${i}`);
if (!$deviceWrapper) {
const html = `<div class="col device-wrapper" id="device-${i}"><img id="device-${i}-pinned" class="device-pin" src="src/img/outline_push_pin_white_18dp.png"><iframe id="device-${i}-draw-area" class="draw-area"></iframe></div>`;
const html = `<div class="col device-wrapper" id="device-${i}"><img id="device-${i}-pinned" class="device-pin" src="src/assets/img/outline_push_pin_white_18dp.png"><iframe id="device-${i}-draw-area" class="draw-area"></iframe></div>`;
document.getElementById('all-devices').insertAdjacentHTML('afterbegin', html);
$deviceWrapper = document.getElementById(`device-${i}`);
}
window.switchClass(document.getElementById(id), 'active-device');
@@ -201,9 +206,9 @@ function updateFields() {
const fields = activeDevice.plugin.config.fields;
fields.forEach((field) => {
// generic input setup
const $elem = document.createElement('input');
$elem.type = 'text';
$elem.value = activeDevice.fields[field.key]; // || field.value;
$elem.value = activeDevice.fields[field.key];
$elem.name = field.key;
$elem.onchange = function onchange(e) {
activeDevice.fields[field.key] = $elem.value;
@@ -211,7 +216,9 @@ function updateFields() {
saveAll();
};
// type specific setup
if (field.type === 'textinput') {
$elem.type = 'text';
const rowHTML = `<tr><th>${field.label}:</th><td colspan="3" id="${field.key}"></td></tr>`;
document.getElementById('device-settings-fields').insertAdjacentHTML('beforeend', rowHTML);
document.getElementById(field.key).appendChild($elem);