diff --git a/actions.js b/actions.js index 4068320..cfc7603 100644 --- a/actions.js +++ b/actions.js @@ -17,10 +17,10 @@ export function compileActionDefinitions(self) { } else if (cmd !== undefined) { self.log('debug', `sending ${cmd} ${JSON.stringify(args)} to ${self.config.host}`) // everything except 'auditionWindow' and 'overrideWindow' works on a specific workspace - self.sendOSC(cmd, args, ['/auditionWindow', '/overrideWindow'].includes(cmd)) + self.sendOSC(cmd, args, ['/auditionWindow', '/alwaysAudition', '/overrideWindow'].includes(cmd)) } // QLab does not send window updates so ask for status - if (self.useTCP && ['/auditionWindow', '/overrideWindow'].includes(cmd)) { + if (self.useTCP && ['/auditionWindow', '/alwaysAudition', '/overrideWindow'].includes(cmd)) { self.sendOSC(cmd, [], true) self.sendOSC('/cue/playhead/valuesForKeys', self.qCueRequest) } @@ -47,6 +47,14 @@ export function compileActionDefinitions(self) { await sendCommand(action, '/go') }, }, + audition_go: { + name: 'Audition GO', + description: 'QLab 5 ONLY', + options: [], + callback: async (action, context) => { + await sendCommand(action, '/auditionGo') + }, + }, stop: { name: 'Stop', options: [], @@ -226,6 +234,40 @@ export function compileActionDefinitions(self) { await sendCommand(action, '/cue/' + optCue + '/panicInTime', timeArg) }, }, + audition_go_cue: { + name: 'Audition Cue', + description: 'QLab5 ONLY', + options: [ + { + type: 'textinput', + label: 'Cue', + id: 'cue', + default: '1', + useVariables: true, + }, + ], + callback: async (action, context) => { + const optCue = await context.parseVariablesInString(action.options.cue) + await sendCommand(action, '/cue/' + optCue + '/audition') + }, + }, + audition_go_id: { + name: 'Audition Cue ID', + description: 'QLab5 ONLY', + options: [ + { + type: 'textinput', + label: 'Cue', + id: 'cueId', + default: '1', + useVariables: true, + }, + ], + callback: async (action, context) => { + const optCueId = await context.parseVariablesInString(action.options.cueId) + await sendCommand(action, '/cue_id/' + optCueId + '/audition') + }, + }, goto_id: { name: 'Goto (Cue ID)', options: [ @@ -239,7 +281,7 @@ export function compileActionDefinitions(self) { ], callback: async (action, context) => { const optCueId = await context.parseVariablesInString(action.options.cueId) - const phID = (self.qVer< 5 ? 'Id' : 'ID') + const phID = self.qVer < 5 ? 'Id' : 'ID' await sendCommand(action, `/playhead${phID}/` + optCueId) }, }, @@ -334,7 +376,8 @@ export function compileActionDefinitions(self) { }, }, auditMode: { - name: 'Audition Window', + name: 'Audition', + description: `QLab 5 sets 'Always Audition' mode\nOtherwise Show/Hide 'Audition Window'`, options: [ { type: 'dropdown', @@ -345,12 +388,32 @@ export function compileActionDefinitions(self) { }, ], callback: async (action, context) => { - await sendCommand(action, '/auditionWindow', { + const act = self.qVer < 5 ? '/auditionWindow' : '/alwaysAudition' + await sendCommand(action, act, { type: 'i', value: setToggle(self.auditMode, action.options.onOff), }) }, }, + auditWindows: { + name: 'Audition Monitors', + description: 'QLab 5 only, open/close ALL audition monitors', + options: [ + { + type: 'dropdown', + label: 'Mode', + id: 'onOff', + default: 1, + choices: Choices.TOGGLE, + }, + ], + callback: async (action, context) => { + await sendCommand(action, '/auditionMonitors', { + type: 'i', + value: setToggle(self.auditMonitors, action.options.onOff), + }) + }, + }, overrideWindow: { name: 'Override Controls Window', options: [ diff --git a/choices.js b/choices.js index b37fb1f..d031a85 100644 --- a/choices.js +++ b/choices.js @@ -16,14 +16,19 @@ export const ON_OFF = [ ] export const OVERRIDE = [ - { id: 'midiInputEnabled', label: 'Midi Input' }, - { id: 'midiOutputEnabled', label: 'Midi Output' }, - { id: 'mscInputEnabled', label: 'MSC Input' }, - { id: 'mscOutputEnabled', label: 'MSC Output' }, - { id: 'sysexInputEnabled', label: 'SysEx Input' }, - { id: 'sysexOutputEnabled', label: 'SysEx Output' }, - { id: 'oscOutputEnabled', label: 'OSC Output' }, - { id: 'timecodeInputEnabled', label: 'Timecode Input' }, - { id: 'timecodeOutputEnabled', label: 'Timecode Output' }, - { id: 'artNetEnabled', label: 'Art-Net Enabled' }, + { id: 'midiInputEnabled', label: 'Midi Input [4, 5]' }, + { id: 'midiOutputEnabled', label: 'Midi Output [4, 5]' }, + { id: 'mscInputEnabled', label: 'MSC Input [4, 5]' }, + { id: 'mscOutputEnabled', label: 'MSC Output [4, 5]' }, + { id: 'sysexInputEnabled', label: 'SysEx Input [4, 5]' }, + { id: 'sysexOutputEnabled', label: 'SysEx Output [4, 5]' }, + { id: 'oscOutputEnabled', label: 'OSC Output [4]' }, + { id: 'timecodeInputEnabled', label: 'Timecode Input [4, 5]' }, + { id: 'timecodeOutputEnabled', label: 'Timecode Output [4, 5]' }, + { id: 'artNetEnabled', label: 'Art-Net Enabled [4]' }, + { id: 'dmxOutputEnabled', label: 'DMX Output [5]' }, + { id: 'networkExternalInputEnabled', label: 'External Network Input [5]' }, + { id: 'networkExternalOutputEnabled', label: 'External Network Output [5]' }, + { id: 'networkLocalInputEnabled', label: 'Local Network Input [5]' }, + { id: 'networkLocalOutputEnabled', label: 'Local Network Output [5]' }, ] diff --git a/companion/HELP.md b/companion/HELP.md index 976a6a2..f31024a 100644 --- a/companion/HELP.md +++ b/companion/HELP.md @@ -26,15 +26,15 @@ https://github.com/sponsors/istnv ## Configuration -| Setting | Description | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **Target IP** | Enter the address of the QLab computer. You can enter 127.0.0.1 if Companion is running on the same computer. | -| **Target Port** | Enter the port number where QLab is listening for OSC messages. This defaults to 53000. Has no effect on QLab4 (or QLab3) | -| **Use TCP?** | Check to enable TCP mode. This is required for variables and feedback. | -| **Use Tenths** | If checked, the variable _r_left_ will display 0.1 seconds when less than 5 seconds. If unchecked, the time left will be adjusted by 1 second for a more accurate count-down. | -| **OSC Passcode** | Enter a passcode if needed for the QLab workspace. QLab 5 requires a passcode to work reliably from Companion. | +| Setting | Description | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Target IP** | Enter the address of the QLab computer. You can enter 127.0.0.1 if Companion is running on the same computer. | +| **Target Port** | Enter the port number where QLab is listening for OSC messages. This defaults to 53000. Has no effect on QLab4 (or QLab3) | +| **Use TCP?** | Check to enable TCP mode. This is required for variables and feedback. | +| **Use Tenths** | If checked, the variable _r_left_ will display 0.1 seconds when less than 5 seconds. If unchecked, the time left will be adjusted by 1 second for a more accurate count-down. | +| **OSC Passcode** | Enter a passcode if needed for the QLab workspace. QLab 5 requires a passcode to work reliably from Companion. | | **Workspace** | Dropdown selection to select a specific Workspace. You can enter 'default' or leave blank to control front-most workspace (if more than one is open) in QLab4. QLab5 commands go to all open workspaces. You can change the IP Control Port too allow multiple workspaces open at once. | -| **Specific Cue List** | Dropdown selection to limit control to a specific cuelist. | +| **Specific Cue List** | Dropdown selection to limit control to a specific cuelist. | ## Actions @@ -59,7 +59,7 @@ https://github.com/sponsors/istnv | **Show Mode** | Enable for Show Mode, Disable for Edit Mode. | | **Audition Window** | Show or Hide the Audition Window. | | **Override Window** | Show or Hide the Override Controls Window. | -| **Master Override** | Set Master override for Midi, MSC, SysEx, OSC, Timecode, Art-Net On or Off | +| **Master Override** | Toggle or Set Master overrides. Enabling an override will open/show the Override Control window
| | **Set Minimum Go** | Sets the time for double-GO protection | | **Increase Prewait** | Increases the prewait time by given time for the selected cue. | | **Decrease Prewait** | Decreases the prewait time by given time for the selected cue. | diff --git a/cues.js b/cues.js index 9800c67..c6cd9f4 100644 --- a/cues.js +++ b/cues.js @@ -10,6 +10,7 @@ class Cue { isPaused = false isArmed = false isFlagged = false + isAuditioning = false infiniteLoop = false holdLastFrame = false autoLoad = false @@ -37,6 +38,7 @@ function JSONtoCue(newCue, j, self) { newCue.qColorName = j.colorName newCue.qType = j.type.toLowerCase() newCue.isRunning = j.isRunning + newCue.isAuditioning = j.isAuditioning newCue.isLoaded = j.isLoaded newCue.isBroken = j.isBroken newCue.isPaused = j.isPaused diff --git a/feedbacks.js b/feedbacks.js index 328c1fb..c01ebe7 100644 --- a/feedbacks.js +++ b/feedbacks.js @@ -170,6 +170,27 @@ export function compileFeedbackDefinitions(self) { } }, }, + ws_audit: { + type: 'boolean', + name: 'Audit Monitors', + description: 'Set Button when ALL Audit monitors are open', + options: [ + // { + // type: 'dropdown', + // label: 'Override', + // id: 'which', + // default: 1, + // choices: Choices.ON_OFF, + // }, + ], + defaultStyle: { + color: combineRgb(255, 255, 255), + bgcolor: combineRgb(102, 0, 0), + }, + callback: (feedback, context) => { + return !!self.auditMonitors + }, + }, override: { type: 'boolean', name: 'Master Override', @@ -211,9 +232,7 @@ export function compileFeedbackDefinitions(self) { bgcolor: combineRgb(102, 0, 0), }, callback: (feedback, context) => { - const options = feedback.options - - return self.overrideWindow == 1 + return !!self.overrideWindow }, }, } diff --git a/package.json b/package.json index b9ed516..5c51302 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "figure53-qlab-advance", - "version": "2.2.2", + "version": "2.3.0", "main": "qlabfb.js", "type": "module", "scripts": { diff --git a/qlabfb.js b/qlabfb.js index 009417e..97824c9 100644 --- a/qlabfb.js +++ b/qlabfb.js @@ -13,19 +13,32 @@ import * as Choices from './choices.js' function cueToStatusChar(cue) { if (cue.isBroken) return '\u2715' + if (cue.isAuditioning) return '\u2772\u23F5\u2773' if (cue.isRunning) return '\u23F5' if (cue.isPaused) return '\u23F8' if (cue.isLoaded) return '\u23FD' return '\u00b7' } +/** + * Returns the passed integer left-padded with '0's + * Will truncate result length is greater than 'len' + * @param {Number} num: number to pad + * @param {Number} len: optional length of result, defaults to 2 + * @since 2.3.0 + */ +function pad0(num, len = 2) { + const zeros = '0'.repeat(len) + return (zeros + num).slice(-len) +} + class QLabInstance extends InstanceBase { qCueRequest = [ { type: 's', value: '["number","uniqueID","listName","type","isPaused","duration","actionElapsed","parent","flagged","notes",' + - '"autoLoad","colorName","isRunning","isLoaded","armed","isBroken","percentActionElapsed","cartPosition",' + + '"autoLoad","colorName","isRunning","isAuditioning","isLoaded","armed","isBroken","percentActionElapsed","cartPosition",' + '"infiniteLoop","holdLastFrame"]', }, ] @@ -143,30 +156,30 @@ class QLabInstance extends InstanceBase { } updateQVars(q) { q = q || new Cue() - var self = this - var qID = q.uniqueID - var qNum = q.qNumber.replace(/[^\w\.]/gi, '_') - var qType = q.qType - var qColor = q.qColor - var oqNum = null - var oqName = null - var oqType = null - var oqColor = 0 - var oqOrder = -1 + + const qID = q.uniqueID + const qNum = q.qNumber.replace(/[^\w\.]/gi, '_') + const qType = q.qType + const qColor = q.qColor + let oqNum = null + let oqName = null + let oqType = null + let oqColor = 0 + let oqOrder = -1 let variableValues = {} // unset old variable? - if (qID in self.wsCues) { - oqNum = self.wsCues[qID].qNumber.replace(/[^\w\.]/gi, '_') - oqName = self.wsCues[qID].qName - oqType = self.wsCues[qID].qType - oqColor = self.wsCues[qID].qColor - oqOrder = self.wsCues[qID].qOrder + if (qID in this.wsCues) { + oqNum = this.wsCues[qID].qNumber.replace(/[^\w\.]/gi, '_') + oqName = this.wsCues[qID].qName + oqType = this.wsCues[qID].qType + oqColor = this.wsCues[qID].qColor + oqOrder = this.wsCues[qID].qOrder if (oqNum != '' && oqNum != q.qNumber) { variableValues['q_' + oqNum + '_name'] = undefined - self.cueColors[oqNum] = 0 - delete self.cueByNum[oqNum] + this.cueColors[oqNum] = 0 + delete this.cueByNum[oqNum] oqName = '' } } @@ -174,8 +187,8 @@ class QLabInstance extends InstanceBase { if (q.qName != oqName || qColor != oqColor) { if (qNum != '') { variableValues['q_' + qNum + '_name'] = q.qName - self.cueColors[qNum] = q.qColor - self.cueByNum[qNum] = qID + this.cueColors[qNum] = q.qColor + this.cueByNum[qNum] = qID } variableValues['id_' + qID + '_name'] = q.qName @@ -186,19 +199,16 @@ class QLabInstance extends InstanceBase { } updateRunning() { - var self = this - var tenths = self.config.useTenths ? 0 : 1 - var rc = self.runningCue + const tenths = this.config.useTenths ? 0 : 1 + const rc = this.runningCue - var tElapsed = rc.duration * rc.pctElapsed + const tElapsed = rc.duration * rc.pctElapsed - var eh = Math.floor(tElapsed / 3600) - var ehh = ('00' + eh).slice(-2) - var em = Math.floor(tElapsed / 60) % 60 - var emm = ('00' + em).slice(-2) - var es = Math.floor(tElapsed % 60) - var ess = ('00' + es).slice(-2) - var eft = '' + const ehh = pad0(Math.floor(tElapsed / 3600)) + const emm = pad0(Math.floor(tElapsed / 60) % 60) + const ess = pad0(Math.floor(tElapsed % 60)) + + let eft = '' if (ehh > 0) { eft = ehh + ':' @@ -208,18 +218,15 @@ class QLabInstance extends InstanceBase { } eft = eft + ess - var tLeft = rc.duration * (1 - rc.pctElapsed) + let tLeft = rc.duration * (1 - rc.pctElapsed) if (tLeft > 0) { tLeft += tenths } - var h = Math.floor(tLeft / 3600) - var hh = ('00' + h).slice(-2) - var m = Math.floor(tLeft / 60) % 60 - var mm = ('00' + m).slice(-2) - var s = Math.floor(tLeft % 60) - var ss = ('00' + s).slice(-2) - var ft = '' + const hh = pad0(Math.floor(tLeft / 3600)) + const mm = pad0(Math.floor(tLeft / 60) % 60) + const ss = pad0(Math.floor(tLeft % 60)) + let ft = '' if (hh > 0) { ft = hh + ':' @@ -230,8 +237,8 @@ class QLabInstance extends InstanceBase { ft = ft + ss if (tenths == 0) { - var f = Math.floor((tLeft - Math.trunc(tLeft)) * 10) - var ms = ('0' + f).slice(-1) + const f = Math.floor((tLeft - Math.trunc(tLeft)) * 10) + const ms = pad0(f, 1) if (tLeft < 5 && tLeft != 0) { ft = ft.slice(-1) + '.' + ms } @@ -300,10 +307,10 @@ class QLabInstance extends InstanceBase { } } sendOSC(node, arg, bare) { - var ws = bare ? '' : this.ws + const ws = bare ? '' : this.ws if (!this.useTCP) { - var host = '' + const host = '' if (this.config.host !== undefined && this.config.host !== '') { host = this.config.host } @@ -387,11 +394,14 @@ class QLabInstance extends InstanceBase { this.sendOSC('/cueLists', []) if (this.qVer < 5) { this.sendOSC('/auditionWindow', [], true) + } else { + this.sendOSC('/alwaysAudition', [], true) + this.sendOSC('/auditionMonitors', [], true) } this.sendOSC('/overrideWindow', [], true) this.sendOSC('/showMode', []) this.sendOSC('/settings/general/minGoTime') - for (var o in Choices.OVERRIDE) { + for (const o in Choices.OVERRIDE) { this.sendOSC('/overrides/' + Choices.OVERRIDE[o].id, [], true) } if (this.timer !== undefined) { @@ -413,6 +423,10 @@ class QLabInstance extends InstanceBase { this.sendOSC((this.cl ? '/cue/' + this.cl : '') + `/playhead${phID}`, []) + if (5 == this.qVer) { + this.sendOSC('/alwaysAudition', [], true) + this.sendOSC('/auditionMonitors', [], true) + } if (4 == this.qVer) { this.sendOSC('/auditionWindow', [], true) } @@ -471,148 +485,146 @@ class QLabInstance extends InstanceBase { } init_osc() { - var self = this - var ws = self.ws + const ws = this.ws - if (self.connecting) { + if (this.connecting) { return } - if (self.qSocket) { - self.ready = false - self.qSocket.close() - delete self.qSocket + if (this.qSocket) { + this.ready = false + this.qSocket.close() + delete this.qSocket } - if (self.config.host) { - if (self.useTCP) { - self.qSocket = new OSC.TCPSocketPort({ + if (this.config.host) { + if (this.useTCP) { + this.qSocket = new OSC.TCPSocketPort({ localAddress: '0.0.0.0', - localPort: 0, // 53000 + self.port_offset, - address: self.config.host, - port: 53000, + localPort: 0, // 53000 + this.port_offset, + address: this.config.host, + port: this.config.port, metadata: true, }) - self.connecting = true + this.connecting = true } else { - self.qSocket = new OSC.UDPPort({ + this.qSocket = new OSC.UDPPort({ localAddress: '0.0.0.0', - localPort: 53001, // 53000 + self.port_offset, - remoteAddress: self.config.host, + localPort: 53001, // 53000 + this.port_offset, + remoteAddress: this.config.host, remotePort: 53000, metadata: true, }) - self.updateStatus(InstanceStatus.Ok, 'UDP Mode') + this.updateStatus(InstanceStatus.Ok, 'UDP Mode') } - self.qSocket.open() + this.qSocket.open() - self.qSocket.on('error', (err) => { - self.log('debug', 'Error: ' + err) - self.connecting = false - if (!self.hasError) { - self.log('error', 'Error: ' + err.message) - self.updateStatus(InstanceStatus.UnknownError, "Can't connect to QLab " + err.message) - self.hasError = true + this.qSocket.on('error', (err) => { + this.log('debug', 'Error: ' + err) + this.connecting = false + if (!this.hasError) { + this.log('error', 'Error: ' + err.message) + this.updateStatus(InstanceStatus.UnknownError, "Can't connect to QLab " + err.message) + this.hasError = true } if (err.code == 'ECONNREFUSED') { - if (self.qSocket) { - self.qSocket.removeAllListeners() + if (this.qSocket) { + this.qSocket.removeAllListeners() } - if (self.timer !== undefined) { - clearTimeout(self.timer) + if (this.timer !== undefined) { + clearTimeout(this.timer) } - if (self.pulse !== undefined) { - clearInterval(self.pulse) - self.pulse = undefined + if (this.pulse !== undefined) { + clearInterval(this.pulse) + this.pulse = undefined } - self.timer = setTimeout(() => { - self.connect() + this.timer = setTimeout(() => { + this.connect() }, 5000) } }) - self.qSocket.on('close', () => { - if (!self.hasError && self.ready) { - self.log('error', 'TCP Connection to QLab Closed') + this.qSocket.on('close', () => { + if (!this.hasError && this.ready) { + this.log('error', 'TCP Connection to QLab Closed') } - self.connecting = false - if (self.ready) { - self.needWorkspace = true - self.needPasscode = false - self.resetVars(true) + this.connecting = false + if (this.ready) { + this.needWorkspace = true + this.needPasscode = false + this.resetVars(true) // the underlying socket issues a final close after // the OSC socket is closed, which gets deleted on 'destroy' - if (self.qSocket != undefined) { - self.qSocket.removeAllListeners() + if (this.qSocket != undefined) { + this.qSocket.removeAllListeners() } - self.log('debug', 'Connection closed') - self.ready = false - if (self.disabled) { - self.updateStatus(InstanceStatus.Disconnected, 'Disabled') + this.log('debug', 'Connection closed') + this.ready = false + if (this.disabled) { + this.updateStatus(InstanceStatus.Disconnected, 'Disabled') } else { - self.updateStatus(InstanceStatus.Disconnected, 'CLOSED') + this.updateStatus(InstanceStatus.Disconnected, 'CLOSED') } } - if (self.timer !== undefined) { - clearTimeout(self.timer) - self.timer = undefined + if (this.timer !== undefined) { + clearTimeout(this.timer) + this.timer = undefined } - if (self.pulse !== undefined) { - clearInterval(self.pulse) - self.pulse = undefined + if (this.pulse !== undefined) { + clearInterval(this.pulse) + this.pulse = undefined } - if (!self.disabled) { + if (!this.disabled) { // don't restart if instance was disabled - self.timer = setTimeout(() => { - self.connect() + this.timer = setTimeout(() => { + this.connect() }, 5000) } - self.hasError = true + this.hasError = true }) - self.qSocket.on('ready', () => { - self.ready = true - self.connecting = false - self.hasError = false - self.log('info', 'Connected to QLab:' + self.config.host) - if (self.useTCP) { - self.updateStatus(InstanceStatus.UnknownWarning, 'No Workspaces') - self.needWorkspace = true - self.prime_vars(ws) + this.qSocket.on('ready', () => { + this.ready = true + this.connecting = false + this.hasError = false + this.log('info', 'Connected to QLab:' + this.config.host) + if (this.useTCP) { + this.updateStatus(InstanceStatus.UnknownWarning, 'No Workspaces') + this.needWorkspace = true + this.prime_vars(ws) } else { - self.needWorkspace = false - self.qSocket.send({ + this.needWorkspace = false + this.qSocket.send({ address: '/version', args: [], }) } }) - self.qSocket.on('message', (message, timetag, info) => { - //self.log('debug', 'received ' + JSON.stringify(message) + `from ${self.qSocket.options.address}`) + this.qSocket.on('message', (message, timetag, info) => { + //this.log('debug', 'received ' + JSON.stringify(message) + `from ${this.qSocket.options.address}`) if (message.address.match(/^\/update\//)) { // debug("readUpdate"); - self.readUpdate(message) + this.readUpdate(message) } else if (message.address.match(/^\/reply\//)) { // debug("readReply"); - self.readReply(message) + this.readReply(message) } else { - self.log('debug', message.address + ' ' + JSON.stringify(message.args)) + this.log('debug', message.address + ' ' + JSON.stringify(message.args)) } }) } - // self.qSocket.on("data", (data) => { - // debug ("Got",data, "from",self.qSocket.options.address); + // this.qSocket.on("data", (data) => { + // debug ("Got",data, "from",this.qSocket.options.address); // }); } /** * update list cues */ updateCues(jCue, stat, ql) { - var self = this // list of useful cue types we're interested in - var qTypes = [ + const qTypes = [ 'audio', 'mic', 'video', @@ -641,20 +653,20 @@ class QLabInstance extends InstanceBase { 'memo', 'script', ] - var q = {} + let q = {} if (Array.isArray(jCue)) { - var idCount = {} - var dupIds = false + const idCount = {} + const dupIds = false for (let i = 0; i < jCue.length; i++) { - q = new Cue(jCue[i], self) + q = new Cue(jCue[i], this) q.qOrder = i if (ql) { q.qList = ql } if (stat == 'u') { - if (!self.cueList[ql].includes(q.uniqueID)) { - self.cueList[ql].push(q.uniqueID) + if (!this.cueList[ql].includes(q.uniqueID)) { + this.cueList[ql].push(q.uniqueID) } } else { if (q.uniqueID in idCount) { @@ -665,51 +677,51 @@ class QLabInstance extends InstanceBase { } if (qTypes.includes(q.qType)) { - self.updateQVars(q) - self.wsCues[q.uniqueID] = q + this.updateQVars(q) + this.wsCues[q.uniqueID] = q } if (stat == 'l') { - self.cueOrder[i] = q.uniqueID + this.cueOrder[i] = q.uniqueID if (ql) { - self.cueList[ql].push(q.uniqueID) + this.cueList[ql].push(q.uniqueID) } } } - delete self.requestedCues[q.uniqueID] + delete this.requestedCues[q.uniqueID] } this.checkFeedbacks('q_bg', 'qid_bg', 'q_run', 'qid_run') if (dupIds) { - self.updateStatus(InstanceStatus.UnknownWarning, 'Multiple cues\nwith the same cue_id') + this.updateStatus(InstanceStatus.UnknownWarning, 'Multiple cues\nwith the same cue_id') } } else { - q = new Cue(jCue, self) + q = new Cue(jCue, this) if (qTypes.includes(q.qType)) { - self.updateQVars(q) - self.wsCues[q.uniqueID] = q - if (3 == self.qVer) { + this.updateQVars(q) + this.wsCues[q.uniqueID] = q + if (3 == this.qVer) { // QLab3 seems to send cue lists as 'group' cues if ('group' == q.qType) { - if (!self.cueList[q.uniqueID]) self.cueList[q.uniqueID] = [] + if (!this.cueList[q.uniqueID]) this.cueList[q.uniqueID] = [] } } else { // a 'cart' is a special 'cue list' if (['cue list', 'cart'].includes(q.qType)) { - if (!self.cueList[q.uniqueID]) { - self.cueList[q.uniqueID] = [] + if (!this.cueList[q.uniqueID]) { + this.cueList[q.uniqueID] = [] } else { - self.sendOSC('/cue_id/' + q.uniqueID + '/children', []) + this.sendOSC('/cue_id/' + q.uniqueID + '/children', []) } } } this.checkFeedbacks('q_run', 'qid_run') this.updatePlaying() - if ('' == self.cl || (self.cueList[self.cl] && self.cueList[self.cl].includes(q.uniqueID))) { - if (q.uniqueID == self.nextCue) { - self.updateNextCue() + if ('' == this.cl || (this.cueList[this.cl] && this.cueList[this.cl].includes(q.uniqueID))) { + if (q.uniqueID == this.nextCue) { + this.updateNextCue() } } } - delete self.requestedCues[q.uniqueID] + delete this.requestedCues[q.uniqueID] } } /** @@ -717,27 +729,25 @@ class QLabInstance extends InstanceBase { */ updatePlaying() { function qState(q) { - var ret = q.uniqueID + ':' - ret += q.isBroken ? '0' : q.isRunning ? '1' : q.isPaused ? '2' : q.isLoaded ? '3' : '4' + let ret = q.uniqueID + ':' + ret += q.isBroken ? '0' : q.isRunning ? '1' : q.isPaused ? '2' : q.isLoaded ? '3' : q.isAuditioning ? '4' : 5 ret += ':' + q.duration + ':' + q.pctElapsed return ret } - var self = this - var hasGroup = false - var hasDuration = false - var i - var cl = self.cl - var cues = self.wsCues - var lastRun = qState(self.runningCue) - var runningCues = [] - var q - - Object.keys(cues).forEach((cue) => { - q = cues[cue] + let hasGroup = false + let hasDuration = false + const cl = this.cl + const cues = this.wsCues + const lastRun = qState(this.runningCue) + let runningCues = [] + + for (const cue in cues) { + // Object.keys(cues).forEach((cue) => { + const q = cues[cue] // some cuelists (for example all manual slides) may not have a pre-programmed duration if (q.isRunning || q.isPaused) { - if (('' == cl && 'cue list' != q.qType) || (self.cueList[cl] && self.cueList[cl].includes(cue))) { + if (('' == cl && 'cue list' != q.qType) || (this.cueList[cl] && this.cueList[cl].includes(cue))) { runningCues.push([cue, q.startedAt]) // if group does not have a duration, ignore // it is probably a playlist, not simultaneous playback @@ -745,30 +755,14 @@ class QLabInstance extends InstanceBase { hasDuration = hasDuration || q.duration > 0 } } - }) + } //) - // } else { - // var cue; - // var clist = self.cueList[self.cl]; - // for(i in clist) { - // cue = cues[clist[i]]; - // if (cue.duration > 0 && (cue.isRunning || cue.isPaused)) { - // if (clist[i] != cue.uniqueID) { - // var x = 1; - // } - // runningCues.push([cue.uniqueID, cue.startedAt]); - // if (cue.qType == "group") { - // hasGroup = true; - // } - // } - // } - // } runningCues.sort((a, b) => b[1] - a[1]) if (runningCues.length == 0) { - self.runningCue = new Cue() + this.runningCue = new Cue() } else { - i = 0 + let i = 0 if (hasGroup) { while (i < runningCues.length && cues[runningCues[i][0]].qType != 'group') { i += 1 @@ -779,110 +773,116 @@ class QLabInstance extends InstanceBase { } } if (i < runningCues.length) { - self.runningCue = cues[runningCues[i][0]] + this.runningCue = cues[runningCues[i][0]] // to reduce network traffic, the query interval logic only asks for running 'updates' // if the playback elapsed is > 0%. Sometimes, the first status response of a new running cue // is exactly when the cue starts, with 0% elapsed and the countdown timer won't run. // Set a new cue with 0% value to 1 here to cause at least one more query to see if the cue is // actually playing. - if (0 == self.runningCue.pctElapsed) { - self.runningCue.pctElapsed = 1 + if (0 == this.runningCue.pctElapsed) { + this.runningCue.pctElapsed = 1 } } } // update if changed - if (qState(self.runningCue) != lastRun) { - self.updateRunning(true) + if (qState(this.runningCue) != lastRun) { + this.updateRunning(true) } } /** * process QLab 'update' */ readUpdate(message) { - var self = this - var ws = self.ws - var ma = message.address - var mf = ma.split('/') + const ws = this.ws + const ma = message.address + const ms = ma.split('/').slice(1) /** * A QLab 'update' message is just the uniqueID for a cue where something 'changed'. * We have to request any other information we need (name, number, isRunning, etc.) */ - if (ma.match(/playbackPosition$/)) { - var cl = ma.substr(63, 36) - if (message.args.length > 0) { - var oa = message.args[0].value - if (self.cl) { - // if a cue is inserted, QLab sends playback changed cue - // before sending the new cue's id update, insert this id into - // the cuelist just in case so the playhead check will find it until then - if (!self.cueList[cl].includes(oa)) { - self.cueList[cl].push(oa) + + switch (ms.slice(-1)[0]) { + case 'playbackPosition': + const cl = ms[3] + if (message.args.length > 0) { + const oa = message.args[0].value + if (this.cl) { + // if a cue is inserted, QLab sends playback changed cue + // before sending the new cue's id update, insert this id into + // the cuelist just in case so the playhead check will find it until then + if (!this.cueList[cl].includes(oa)) { + this.cueList[cl].push(oa) + } + } + if ((this.cl == '' || cl == this.cl) && oa !== this.nextCue) { + // playhead changed + this.nextCue = oa + this.log('debug', 'playhead: ' + oa) + this.sendOSC('/cue_id/' + oa + '/valuesForKeys', this.qCueRequest) + this.requestedCues[oa] = Date.now() } + break + } + case '[root group of cue lists]': + this.sendOSC('/doubleGoWindowRemaining') + break + case 'disconnect': + this.updateStatus(InstanceStatus.UnknownWarning, 'No Workspaces') + this.needWorkspace = true + this.needPasscode = false + this.lastRunID = 'x' + if (this.pulse != undefined) { + clearInterval(this.pulse) + this.pulse = undefined } - if ((self.cl == '' || cl == self.cl) && oa !== self.nextCue) { - // playhead changed - self.nextCue = oa - self.log('debug', 'playhead: ' + oa) - self.sendOSC('/cue_id/' + oa + '/valuesForKeys', self.qCueRequest) - self.requestedCues[oa] = Date.now() + this.resetVars(true) + this.prime_vars(ws) + break + case 'general': + // ug. 8 more bytes and they could have sent the 'new' value :( + this.sendOSC('/settings/general/minGoTime') + break + case 'overrides': + for (const o in Choices.OVERRIDE) { + this.sendOSC('/overrides/' + Choices.OVERRIDE[o].id, [], true) + } + break + case 'dashboard': // lighting, ignore for now + break + default: + if (ms.length == 3 && 'workspace' == ms[1]) { + // update to 'workspace' + this.sendOSC('/showMode', []) + this.sendOSC('/auditionWindow', [], true) + this.sendOSC('/overrideWindow', [], true) + } else if ('cue_id' == ms[3]) { + // get cue information for 'updated' cue + const node = '/' + ms.slice(1).join('/') + '/valuesForKeys' + const uniqueID = ms[4] + this.sendOSC(node, this.qCueRequest, true) + // save info request time to verify a response. + // QLab sends an update when a cue is deleted + // but fails to respond to a request for info. + // If there is no response after a few pulses + // we delete our copy of the cue + this.requestedCues[uniqueID] = Date.now() + } else { + this.log('debug', `====> unknown OSC message: ${ma} ` + JSON.stringify(message.args)) } - } else if (self.cl == '' || cl == self.cl) { - // no playhead - self.nextCue = '' - self.updateNextCue() - } - } else if (ma.match(/cue lists\]$/)) { - self.sendOSC('/doubleGoWindowRemaining') - } else if (ma.match(/\/cue_id\//)) { - // get cue information for 'updated' cue - var node = ma.substring(7) + '/valuesForKeys' - var uniqueID = ma.slice(-36) - self.sendOSC(node, self.qCueRequest) - // save info request time to verify a response. - // QLab sends an update when a cue is deleted - // but fails to respond to a request for info. - // If there is no response after a few pulses - // we delete our copy of the cue - self.requestedCues[uniqueID] = Date.now() - } else if (ma.match(/\/disconnect$/)) { - self.updateStatus(InstanceStatus.UnknownWarning, 'No Workspaces') - self.needWorkspace = true - self.needPasscode = false - self.lastRunID = 'x' - if (self.pulse != undefined) { - clearInterval(self.pulse) - self.pulse = undefined - } - self.resetVars(true) - self.prime_vars(ws) - } else if (mf.length == 4 && mf[2] == 'workspace') { - self.sendOSC('/showMode', []) - self.sendOSC('/auditionWindow', [], true) - self.sendOSC('/overrideWindow', [], true) - } else if (ma.match(/\/settings\/general$/)) { - // ug. 8 more bytes and they could have sent the 'new' value :( - self.sendOSC('/settings/general/minGoTime') - } else if (ma.match(/\/settings\/overrides$/)) { - for (var o in Choices.OVERRIDE) { - self.sendOSC('/overrides/' + Choices.OVERRIDE[o].id, [], true) - } } - // self.log('debug', '=====> OSC message: ' + ma + ' ' + JSON.stringify(message.args)) } /** * process QLab 'reply' */ readReply(message) { - var ws = this.ws - var ma = message.address - var j = {} - var i = 0 - var q - var uniqueID - var playheadId - var cl = this.cl - var qr = this.qCueRequest + let ws = this.ws + const ma = message.address + const mn = ma.split('/').slice(1) + let j = {} + const i = 0 + const cl = this.cl + const qr = this.qCueRequest try { j = JSON.parse(message.args[0].value) @@ -890,153 +890,182 @@ class QLabInstance extends InstanceBase { /* ingnore errors */ } - if (ma.match(/\/workspaces$/)) { - this.wsList = {} - if (j.data.length == 0) { - this.needPasscode = false - this.wrongPasscode = '' - this.needWorkspace = truethis.updateStatus(InstanceStatus.UnknownWarning, 'No Workspaces') - } else { - for (const w of j.data) { - ws = new Workspace(w) - this.wsList[ws.uniqueID] = ws + switch ( + mn.slice(-1)[0] // last segment of address + ) { + case 'workspaces': + this.wsList = {} + if (j.data.length == 0) { + this.needPasscode = false + this.wrongPasscode = '' + this.needWorkspace = true + this.updateStatus(InstanceStatus.UnknownWarning, 'No Workspaces') + } else { + for (const w of j.data) { + ws = new Workspace(w) + this.wsList[ws.uniqueID] = ws + } } - } - } else if (ma.match(/\/connect$/)) { - if (['badpass', 'denied'].includes(j.data)) { - if (!this.needPasscode) { - this.needPasscode = true - this.updateStatus(InstanceStatus.ConnectionFailure, 'Wrong Passcode') - this.wrongPasscode = this.config.passcode - this.wrongPasscodeAt = Date.now() - this.prime_vars(ws) + break + case 'connect': + if (['badpass', 'denied'].includes(j.data)) { + if (!this.needPasscode) { + this.needPasscode = true + this.updateStatus(InstanceStatus.ConnectionFailure, 'Wrong Passcode') + this.wrongPasscode = this.config.passcode + this.wrongPasscodeAt = Date.now() + this.prime_vars(ws) + } + } else if (j.data == 'error') { + this.needPasscode = false + this.needWorkspace = true + this.updateStatus(InstanceStatus.UnknownWarning, 'No Workspaces') + } else if (j.data.slice(0, 2) == 'ok') { + this.needPasscode = false + this.needWorkspace = false + this.wrongPasscode = '' + this.updateStatus(InstanceStatus.Ok, 'Connected to ' + this.host) } - } else if (j.data == 'error') { - this.needPasscode = false - this.needWorkspace = true - this.updateStatus(InstanceStatus.UnknownWarning, 'No Workspaces') - } else if (j.data.slice(0, 2) == 'ok') { - this.needPasscode = false - this.needWorkspace = false - this.wrongPasscode = '' - this.updateStatus(InstanceStatus.Ok, 'Connected to ' + this.host) - } - } else if (ma.match(/updates$/)) { - // only works on QLab > 3 - if ('denied' != j.status) { - this.needWorkspace = false - this.updateStatus(InstanceStatus.Ok, 'Connected to QLab') - if (this.pulse !== undefined) { - this.log('debug', 'cleared stray interval (u)') - clearInterval(this.pulse) + break + case 'updates': + // only works on QLab > 3 + if ('denied' != j.status) { + this.needWorkspace = false + this.updateStatus(InstanceStatus.Ok, 'Connected to QLab') + if (this.pulse !== undefined) { + this.log('debug', 'cleared stray interval (u)') + clearInterval(this.pulse) + } + this.pulse = setInterval( + () => { + this.rePulse() + }, + this.config.useTenths ? 100 : 250 + ) } - this.pulse = setInterval( - () => { - this.rePulse() - }, - this.config.useTenths ? 100 : 250 - ) - } - } else if (ma.match(/version$/)) { - if (j.data != undefined) { - this.qVer = parseInt(j.data) - this.setVariableValues({ q_ver: j.data }) - } - if (3 == this.qVer) { - // QLab3 always has a 'workspace' (it may be empty) - if (this.pulse !== undefined) { - this.log('debug', 'cleared stray interval (v)') - clearInterval(this.pulse) + break + case 'version': + if (j.data != undefined) { + this.qVer = parseInt(j.data) + this.setVariableValues({ q_ver: j.data }) } - this.pulse = setInterval( - () => { - this.rePulse() - }, - this.config.useTenths ? 100 : 250 - ) - } else { - this.needWorkspace = this.qVer > 3 && this.useTCP - } - } else if (ma.match(/uniqueID$/)) { - if (j.data) { - this.nextCue = j.data - this.updateNextCue() - this.sendOSC('/cue/playhead/valuesForKeys', qr) - } - } else if (ma.match(/playheadI[dD]$/)) { - if (j.data) { - playheadId = j.data - uniqueID = ma.substr(14, 36) - delete this.requestedCues[uniqueID] - if ((cl == '' || uniqueID == cl) && this.nextCue != playheadId) { - // playhead changed due to cue list change in QLab - if (playheadId == 'none') { - this.nextCue = '' - } else { - this.nextCue = playheadId + if (3 == this.qVer) { + // QLab3 always has a 'workspace' (it may be empty) + if (this.pulse !== undefined) { + this.log('debug', 'cleared stray interval (v)') + clearInterval(this.pulse) } + this.pulse = setInterval( + () => { + this.rePulse() + }, + this.config.useTenths ? 100 : 250 + ) + } else { + this.needWorkspace = this.qVer > 3 && this.useTCP + } + break + case 'uniqueID': + if (j.data) { + this.nextCue = j.data this.updateNextCue() - //this.sendOSC("/cue/" + j.data + "/children"); + this.sendOSC('/cue/playhead/valuesForKeys', qr) } - } - } else if (ma.match(/\/cueLists$/)) { - if (j.data) { - i = 0 - while (i < j.data.length) { - q = j.data[i] - this.updateCues(q, 'l') - this.updateCues(q.cues, 'l', q.uniqueID) - i++ + break + case 'playheadId': // pre q5 + case 'playheadID': // q5 + if (j.data) { + const playheadId = j.data + const uniqueID = ma.substr(14, 36) + delete this.requestedCues[uniqueID] + if ((cl == '' || uniqueID == cl) && this.nextCue != playheadId) { + // playhead changed due to cue list change in QLab + if (playheadId == 'none') { + this.nextCue = '' + } else { + this.nextCue = playheadId + } + this.updateNextCue() + this.sendOSC('/cue/' + j.data + '/children') + } } - this.sendOSC('/cue/active/valuesForKeys', qr) - } - } else if (ma.match(/children$/)) { - if (j.data) { - uniqueID = ma.substr(14, 36) - this.updateCues(j.data, 'u', uniqueID) - } - } else if (ma.match(/runningOrPausedCues$/)) { - if (j.data != undefined) { - i = 0 - while (i < j.data.length) { - q = j.data[i] - this.sendOSC('/cue_id/' + q.uniqueID + '/valuesForKeys', qr) - i++ + break + case 'cueLists': + if (j.data) { + for (const i in j.data) { + const q = j.data[i] + this.updateCues(q, 'l') + this.updateCues(q.cues, 'l', q.uniqueID) + } + this.sendOSC('/cue/active/valuesForKeys', qr) + } + break + case 'children': + if (j.data) { + let uniqueID = ma.substr(14, 36) + this.updateCues(j.data, 'u', uniqueID) + } + break + case 'runningOrPausedCues': + if (j.data != undefined) { + for (const i in j.data) { + const q = j.data[i] + this.sendOSC('/cue_id/' + q.uniqueID + '/valuesForKeys', qr) + } + } + break + case 'valuesForKeys': + this.updateCues(j.data, 'v') + delete this.requestedCues[j.data.uniqueID] + break + case 'showMode': + if (this.showMode != j.data) { + this.showMode = j.data + this.checkFeedbacks('ws_mode') + } + break + case 'showMode': + if (this.showMode != j.data) { + this.showMode = j.data + this.checkFeedbacks('ws_mode') + } + break + case 'auditionWindow': // pre q5 + case 'alwaysAudition': // q5 + if (this.auditMode != j.data) { + this.auditMode = j.data + this.checkFeedbacks('ws_mode') + } + break + case 'auditionMonitors': // q5 + if (this.auditMonitors != j.data) { + this.auditMonitors = j.data + this.checkFeedbacks('ws_audit') + } + break + case 'overrideWindow': + if (this.overrideWindow != j.data) { + this.overrideWindow = j.data + this.checkFeedbacks('override_visible') + } + break + case 'minGoTime': + this.minGo = j.data + this.setVariableValues({ min_go: (Math.round(j.data * 100) / 100).toFixed(2) }) + break + case 'doubleGoWindowRemaining': + const goLeft = Math.round(j.data * 1000) + this.goDisabled = goLeft > 0 + this.goAfter = Date.now() + goLeft + this.checkFeedbacks('min_go') + break + default: + if ('overrides' == mn[1]) { + this.overrides[mn[2]] = j.data + this.checkFeedbacks('override') + } else { + this.log('debug', `====> unknown OSC message: ${ma} ` + JSON.stringify(message.args)) } - } - } else if (ma.match(/valuesForKeys$/)) { - this.updateCues(j.data, 'v') - uniqueID = ma.substr(14, 36) - delete this.requestedCues[uniqueID] - } else if (ma.match(/showMode$/)) { - if (this.showMode != j.data) { - this.showMode = j.data - this.checkFeedbacks('ws_mode') - } - } else if (ma.match(/auditionWindow$/)) { - if (this.auditMode != j.data) { - this.auditMode = j.data - this.checkFeedbacks('ws_mode') - } - } else if (ma.match(/overrideWindow$/)) { - if (this.overrideWindow != j.data) { - this.overrideWindow = j.data - this.checkFeedbacks('override_visible') - } - } else if (ma.match(/minGoTime$/)) { - this.minGo = j.data - this.setVariableValues({ min_go: (Math.round(j.data * 100) / 100).toFixed(2) }) - } else if (ma.match(/\/doubleGoWindowRemaining$/)) { - var goLeft = Math.round(j.data * 1000) - this.goDisabled = goLeft > 0 - this.goAfter = Date.now() + goLeft - this.checkFeedbacks('min_go') - } else if (ma.match(/^\/reply\/overrides\//)) { - var o = ma.split('/')[3] - this.overrides[o] = j.data - this.checkFeedbacks('override') - } else { - this.log('debug', '=====> OSC message: ' + ma + ' ' + JSON.stringify(message.args)) } }