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..8197996
--- /dev/null
+++ b/plugins/scs/info.html
@@ -0,0 +1,6 @@
+
Connection Requirements
+
+ - Remote App must be set to OSC App under Remote App Interface settings
+ - Network Protocol must be set to TCP under Remote App Interface settings
+ - OSC Version must be set to 1.1 under Remote App Interface settings
+
diff --git a/plugins/scs/main.js b/plugins/scs/main.js
new file mode 100644
index 0000000..d7db126
--- /dev/null
+++ b/plugins/scs/main.js
@@ -0,0 +1,242 @@
+exports.config = {
+ defaultName: 'Show Cue Systems',
+ connectionType: 'osc',
+ defaultPort: 58100,
+ mayChangePorts: true,
+ heartbeatInterval: 5000,
+ searchOptions: {
+ type: 'TCPport',
+ searchBuffer: Buffer.from('/status', 'ascii'),
+ testPort: 58100,
+ 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',
+};
+
+const activationMap = {
+ man: 'Manual',
+ 'm+c': 'Manual w/ Confirmation',
+ auto: 'Auto-start',
+ 'a+c': 'Auto-start w/ Confirmation',
+ callq: 'Call Cue',
+ hot: 'Hotkey (Trigger)',
+ hktg: 'Hotkey (Toggle)',
+ hknt: 'Hotkey (Note)',
+ time: 'Time-Based',
+ ext: 'External (Trigger)',
+ extg: 'External (Toggle)',
+ exnt: 'External (Note)',
+ mtc: 'MIDI Time Code',
+ ocm: 'On Cue Marker',
+};
+
+const stateMap = {
+ 0: 'Ready',
+ 1: 'Playing',
+ 2: 'Paused',
+ 3: 'Completed',
+};
+
+exports.ready = function ready(_device) {
+ const device = _device;
+ device.data.cues = [];
+ device.data.initialized = false;
+ device.send('/prod/gettitle');
+};
+
+exports.data = function data(_device, oscData) {
+ const device = _device;
+ this.deviceInfoUpdate(device, 'status', 'ok');
+ const messagePath = oscData.address;
+ if (messagePath === '/prod/gettitle') {
+ const title = oscData.args[0].replaceAll('"', '').trim();
+ this.deviceInfoUpdate(device, 'defaultName', title);
+ device.send('/info/scsversion');
+ } else if (messagePath === '/info/scsversion') {
+ const versionInfo = oscData.args;
+ const versionNumber = versionInfo[0].toString();
+ 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('/info/finalcue');
+ } else if (messagePath === '/info/finalcue') {
+ const cueCount = oscData.args[0];
+ if (cueCount !== device.data.cues.length) {
+ device.data.initialized = false;
+ device.data.cues = new Array(cueCount).fill(0).map(() => ({
+ label: '',
+ page: '',
+ description: '',
+ type: { code: '', display: '' },
+ state: '',
+ activation: { code: '', display: '' },
+ file_info: '',
+ length: '',
+ colors: ['', ''],
+ }));
+ device.send('/cue/getitemsn', [
+ { type: 'i', value: 1 },
+ { type: 's', value: 'QNTCLSPARZ' },
+ ]);
+ }
+ } else if (messagePath === '/info/getcue') {
+ const cueLabelInfo = oscData.args;
+ const cueNumber = cueLabelInfo[0];
+ const cueLabel = cueLabelInfo[1].replaceAll('"', '');
+
+ device.data.cues[cueNumber - 1].label = cueLabel;
+ device.draw();
+ } else if (messagePath === '/cue/getpage') {
+ const pageInfo = oscData.args;
+ 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('/cue/getpage', [{ type: 's', value: device.data.cues[cueIndex + 1].label }]);
+ } else {
+ device.draw();
+ device.data.initialized = true;
+ device.send('/status');
+ }
+ } else if (messagePath === '/cue/getname') {
+ const nameInfo = oscData.args;
+ 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 = oscData.args;
+ 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 = oscData.args;
+ if (itemParts.length === 12) {
+ const cueIndex = itemParts[0] - 1;
+ device.data.cues[cueIndex].label = itemParts[2];
+ device.data.cues[cueIndex].description = itemParts[3];
+ device.data.cues[cueIndex].type = {
+ code: itemParts[4],
+ display: typeMap[itemParts[4]],
+ };
+ device.data.cues[cueIndex].colors = itemParts[5].split(', ');
+ device.data.cues[cueIndex].length = millisToString(itemParts[6]);
+ device.data.cues[cueIndex].state = stateMap[itemParts[7]];
+ device.data.cues[cueIndex].position = itemParts[8];
+ device.data.cues[cueIndex].activation = {
+ code: itemParts[9],
+ display: activationMap[itemParts[9]],
+ };
+ device.data.cues[cueIndex].repeat = itemParts[10];
+ device.data.cues[cueIndex].loop = itemParts[11];
+ device.draw();
+
+ if (!device.data.initialized) {
+ if (cueIndex < device.data.cues.length - 1) {
+ device.send('/cue/getitemsn', [
+ { type: 'i', value: cueIndex + 2 },
+ { type: 's', value: 'QNTCLSPARZ' },
+ ]);
+ } else {
+ device.send('/cue/getpage', [{ type: 's', value: device.data.cues[0].label }]);
+ }
+ }
+ } else {
+ console.error('bad response from getitems');
+ }
+ } else if (messagePath === '/cue/statechange') {
+ const cueLabel = oscData.args[0];
+ const cueIndex = device.data.cues.map((cue) => cue.label).indexOf(cueLabel);
+ if (cueIndex !== undefined) {
+ device.data.cues[cueIndex].state = stateMap[oscData.args[1]];
+ device.draw();
+ device.send('/cue/getitemsn', [
+ { type: 'i', value: cueIndex + 1 },
+ { type: 's', value: 'QNTCLSPARZ' },
+ ]);
+ }
+ } else if (messagePath === '/status') {
+ // const status = oscData.args[0];
+ device.send('/info/currcue');
+ } else if (messagePath === '/connected') {
+ device.send('/prod/gettitle');
+ } else if (messagePath === '/info/currcue') {
+ device.data.currentCue = oscData.args[0];
+ device.draw();
+ } else if (messagePath === '/info/nextcue') {
+ device.data.nextCue = oscData.args[0];
+ device.draw();
+ device.data.initialized = false;
+ device.send('/cue/getitemsn', [
+ { type: 'i', value: 1 },
+ { type: 's', value: 'QNTCLSPARZ' },
+ ]);
+ } else {
+ console.error(`unhandled message path: ${messagePath}`);
+ }
+};
+
+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) {
+ device.send('/info/finalcue');
+};
diff --git a/plugins/scs/styles.css b/plugins/scs/styles.css
new file mode 100644
index 0000000..e0b94ac
--- /dev/null
+++ b/plugins/scs/styles.css
@@ -0,0 +1,17 @@
+table {
+ background: white;
+}
+
+th {
+ text-align: left;
+ font-weight: normal;
+}
+
+.header-row {
+ background: white;
+}
+
+td {
+ padding-left: 5px;
+ padding-right: 5px;
+}
diff --git a/plugins/scs/template.ejs b/plugins/scs/template.ejs
new file mode 100644
index 0000000..f090df8
--- /dev/null
+++ b/plugins/scs/template.ejs
@@ -0,0 +1,34 @@
+
+
+
+ <% data.cues.forEach((cue, index )=>{ %>
+
+ <% if (data.currentCue === index + 1){ %>
+
+ <%} else if (index + 1 < data.currentCue){ %>
+
+ <%} else{ %>
+
+ <% } %>
+
+
+ | <%=cue.label%> |
+ <%=cue.page%> |
+ <%=cue.description%> |
+ <%=cue.type.display%> |
+ <%=cue.state%> |
+ <%=cue.activation.display%> |
+ <%=cue.length%> |
+
+ <% }) %>
+
\ No newline at end of file
diff --git a/src/device.js b/src/device.js
index bd48864..14f6cd9 100644
--- a/src/device.js
+++ b/src/device.js
@@ -122,6 +122,7 @@ function initDeviceConnection(id) {
device.connection = new osc.TCPSocketPort({
address: device.addresses[0],
port: device.remotePort,
+ useSLIP: plugins[type].config.useSLIP !== undefined ? plugins[type].config.useSLIP : true
});
}
device.connection.open();