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

+ 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 @@ +
+

<%= listName %>

+
+ + + + + + + + + + + <% data.cues.forEach((cue, index )=>{ %> + + <% if (data.currentCue === index + 1){ %> + + <%} else if (index + 1 < data.currentCue){ %> + + <%} else{ %> + + <% } %> + + + + + + + + + + + <% }) %> +
LabelPageDescriptionCue TypeStateActivationLength
<%=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();