diff --git a/plugins/scs/icon.png b/plugins/scs/icon.png
new file mode 100644
index 0000000..383037a
Binary files /dev/null and b/plugins/scs/icon.png differ
diff --git a/plugins/scs/info.html b/plugins/scs/info.html
new file mode 100644
index 0000000..9863b00
--- /dev/null
+++ b/plugins/scs/info.html
@@ -0,0 +1,4 @@
+
Connection Requirements
+
+ - Network Protocol must be set to TCP under Remote App Interface settings
+
diff --git a/plugins/scs/main.js b/plugins/scs/main.js
new file mode 100644
index 0000000..638a545
--- /dev/null
+++ b/plugins/scs/main.js
@@ -0,0 +1,187 @@
+exports.config = {
+ defaultName: 'Show Cue Systems',
+ connectionType: 'TCPsocket',
+ defaultPort: 58000,
+ mayChangePort: true,
+ heartbeatInterval: 5000,
+ searchOptions: {
+ type: 'TCPport',
+ searchBuffer: Buffer.from('/_status', 'ascii'),
+ testPort: 58000,
+ validateResponse(msg, info) {
+ return msg.toString().includes('/_status');
+ },
+ },
+ fields: [],
+};
+
+const typeMap = {
+ A: 'Video/Image',
+ E: 'Memo',
+ F: 'Audio File',
+ G: 'GoTo Cue',
+ H: 'Multi-File Cue',
+ I: 'Live Input',
+ J: 'Enable/Disable',
+ K: 'Lighting',
+ L: 'Level Change',
+ M: 'Control Send',
+ N: 'Note',
+ P: 'Playlist',
+ Q: 'Call Cue',
+ R: 'Run Program',
+ S: 'SFR',
+ T: 'Set Position',
+ U: 'MIDI Time Code',
+};
+
+const versionMap = {
+ 10: 'LITE',
+ 20: 'STD',
+ 30: 'PRO',
+ 40: 'PLUS',
+ 45: 'DEMO',
+ 50: 'PLAT',
+};
+
+exports.ready = function ready(_device) {
+ const device = _device;
+ console.log('Show Cue Systems ready');
+ device.data.cues = [];
+ device.send(Buffer.from('/_prod/gettitle'));
+};
+
+exports.data = function data(_device, message) {
+ const device = _device;
+ this.deviceInfoUpdate(device, 'status', 'ok');
+ const msg = message.toString();
+ const messagePath = msg.substring(0, msg.indexOf(' '));
+ if (messagePath === '/_prod/gettitle') {
+ const title = msg.replace(messagePath, '').replaceAll('"', '').trim();
+ this.deviceInfoUpdate(device, 'defaultName', title);
+ device.send(Buffer.from('/_info/scsversion'));
+ } else if (messagePath === '/_info/scsversion') {
+ const versionInfo = msg.replace(messagePath, '').trim().split(' ');
+ const versionNumber = versionInfo[0];
+ const versionMajor = versionNumber.substring(0, 2).replace('0', '');
+ const versionMinor = versionNumber.substring(2, 4).replace('0', '');
+ const versionPatch = versionNumber.substring(4, 6).replace('0', '');
+ const versionString = `${versionMajor}.${versionMinor}.${versionPatch}`;
+
+ const versionType = versionMap[versionInfo[1]];
+ device.data.version = {
+ number: versionString,
+ type: versionType,
+ };
+ device.send(Buffer.from('/_info/finalcue', 'ascii'));
+ } else if (messagePath === '/_info/finalcue') {
+ const cueCount = parseInt(msg.replace(messagePath, '').trim(), 10);
+ device.data.cues = new Array(cueCount).fill(0).map(() => ({
+ label: '',
+ page: '',
+ description: '',
+ type: { code: '', display: '' },
+ state: '',
+ activation: '',
+ file_info: '',
+ length: '',
+ colors: ['', ''],
+ }));
+ device.send(Buffer.from(`/_cue/getitemsn 1 QNTCLSPARZ`));
+ } else if (messagePath === '/_info/getcue') {
+ const cueLabelInfo = msg.replace(messagePath, '').trim().split(' ');
+ const cueNumber = parseInt(cueLabelInfo[0], 10);
+ const cueLabel = cueLabelInfo[1].replaceAll('"', '');
+
+ device.data.cues[cueNumber - 1].label = cueLabel;
+ device.draw();
+ } else if (messagePath === '/_cue/getpage') {
+ const pageInfo = msg.replace(messagePath, '').trim().match(/".*?"/g);
+ const cueLabel = pageInfo[0].replaceAll('"', '');
+ const cueIndex = device.data.cues.map((cue) => cue.label).indexOf(cueLabel);
+ device.data.cues[cueIndex].page = pageInfo[1].replaceAll('"', '');
+ if (cueIndex < device.data.cues.length - 1) {
+ device.send(Buffer.from(`/_cue/getpage ${device.data.cues[cueIndex + 1].label}`));
+ } else {
+ // console.log('done loading cue pages');
+ device.draw();
+ device.send(Buffer.from('/_status'));
+ }
+ } else if (messagePath === '/_cue/getname') {
+ const nameInfo = msg.replace(messagePath, '').trim().match(/".*?"/g);
+ const cueLabel = nameInfo[0].replaceAll('"', '');
+ const cueIndex = device.data.cues.map((cue) => cue.label).indexOf(cueLabel);
+
+ device.data.cues[cueIndex].description = nameInfo[1].replaceAll('"', '');
+ device.draw();
+ } else if (messagePath === '/_cue/gettype') {
+ const typeInfo = msg.replace(messagePath, '').trim().match(/".*?"/g);
+ const cueLabel = typeInfo[0].replaceAll('"', '');
+ const cueIndex = device.data.cues.map((cue) => cue.label).indexOf(cueLabel);
+
+ device.data.cues[cueIndex].type = {
+ code: typeInfo[1].replaceAll('"', ''),
+ display: typeMap[typeInfo[1].replaceAll('"', '')],
+ };
+ device.draw();
+ } else if (messagePath === '/_cue/getitemsn') {
+ const itemParts = msg
+ .replace(messagePath, '')
+ .trim('')
+ .match(/".*?"|\d+/g);
+ if (itemParts.length === 12) {
+ const cueIndex = itemParts[0] - 1;
+ device.data.cues[cueIndex].label = sanitizeSCSString(itemParts[2]);
+ device.data.cues[cueIndex].description = sanitizeSCSString(itemParts[3]);
+ device.data.cues[cueIndex].type = {
+ code: sanitizeSCSString(itemParts[4]),
+ display: typeMap[sanitizeSCSString(itemParts[4])],
+ };
+ device.data.cues[cueIndex].colors = sanitizeSCSString(itemParts[5]).split(', ');
+ device.data.cues[cueIndex].length = millisToString(itemParts[6]);
+ device.data.cues[cueIndex].state = itemParts[7];
+ device.data.cues[cueIndex].position = itemParts[8];
+ device.data.cues[cueIndex].activation = sanitizeSCSString(itemParts[9]);
+ device.data.cues[cueIndex].repeat = itemParts[10];
+ device.data.cues[cueIndex].loop = itemParts[11];
+ device.draw();
+
+ if (cueIndex < device.data.cues.length - 1) {
+ device.send(Buffer.from(`/_cue/getitemsn ${cueIndex + 2} QNTCLSPARZ`));
+ } else {
+ // console.log('done loading cue items');
+ device.send(Buffer.from(`/_cue/getpage ${device.data.cues[0].label}`));
+ }
+ } else {
+ console.error('bad response from getitems');
+ }
+ } else if (messagePath === '/_status') {
+ const status = parseInt(msg.split(' ')[1].trim(), 10);
+ console.log(`status: ${status}`);
+ } else {
+ console.error(`unhandled message path: ${messagePath}`);
+ }
+};
+
+function sanitizeSCSString(string) {
+ return string.substring(1, string.length - 1);
+}
+
+function millisToString(duration) {
+ const milliseconds = parseInt(duration % 1000, 10);
+ let seconds = parseInt((duration / 1000) % 60, 10);
+ const minutes = parseInt((duration / (1000 * 60)) % 60, 10);
+
+ if (minutes === 0) {
+ return `${seconds}.${milliseconds}`;
+ }
+ if (seconds === 0) {
+ return `0.${milliseconds}`;
+ }
+ if (seconds < 10) {
+ seconds = `0${seconds}`;
+ }
+ return `${minutes}:${seconds}.${milliseconds}`;
+}
+
+exports.heartbeat = function heartbeat(device) {};
diff --git a/plugins/scs/styles.css b/plugins/scs/styles.css
new file mode 100644
index 0000000..35b357a
--- /dev/null
+++ b/plugins/scs/styles.css
@@ -0,0 +1,12 @@
+table {
+ background: white;
+}
+
+th {
+ text-align: left;
+ font-weight: normal;
+}
+
+.header-row {
+ background: white;
+}
diff --git a/plugins/scs/template.ejs b/plugins/scs/template.ejs
new file mode 100644
index 0000000..0d4ac32
--- /dev/null
+++ b/plugins/scs/template.ejs
@@ -0,0 +1,25 @@
+
+
+
+ <% data.cues.forEach((cue)=>{ %>
+
+ | <%=cue.label%> |
+ <%=cue.page%> |
+ <%=cue.description%> |
+ <%=cue.type.display%> |
+ <%=cue.state%> |
+ <%=cue.activation%> |
+ <%=cue.length%> |
+
+ <% }) %>
+
\ No newline at end of file