diff --git a/plugins/lightfactory/icon.png b/plugins/lightfactory/icon.png new file mode 100644 index 0000000..08cba27 Binary files /dev/null and b/plugins/lightfactory/icon.png differ diff --git a/plugins/lightfactory/info.html b/plugins/lightfactory/info.html new file mode 100644 index 0000000..e69de29 diff --git a/plugins/lightfactory/main.js b/plugins/lightfactory/main.js new file mode 100644 index 0000000..9cdfbf1 --- /dev/null +++ b/plugins/lightfactory/main.js @@ -0,0 +1,136 @@ +exports.config = { + defaultName: 'LightFactory', + connectionType: 'TCPsocket', + remotePort: 3100, + mayChangePorts: true, + heartbeatInterval: 5000, + heartbeatTimeout: 10000, + searchOptions: { + type: 'TCPport', + searchBuffer: Buffer.from('\n', 'ascii'), + testPort: 3100, + validateResponse(msg, info) { + return msg.toString().includes('LightFactory'); + }, + }, +}; + +exports.ready = function ready(_device) { + const device = _device; + device.data.cueLists = {}; + device.send('get cue list\n'); +}; + +function cleanMessage(message) { + return message.split('\n').filter((line) => line.trim() !== ''); +} + +// TODO(jwetzell): try to grab notes? +exports.data = function data(_device, _message) { + const message = _message.toString(); + const device = _device; + + if (message.includes('Current Cue List')) { + // NOTE(jwetzell): loading cue list info + const cueListMessage = cleanMessage(message); + if (cueListMessage.includes('') && cueListMessage.includes('')) { + const cueListstartIndex = cueListMessage.indexOf(''); + const cueListFinishedIndex = cueListMessage.indexOf(''); + + for (let i = cueListstartIndex + 1; i < cueListFinishedIndex; i++) { + const cueListName = cueListMessage[i]; + if (device.data.cueLists[cueListName] === undefined) { + device.data.cueLists[cueListName] = { + name: cueListName, + cues: {}, + active: false, + }; + } else { + // NOTE(jwetzell): reset active as it will be computed in a few lines + device.data.cueLists[cueListName].active = false; + } + } + const activeCueListLine = cueListMessage[cueListFinishedIndex + 1]; + if (activeCueListLine) { + const activeCueListLineParts = activeCueListLine.split('Current Cue List'); + const activeCueList = activeCueListLineParts[activeCueListLineParts.length - 1].trim(); + if (activeCueList && device.data.cueLists[activeCueList] !== undefined) { + device.data.cueLists[activeCueList].active = true; + } + } + device.data.cueListToLoad = Object.keys(device.data.cueLists)[0]; + device.send(`get cues ${device.data.cueListToLoad}\n`); + } + } else if (message.includes('Current Live Cue')) { + // NOTE(jwetzell): loading cues for a cue list + const cueList = device.data.cueLists[device.data.cueListToLoad]; + const cueListCuesMessage = cleanMessage(message); + + // NOTE(jwetzell): this is so gross + if (cueListCuesMessage.includes('') && cueListCuesMessage.includes('')) { + const cuesStartIndex = cueListCuesMessage.indexOf(''); + const cuesFinishedIndex = cueListCuesMessage.indexOf(''); + + const validCueNumbers = []; + for (let i = cuesStartIndex + 1; i < cuesFinishedIndex; i++) { + const cueData = cueListCuesMessage[i]; + const cueSplit = cueData.indexOf(' '); + const cueNumber = cueSplit > 0 ? cueData.substring(0, cueSplit).trim() : cueData.trim(); + const cueName = cueSplit > 0 ? cueData.substring(cueSplit + 1).trim() : ''; + cueList.cues[cueNumber] = { + name: cueName, + number: cueNumber, + active: false, + }; + validCueNumbers.push(cueNumber); + } + + const invalidCueNumbers = Object.keys(cueList.cues).filter((cueNumber) => !validCueNumbers.includes(cueNumber)); + + invalidCueNumbers.forEach((cueNumber) => { + delete cueList.cues[cueNumber]; + }); + + const activeCueLine = cueListCuesMessage[cuesFinishedIndex + 1]; + if (activeCueLine) { + const activeCueLineParts = activeCueLine.split('Current Live Cue'); + const activeCue = activeCueLineParts[activeCueLineParts.length - 1].trim(); + if (activeCue && cueList.cues[activeCue] !== undefined) { + cueList.cues[activeCue].active = true; + } + } + } + // NOTE(jwetzell): load next cue list's cues if there is one + const cueListNames = Object.keys(device.data.cueLists); + const cueListIndex = cueListNames.indexOf(device.data.cueListToLoad); + device.data.cueListToLoad = cueListNames.at(cueListIndex + 1); + if (device.data.cueListToLoad !== undefined) { + device.send(`get cues ${device.data.cueListToLoad}\n`); + } + } else if (message.includes('Running cue') || message.includes('Goto cue')) { + const cueUpdateMessage = message + .trim() + .slice(0, -1) + .split(':') + .map((item) => item.trim()); + if (cueUpdateMessage.length === 3) { + const cueListName = cueUpdateMessage[1]; + const updatedCue = cueUpdateMessage[2]; + + const split = updatedCue.indexOf(' '); + + const updatedCueNumber = split > 0 ? updatedCue.substring(0, split).trim() : updatedCue.trim(); + if (device.data.cueLists[cueListName] !== undefined) { + Object.entries(device.data.cueLists[cueListName].cues).forEach(([cueName, cueData]) => { + cueData.active = cueData.number === updatedCueNumber; + }); + } + } + } + device.draw(); +}; + +// TODO(jwetzell): keep alive? refresh cue list info? +exports.heartbeat = function heartbeat(device) { + device.send('get cue list\n'); +}; diff --git a/plugins/lightfactory/styles.css b/plugins/lightfactory/styles.css new file mode 100644 index 0000000..4f08140 --- /dev/null +++ b/plugins/lightfactory/styles.css @@ -0,0 +1,52 @@ +section { + background-color: #363636; + border: gray 1px solid; + padding: 2px; + padding-top: 0px; + overflow: hidden; + font-size: 8px; + margin-bottom: 10px; + width: 300px; +} +table { + border: #414142 1px solid; + background-color: #272727; + width: 100%; +} +table, +th, +td { + border-collapse: collapse; + color: #ccc; + font-size: 12px; +} +th { + padding: 2px 10px; + border: #282828 1px solid; + background: linear-gradient(#404040, #363636); + text-align: left; +} +td { + text-align: left; + padding: 7px 7px; + font-family: sans-serif; + position: relative; +} +tr { + border-bottom: #363636 1px solid; +} +tr:nth-child(odd) { + background-color: #2a2a2a; +} +tr:nth-child(even) { + background-color: #202020; +} + +.active_indicator { + color: #00ff00; + font-size: 13px; + position: absolute; + top: 4px; + left: -4px; + transform: rotate(-45deg); +} diff --git a/plugins/lightfactory/template.ejs b/plugins/lightfactory/template.ejs new file mode 100644 index 0000000..2fe80ad --- /dev/null +++ b/plugins/lightfactory/template.ejs @@ -0,0 +1,35 @@ +
+

<%= listName %>

+
+ +
+ + <% Object.entries(data.cueLists).forEach(([cueListName,cueListData]) => { %> + +
+

Cue List: <%= cueListData.name %> <%= cueListData.active ? '(Active)': '' %>

+ + + + + + + + + + <% Object.entries(cueListData.cues).forEach(([cueName,cueData]) => { %> + + + + + + + + <% }); %> +
Cue NoDescription
<%= cueData.active ? '◢': '' %><%= cueData.number %><%= cueData.name %>
+
+ + <% + }); + %> +
\ No newline at end of file