Resi cue countdowns on ProPresenter and in Companion

The Resi -> ProPresenter bridge, its installer and site kit, and the
Companion module resi-studio-cues, as running at North Attleboro.

Bridge (resi_to_propresenter.py)
- Follows a Resi decoder's playhead and keeps ProPresenter's NextCue timer
  counting down to the next cue; optional cue name as a stage message, sent
  once per cue (ProPresenter flashes it on every send) and off by default.
- Playhead estimate no longer learns a cadence from the partial first gap
  after a start, so a 30s Resi doesn't read as paused half the time.
- Backs off on 429, holds 2 min before retrying a failed startup login, and
  logs Resi's actual reason (wrong password, USER_LOCKED, rate limit).
- --setup asks for the login, decoder, ProPresenter and timer.

install_bridge.sh installs it as a login service (outside ~/Documents,
which launchd may not read); make_kit.sh builds the kit for another site.

Companion module resi-studio-cues 0.1.4 (source/, package committed)
- Same playhead fix; login and request hold-off (15s doubling to 5 min);
  a 401/403 on a fresh session is a permission, not a reason to sign in
  again; init never fails on a Resi problem, so Companion can't restart it
  into a login loop.

Tests: test_playhead.py, test_stage_message.py, test_rate_limit.py and
source/backoff_test.js run offline; test_end_to_end.py drives a real
ProPresenter and refuses to run without an explicit flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 18:11:01 -04:00
co-authored by Claude Opus 5.5
commit 1a658875bc
24 changed files with 4548 additions and 0 deletions
+303
View File
@@ -0,0 +1,303 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UpdatePresets = UpdatePresets;
const base_1 = require("@companion-module/base");
function UpdatePresets(self) {
const presets = {};
// Helper to reference THIS instance’s variables in button text
// Companion variable syntax uses the instance label.
const L = self.label;
// ----------------------------
// STATUS / HEALTH
// ----------------------------
presets['status_connected'] = {
type: 'button',
category: 'Resi • Status',
name: 'Connected (green) / Error (red)',
style: {
text: `Resi\n$(${L}:auth_status)`,
size: '14',
color: (0, base_1.combineRgb)(255, 255, 255),
bgcolor: (0, base_1.combineRgb)(40, 40, 40),
show_topbar: true,
},
steps: [],
feedbacks: [
{
feedbackId: 'resi_connected',
options: {},
},
{
feedbackId: 'resi_error',
options: {},
},
],
};
presets['status_player_active'] = {
type: 'button',
category: 'Resi • Status',
name: 'Player Active',
style: {
text: `Player\n$(${L}:current_position)`,
size: '14',
color: (0, base_1.combineRgb)(255, 255, 255),
bgcolor: (0, base_1.combineRgb)(40, 40, 40),
show_topbar: true,
},
steps: [],
feedbacks: [
{
feedbackId: 'player_active',
options: {},
},
],
};
presets['status_frozen'] = {
type: 'button',
category: 'Resi • Status',
name: 'Playback Frozen Warning',
style: {
text: `FROZEN?\n$(${L}:current_position)`,
size: '14',
color: (0, base_1.combineRgb)(255, 255, 255),
bgcolor: (0, base_1.combineRgb)(40, 40, 40),
show_topbar: true,
},
steps: [],
feedbacks: [
{
feedbackId: 'playback_frozen',
options: {
staleSeconds: 6,
},
},
],
};
// ----------------------------
// CUE DISPLAY (NO ACTIONS)
// ----------------------------
presets['cue_next_display'] = {
type: 'button',
category: 'Resi • Cues',
name: 'Next Cue Display',
style: {
text: `NEXT:\n$(${L}:next_cue_name)\nT- $(${L}:next_cue_time_in_hms)`,
size: '14',
color: (0, base_1.combineRgb)(255, 255, 255),
bgcolor: (0, base_1.combineRgb)(0, 0, 0),
show_topbar: false,
},
steps: [],
feedbacks: [
{
feedbackId: 'approaching_next_cue',
options: { threshold: 30 },
},
],
};
presets['cue_prev_display'] = {
type: 'button',
category: 'Resi • Cues',
name: 'Previous Cue Display',
style: {
text: `PREV:\n$(${L}:prev_cue_name)\n+ $(${L}:prev_cue_time_ago_in_hms)`,
size: '14',
color: (0, base_1.combineRgb)(255, 255, 255),
bgcolor: (0, base_1.combineRgb)(0, 0, 0),
show_topbar: false,
},
steps: [],
feedbacks: [],
};
// ----------------------------
// MAINTENANCE / REFRESH
// ----------------------------
presets['refresh_profile'] = {
type: 'button',
category: 'Resi • Maintenance',
name: 'Refresh Profile',
style: {
text: 'Refresh\nProfile',
size: '18',
color: (0, base_1.combineRgb)(255, 255, 255),
bgcolor: (0, base_1.combineRgb)(60, 60, 60),
show_topbar: false,
},
steps: [
{
down: [
{
actionId: 'refresh_profile',
options: {},
},
],
up: [],
},
],
feedbacks: [
{ feedbackId: 'resi_connected', options: {} },
{ feedbackId: 'resi_error', options: {} },
],
};
presets['refresh_lists'] = {
type: 'button',
category: 'Resi • Maintenance',
name: 'Refresh Venues/Players',
style: {
text: 'Refresh\nLists',
size: '18',
color: (0, base_1.combineRgb)(255, 255, 255),
bgcolor: (0, base_1.combineRgb)(60, 60, 60),
show_topbar: false,
},
steps: [
{
down: [
{
actionId: 'refresh_lists',
options: {},
},
],
up: [],
},
],
feedbacks: [
{ feedbackId: 'resi_connected', options: {} },
{ feedbackId: 'resi_error', options: {} },
],
};
// ----------------------------
// CUE ACTIONS
// ----------------------------
presets['add_cue_player'] = {
type: 'button',
category: 'Resi • Cues',
name: 'Add Cue @ Player Position',
style: {
text: 'Add Cue\n@ Player',
size: '18',
color: (0, base_1.combineRgb)(0, 0, 0),
bgcolor: (0, base_1.combineRgb)(180, 180, 180),
show_topbar: false,
},
steps: [
{
down: [
{
actionId: 'add_cue_at_position',
options: {
name: 'Web Cue',
privateCue: false,
positionMode: 'player',
customPosition: '00:00:00.000',
},
},
],
up: [],
},
],
feedbacks: [
{ feedbackId: 'resi_connected', options: {} },
{ feedbackId: 'resi_error', options: {} },
],
};
presets['add_cue_custom'] = {
type: 'button',
category: 'Resi • Cues',
name: 'Add Cue @ Custom Time',
style: {
text: 'Add Cue\n@ Custom',
size: '18',
color: (0, base_1.combineRgb)(0, 0, 0),
bgcolor: (0, base_1.combineRgb)(180, 180, 180),
show_topbar: false,
},
steps: [
{
down: [
{
actionId: 'add_cue_at_position',
options: {
// user can edit this preset, OR replace with a variable
name: 'Custom Cue',
privateCue: false,
positionMode: 'custom',
customPosition: '00:10:00.000',
},
},
],
up: [],
},
],
feedbacks: [
{ feedbackId: 'resi_connected', options: {} },
{ feedbackId: 'resi_error', options: {} },
],
};
presets['lookup_cue'] = {
type: 'button',
category: 'Resi • Cues',
name: 'Lookup Cue (stores in lookup_* vars)',
style: {
text: 'Lookup\nCue',
size: '18',
color: (0, base_1.combineRgb)(255, 255, 255),
bgcolor: (0, base_1.combineRgb)(0, 110, 200),
show_topbar: false,
},
steps: [
{
down: [
{
actionId: 'lookup_cue_by_name',
options: {
// user edits this preset to their cue name or uses variables
cueName: 'sermon start',
matchMode: 'exact',
},
},
],
up: [],
},
],
feedbacks: [
{ feedbackId: 'resi_connected', options: {} },
{ feedbackId: 'resi_error', options: {} },
],
};
presets['delete_cue'] = {
type: 'button',
category: 'Resi • Cues',
name: 'Delete Cue by Name',
style: {
text: 'DELETE\nCue',
size: '18',
color: (0, base_1.combineRgb)(255, 255, 255),
bgcolor: (0, base_1.combineRgb)(140, 0, 0),
show_topbar: false,
},
steps: [
{
down: [
{
actionId: 'delete_cue_by_name',
options: {
// user edits this preset to the cue they want to delete
cueName: 'Companion Test',
matchMode: 'exact',
deleteMode: 'first',
onlyMine: true,
},
},
],
up: [],
},
],
feedbacks: [
{ feedbackId: 'resi_connected', options: {} },
{ feedbackId: 'resi_error', options: {} },
],
};
self.setPresetDefinitions(presets);
}
//# sourceMappingURL=presets.js.map