Add real-time timer updates to Eos

This commit is contained in:
Ross Henderson
2026-04-06 20:57:28 +01:00
parent bf2a975b4b
commit f26d1a9d49
4 changed files with 238 additions and 15 deletions
+22 -9
View File
@@ -11,7 +11,9 @@
</tr>
<% } %>
<% if(isActive){ %>
<% if(isRunning){ %>
<tr class="running-cue scroll-position">
<% }else if(isComplete || isActive){ %>
<tr class="active-cue scroll-position">
<% }else{ %>
</tr>
@@ -53,7 +55,8 @@
<td class="black hide-medium">
<%= part.prettyDuration(part.beamTimeDuration, true) %>
</td>
<td class="hide-small"><%= part.prettyDuration(part.duration) %></td>
<% var durValue = (isRunning && remainingDuration !== null && remainingDuration !== undefined) ? remainingDuration : part.duration %>
<td class="hide-small"><%= part.prettyDuration(durValue) %></td>
<% }else{ %>
<td class="black" colspan="2"></td>
@@ -63,13 +66,23 @@
<td class="hide-small"></td>
<% } %>
<td class="black hide-small"><%= part.mark %></td>
<td class="black hide-small"><%= part.block %></td>
<td class="black hide-small"><%= part.assert %></td>
<td class="black hide-small">
<%= (part.follow>=0)? "F"+part.prettyDuration(part.follow) : "" %>
<%= (part.hang>=0)? "H"+part.prettyDuration(part.hang) : "" %>
</td>
<td class="black hide-small"><%= part.mark %></td>
<td class="black hide-small"><%= part.block %></td>
<td class="black hide-small"><%= part.assert %></td>
<% var followDisplay = (part.follow>=0)? "F"+part.prettyDuration(part.follow) : "" %>
<% var hangDisplay = (part.hang>=0)? "H"+part.prettyDuration(part.hang) : "" %>
<% var followHangDisplay = followDisplay + hangDisplay %>
<% var isFollowHangOnlyRunning = (followHangRemainingDuration !== null && followHangRemainingDuration !== undefined && followHangRemainingDuration > 0 && !isRunning) %>
<% if(followHangRemainingDuration !== null && followHangRemainingDuration !== undefined && followHangRemainingDuration > 0){ %>
<% if(part.follow >= 0){ %>
<% followHangDisplay = "F"+part.prettyDuration(followHangRemainingDuration) %>
<% }else if(part.hang >= 0 && !isRunning){ %>
<% followHangDisplay = "H"+part.prettyDuration(followHangRemainingDuration) %>
<% } %>
<% } %>
<td class="black hide-small <%= isFollowHangOnlyRunning ? 'running-follow-hang' : '' %>">
<%= followHangDisplay %>
</td>
<td class="black" style="text-align: left">
<%= (partNumber==0 ? "": "&nbsp;&nbsp;&nbsp;") %>
<%= part.label %>
+180 -4
View File
@@ -27,6 +27,9 @@ exports.config = {
action(_device) {
const device = _device;
device.data.cueListFilter = device.fields.cueListFilter;
if (device.data.EOS) {
subscribeToCueList(device);
}
device.draw();
},
},
@@ -40,10 +43,14 @@ exports.ready = function ready(_device) {
cue: _.template(fs.readFileSync(path.join(__dirname, `cue.ejs`))),
};
device.data.cueListFilter = device.fields.cueListFilter;
device.data.EOS.progressCueList = resolveConfiguredCueList(device);
device.send('/eos/get/cuelist/count');
device.send('/eos/get/version');
device.send('/eos/subscribe', [{ type: 'i', value: 1 }]);
// subscribe to the selected cue list specifically
subscribeToCueList(device);
};
exports.data = function data(_device, osc) {
@@ -80,6 +87,7 @@ exports.data = function data(_device, osc) {
device.update('cueData', {
cue: device.data.EOS.cueLists[addressParts[4]][addressParts[5]],
cueListNumber: addressParts[4],
cueNumber: addressParts[5],
uid: osc.args[1],
});
@@ -92,9 +100,88 @@ exports.data = function data(_device, osc) {
if (osc.args.length === 3) {
device.data.EOS.cueLists[addressParts[4]][addressParts[5]][0].extLinks = osc.args[2];
}
} else if (match(addressParts, ['eos', 'out', 'cuelist', '8001', '1'])) {
const cueListNumber = resolveConfiguredCueList(device);
const cueNumber = cueNumberValue(osc.args[1]);
const totalMs = nonNegativeInt(osc.args[6]);
const cueRemainingMs = nonNegativeInt(osc.args[7]);
const previousCueNumber = cueNumberValue(device.data.EOS.activeCueByList[cueListNumber]);
if (cueNumber !== undefined) {
device.data.EOS.activeCueByList[cueListNumber] = cueNumber;
device.data.EOS.activeCue = cueNumber;
}
if (cueNumber !== undefined || cueRemainingMs !== null || totalMs !== null) {
const progress = device.data.EOS.progressByList[cueListNumber] || {};
if (cueNumber !== undefined) {
progress.cueNumber = cueNumber;
}
if (cueRemainingMs !== null) {
progress.cueRemainingMs = cueRemainingMs;
}
if (totalMs !== null) {
progress.totalMs = totalMs;
}
device.data.EOS.progressByList[cueListNumber] = progress;
}
if (previousCueNumber !== undefined && previousCueNumber !== cueNumber) {
device.update('cueState', {
cueListNumber,
cueNumber: previousCueNumber,
});
}
if (cueNumber !== undefined) {
device.update('cueState', {
cueListNumber,
cueNumber,
});
}
} else if (match(addressParts, ['eos', 'out', 'cuelist', '8001'])) {
const cueListNumber = resolveConfiguredCueList(device);
const followHangRemainingMs = nonNegativeInt(osc.args[2]);
if (followHangRemainingMs !== null) {
const progress = device.data.EOS.progressByList[cueListNumber] || {};
const activeCueForList = cueNumberValue(device.data.EOS.activeCueByList[cueListNumber]);
if (progress.cueNumber === undefined && activeCueForList !== undefined) {
progress.cueNumber = activeCueForList;
}
progress.followHangRemainingMs = followHangRemainingMs;
device.data.EOS.progressByList[cueListNumber] = progress;
if (progress.cueNumber !== undefined) {
device.update('cueState', {
cueListNumber,
cueNumber: progress.cueNumber,
});
}
}
} else if (match(addressParts, ['eos', 'out', 'event', 'cue', '*', '*', 'fire'])) {
device.data.EOS.activeCue = addressParts[5];
device.draw();
const cueListNumber = cueNumberValue(addressParts[4]);
const cueNumber = cueNumberValue(addressParts[5]);
const previousCueNumber = cueNumberValue(device.data.EOS.activeCueByList[cueListNumber]);
device.data.EOS.activeCue = cueNumber;
device.data.EOS.activeCueByList[cueListNumber] = cueNumber;
device.data.EOS.progressByList[cueListNumber] = {
cueNumber,
cueRemainingMs: null,
followHangRemainingMs: null,
totalMs: null,
};
if (previousCueNumber !== undefined && previousCueNumber !== cueNumber) {
device.update('cueState', {
cueListNumber,
cueNumber: previousCueNumber,
});
}
device.update('cueState', {
cueListNumber,
cueNumber,
});
const cues = device.data.EOS.cueLists[addressParts[4]][addressParts[5]];
if (cues) {
device.update('activeCue', {
@@ -115,18 +202,48 @@ exports.data = function data(_device, osc) {
exports.update = function update(device, doc, updateType, data) {
if (updateType === 'cueData') {
const $elem = doc.getElementById(data.uid);
const cueState = cueRenderState(device, data.cueListNumber, data.cueNumber);
if ($elem) {
$elem.outerHTML = device.templates.cue({
q: data.cue,
cueNumber: data.cueNumber,
isActive: false,
isActive: cueState.isComplete,
isComplete: cueState.isComplete,
isRunning: cueState.isRunning,
remainingDuration: cueState.remainingDuration,
followHangRemainingDuration: cueState.followHangRemainingDuration,
});
} else {
device.draw();
}
} else if (updateType === 'cueState') {
const cueListNumber = cueNumberValue(data.cueListNumber);
const cueNumber = cueNumberValue(data.cueNumber);
const cues = device.data.EOS.cueLists?.[cueListNumber]?.[cueNumber];
if (!cues || !cues[0]) {
device.draw();
return;
}
const $elem = doc.getElementById(cues[0].uid);
if ($elem) {
const cueState = cueRenderState(device, cueListNumber, cueNumber);
$elem.outerHTML = device.templates.cue({
q: cues,
cueNumber,
isActive: cueState.isComplete,
isComplete: cueState.isComplete,
isRunning: cueState.isRunning,
remainingDuration: cueState.remainingDuration,
followHangRemainingDuration: cueState.followHangRemainingDuration,
});
} else {
device.draw();
}
} else if (updateType === 'activeCue') {
const $elem = doc.getElementById(data.uid);
$elem.scrollIntoView({ behavior: 'smooth', block: 'center' });
if ($elem) {
$elem.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
};
@@ -134,6 +251,62 @@ exports.heartbeat = function heartbeat(device) {
device.send('/eos/ping');
};
function resolveConfiguredCueList(device) {
const cueListFilter = cueNumberValue(device.data.cueListFilter);
return cueListFilter || '1';
}
function subscribeToCueList(device) {
const cueListNumber = resolveConfiguredCueList(device);
const eosState = device.data.EOS;
eosState.progressCueList = cueListNumber;
device.send(`/eos/cuelist/8001/config/${cueListNumber}/0/0`);
}
function cueRenderState(device, cueListNumber, cueNumber) {
const list = cueNumberValue(cueListNumber);
const cue = cueNumberValue(cueNumber);
const progress = device.data.EOS.progressByList[list];
const activeCue = cueNumberValue(device.data.EOS.activeCueByList[list]);
const cueRemainingMs = progress?.cueRemainingMs;
const followHangRemainingMs = progress?.followHangRemainingMs;
const isComplete = cue === activeCue;
const isRunning =
isComplete &&
progress !== undefined &&
cue === cueNumberValue(progress.cueNumber) &&
cueRemainingMs !== null &&
cueRemainingMs > 0;
const hasFollowHangCountdown = isComplete && followHangRemainingMs !== null && followHangRemainingMs > 0;
return {
isRunning,
isComplete,
remainingDuration: isRunning ? cueRemainingMs : null,
followHangRemainingDuration: hasFollowHangCountdown ? followHangRemainingMs : null,
};
}
function cueNumberValue(value) {
if (value === undefined || value === null) {
return undefined;
}
return `${value}`.trim();
}
function nonNegativeInt(value) {
if (value === undefined || value === null) {
return null;
}
const numericValue = Number(value);
if (!Number.isFinite(numericValue)) {
return null;
}
return Math.max(0, Math.round(numericValue));
}
function match(testArray, patternArray) {
let out = true;
if (testArray.length !== patternArray.length) {
@@ -152,5 +325,8 @@ class EOS {
this.showName = '';
this.cueLists = {};
this.activeCue = undefined;
this.activeCueByList = {};
this.progressByList = {};
this.progressCueList = '1';
}
}
+19
View File
@@ -63,6 +63,25 @@ tr.active-cue .time {
border-color: #c78b07;
}
tr.running-cue {
color: #ae0009;
}
tr.running-cue td.num div {
margin: 0px;
padding: 1px 6px;
background-color: #ae0009;
border-radius: 4px;
color: white;
}
tr.running-cue .time {
border-color: #ae0009;
}
td.running-follow-hang {
color: #ae0009;
font-weight: 700;
}
.list_name {
color: lightgray;
margin-left: 5px;
+17 -2
View File
@@ -25,15 +25,30 @@
</tr>
<% var cues = Object.keys(data.EOS.cueLists[i]).sort(function(a, b){return Number(a)-Number(b)}) %>
<% var activeCueByList = data.EOS.activeCueByList || {} %>
<% var progressByList = data.EOS.progressByList || {} %>
<% var activeCueNumber = (activeCueByList[i] !== undefined) ? (activeCueByList[i] + "") : undefined %>
<% var progressState = progressByList[i] || {} %>
<% var progressCueNumber = (progressState.cueNumber !== undefined && progressState.cueNumber !== null) ? (progressState.cueNumber + "") : undefined %>
<% var hasCueRemaining = (progressState.cueRemainingMs !== undefined && progressState.cueRemainingMs !== null) %>
<% var hasFollowHangRemaining = (progressState.followHangRemainingMs !== undefined && progressState.followHangRemainingMs !== null) %>
<% for(var j=0; j<cues.length; j++){ %>
<% var q = data.EOS.cueLists[i][cues[j]] %>
<% var isComplete = (activeCueNumber !== undefined && cues[j] == activeCueNumber) %>
<% var isRunning = isComplete && hasCueRemaining && progressCueNumber === cues[j] && progressState.cueRemainingMs > 0 %>
<% var remainingDuration = isRunning ? progressState.cueRemainingMs : null %>
<% var followHangRemainingDuration = (isComplete && !isRunning && hasFollowHangRemaining && progressState.followHangRemainingMs > 0) ? progressState.followHangRemainingMs : null %>
<%= templates.cue({
q: q,
cues: cues,
cueNumber: cues[j],
isActive: (cues[j]==data.EOS.activeCue+"")
isActive: isComplete,
isComplete: isComplete,
isRunning: isRunning,
remainingDuration: remainingDuration,
followHangRemainingDuration: followHangRemainingDuration
}) %>
<% } %>