Merge branch 'main' into plugin-sennheiser-wireless
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
@@ -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>)
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"> </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"> </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"> </div>
|
||||
</div>
|
||||
<div id="me-<%= meIndex %>-ftb" class="atem-input">
|
||||
<div class="source-label">FTB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
@@ -2,17 +2,15 @@
|
||||
|
||||
<h4>Shell/ECU → Network</h4>
|
||||
<ul>
|
||||
<li>
|
||||
Enable <em>✔ TCP OSC</em>. The TCP format dropdown does not matter.
|
||||
</li>
|
||||
<li>Enable <em>✔ 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 → Show Control → OSC</h4>
|
||||
<ul>
|
||||
<li>Enable <em>✔ OSC RX</em></li>
|
||||
<li>Enable <em>✔ 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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} — ${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`);
|
||||
|
||||
@@ -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"){ %>
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||