modules/resi-cues/ — how long until the next Resi cue point, for a campus running video from another site.
The tile is one number as large as the tile allows: the countdown to the next cue, its name above it, the playhead and the cue just passed underneath, and the whole cue list behind a tile switch. It also declares "provides": ["timers"] and serves /timers + /timers/stream, so the countdown and the playhead appear among the ProPresenter timers in the Timers module without either side knowing about the other.
Both the cue list and the decoder's position come from Resi's cloud rather than the decoder — the decoder reports upstream and central.resi.io hands it back — so nothing is installed at the decoder end.
What replaced what
The first version of this branch was a Python script under modules/ with a LaunchAgent to keep it alive. The guide rules that out in as many words — everything a module does runs inside its server entry, no companion programs or daemons an operator starts by hand — and it could not put anything on a dashboard either, which was half the point. It is gone; the agent is uninstalled.
Driving the ProPresenter timer on the stage screen is Companion's job now. It is already a service on that machine, so nothing new has to be kept alive, and this module no longer reaches ProPresenter at all — it only has to be right about Resi, which is the hard part.
The part worth reading twice
Resi republishes a decoder's position only every few seconds, on an irregular cadence, and each reading is already stale when it appears. A countdown built straight from it sits seconds behind and lurches.
So in resi.js every reading is kept as media time minus wall time, and the largest recent value is trusted — the reading that arrived with the least delay — which a slow round trip cannot drag backwards. A reading outside the link's measured noise floor counts as a seek: drop the lock, snap, re-read the cue list at once. Playback is only called paused once the playhead has been still for longer than Resi's own cadence, because a fixed timeout mistakes a slow update for a stop and freezes the countdown mid-service.
The countdown then ticks in the browser: the server sends the playhead with the instant it was true, the tile adds the elapsed time since, and the Timers module extrapolates from startedAt the same way. A snapshot only goes out when something structural changes — a new cue, a pause, a seek — not at 1 Hz to every open dashboard.
Testing
resi.test.js drives the estimator on a fake clock against nine simulated Resi profiles (cadences 1–12s, staleness to 3s, irregular timing, variable round trips): lock-on, a fired cue, a seek backwards, a pause, and a pause longer than the offset window. A punctual fixture proves nothing here — against one, three separate breakages of the estimator go unnoticed, which is why the fixture is deliberately unkind.
server.test.js runs the real server entry against a canned Resi account and checks what the two consumers actually read, including the /timers shape field by field.
Both pass, and every mutation tried against either turns them red. Neither reaches the network. What is not covered: the live Resi calls against a real account, and the tile in a browser — this machine has no Node, so both suites were run under JavaScriptCore with a require shim.
`modules/resi-cues/` — how long until the next Resi cue point, for a campus running video from another site.
The tile is one number as large as the tile allows: the countdown to the next cue, its name above it, the playhead and the cue just passed underneath, and the whole cue list behind a tile switch. It also declares `"provides": ["timers"]` and serves `/timers` + `/timers/stream`, so the countdown and the playhead appear among the ProPresenter timers in the **Timers** module without either side knowing about the other.
Both the cue list and the decoder's position come from Resi's cloud rather than the decoder — the decoder reports upstream and `central.resi.io` hands it back — so nothing is installed at the decoder end.
### What replaced what
The first version of this branch was a Python script under `modules/` with a LaunchAgent to keep it alive. The guide rules that out in as many words — everything a module does runs inside its server entry, no companion programs or daemons an operator starts by hand — and it could not put anything on a dashboard either, which was half the point. It is gone; the agent is uninstalled.
Driving the ProPresenter timer on the stage screen is **Companion's** job now. It is already a service on that machine, so nothing new has to be kept alive, and this module no longer reaches ProPresenter at all — it only has to be right about Resi, which is the hard part.
### The part worth reading twice
Resi republishes a decoder's `position` only every few seconds, on an irregular cadence, and each reading is already stale when it appears. A countdown built straight from it sits seconds behind and lurches.
So in `resi.js` every reading is kept as *media time minus wall time*, and the **largest** recent value is trusted — the reading that arrived with the least delay — which a slow round trip cannot drag backwards. A reading outside the link's *measured* noise floor counts as a seek: drop the lock, snap, re-read the cue list at once. Playback is only called paused once the playhead has been still for longer than Resi's own cadence, because a fixed timeout mistakes a slow update for a stop and freezes the countdown mid-service.
The countdown then ticks in the browser: the server sends the playhead with the instant it was true, the tile adds the elapsed time since, and the Timers module extrapolates from `startedAt` the same way. A snapshot only goes out when something structural changes — a new cue, a pause, a seek — not at 1 Hz to every open dashboard.
### Testing
`resi.test.js` drives the estimator on a fake clock against nine simulated Resi profiles (cadences 1–12s, staleness to 3s, irregular timing, variable round trips): lock-on, a fired cue, a seek backwards, a pause, and a pause longer than the offset window. A punctual fixture proves nothing here — against one, three separate breakages of the estimator go unnoticed, which is why the fixture is deliberately unkind.
`server.test.js` runs the real server entry against a canned Resi account and checks what the two consumers actually read, including the `/timers` shape field by field.
node modules/resi-cues/resi.test.js # 57 tests
node modules/resi-cues/server.test.js # 10 tests
Both pass, and every mutation tried against either turns them red. Neither reaches the network. What is *not* covered: the live Resi calls against a real account, and the tile in a browser — this machine has no Node, so both suites were run under JavaScriptCore with a `require` shim.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
christian
added 1 commit 2026-09-24 11:36:17 -04:00
The ask was a Stream Deck button showing the time until the next Resi cue.
What it turned into is simpler: ProPresenter already renders countdowns, and
it already has an HTTP API, so nothing has to sit in between except something
that knows when the next cue is.
`resi_to_propresenter.py` watches a decoder's playhead and keeps a
ProPresenter countdown timer armed with the seconds until its next cue, with
the cue's name as a stage message beside it. Both halves of that come from
Resi's cloud rather than the decoder — the decoder reports its position
upstream and `central.resi.io` hands it back — so there is nothing to install
at the decoder end and nothing polling it every second.
The playhead is estimated rather than repeated. Resi republishes `position`
only every few seconds, irregularly, and each reading is already stale when
it appears; sent straight through, the countdown sat three seconds behind and
lurched. So each reading is kept as media time minus wall time and the
largest recent value is trusted — the reading that arrived with the least
delay — which a slow round trip can no longer drag backwards. A reading
outside the link's measured noise floor counts as a seek: drop the lock, snap,
and re-read the cue list immediately instead of on its timer. Playback is
only called paused once the playhead has been still for longer than Resi's own
cadence, because a fixed timeout mistakes a slow update for a stop and freezes
the countdown mid-service.
This is not a tile. There is no `module.json`, so `readManifests` skips the
folder and it never reaches the + picker; it lives under `modules/` because
that is where the production tooling lives. That is also why it carries a
README when no real module does — there is no *Included modules* entry it
could have instead — and the repo README says so where the module list ends.
`install-agent.command` sets up a LaunchAgent so it runs from login and
restarts if it stops. It deliberately runs a copy in Application Support: a
login agent cannot read `~/Documents` without Full Disk Access, and macOS
answers "Operation not permitted" rather than asking. Settings live there too,
outside the checkout, the same way module config does. The Resi password is
read from the Keychain and never written to disk; with none set the agent
says so once and waits rather than relaunching in a loop.
`test_playhead.py` drives the estimator on a fake clock against nine
simulated Resi profiles — update cadences of one to twelve seconds, staleness
to three, irregular timing, variable round trips — and six of seven deliberate
breakages of the estimator turn it red. `test_end_to_end.py` runs the real
loop against a real ProPresenter with a simulated decoder, and has: it armed
for the first cue, counted down in step, rolled over to the second and stopped
when the cues ran out. Neither covers the live Resi calls, which need real
credentials.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
christian
added 1 commit 2026-09-24 12:09:10 -04:00
Yesterday's version of this was a Python script under `modules/` with a
LaunchAgent to keep it alive — which the guide rules out in as many words:
everything a module does runs inside its server entry, no companion programs
or daemons an operator starts by hand. It also could not put anything on a
dashboard, which is half of what it was wanted for. Both halves are now in
the right place.
`modules/resi-cues/` is an ordinary module. Its tile is one number as large as
the tile allows — the countdown to the next Resi cue, the cue's name above it,
the playhead and the cue just passed underneath, and the whole cue list behind
a tile switch. It also declares `"provides": ["timers"]` and serves `/timers`
and `/timers/stream`, so the countdown and the playhead turn up among the
ProPresenter timers in the Timers module without either side knowing about
the other.
The countdown ticks in the browser. The server sends the playhead with the
instant it was true and whether playback is running, and the tile adds the
elapsed time since; the Timers module extrapolates from `startedAt` the same
way. So a snapshot only has to be sent when something structural changes — a
new cue, a pause, a seek — rather than at 1 Hz to every open dashboard.
`resi.js` holds the part worth reading twice. Resi republishes a decoder's
position only every few seconds, on an irregular cadence, and each reading is
already stale when it appears; a countdown built straight from it sits seconds
behind and lurches. So every reading is kept as media time minus wall time and
the largest recent value is trusted — the reading that arrived with the least
delay — which a slow round trip cannot drag backwards. A reading outside the
link's measured noise floor counts as a seek: drop the lock, snap, and re-read
the cue list at once instead of on its timer. Playback is only called paused
once the playhead has been still for longer than Resi's own cadence, because a
fixed timeout mistakes a slow update for a stop and freezes the countdown
mid-service.
Driving the ProPresenter timer on the stage screen is Companion's job now. It
is already a service on that machine, so nothing new has to be kept alive, and
this module no longer has to reach ProPresenter at all — it only has to be
right about Resi, which is the hard part.
`resi.test.js` drives the estimator on a fake clock against nine simulated
Resi profiles — cadences of one to twelve seconds, staleness to three,
irregular timing, variable round trips — covering lock-on, a fired cue, a seek
backwards, a pause, and a pause longer than the offset window. A punctual
fixture proves nothing here: against one, three separate breakages of the
estimator go unnoticed. `server.test.js` runs the real server entry against a
canned Resi account and checks what the two consumers actually read, including
the `/timers` shape field by field. Every mutation tried against either turns
them red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
christian
changed title from Resi cue countdowns reach the stage screen without Companion to Resi Cues: the countdown to the next cue, on the board and as a timer2026-09-24 12:09:28 -04:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
modules/resi-cues/— how long until the next Resi cue point, for a campus running video from another site.The tile is one number as large as the tile allows: the countdown to the next cue, its name above it, the playhead and the cue just passed underneath, and the whole cue list behind a tile switch. It also declares
"provides": ["timers"]and serves/timers+/timers/stream, so the countdown and the playhead appear among the ProPresenter timers in the Timers module without either side knowing about the other.Both the cue list and the decoder's position come from Resi's cloud rather than the decoder — the decoder reports upstream and
central.resi.iohands it back — so nothing is installed at the decoder end.What replaced what
The first version of this branch was a Python script under
modules/with a LaunchAgent to keep it alive. The guide rules that out in as many words — everything a module does runs inside its server entry, no companion programs or daemons an operator starts by hand — and it could not put anything on a dashboard either, which was half the point. It is gone; the agent is uninstalled.Driving the ProPresenter timer on the stage screen is Companion's job now. It is already a service on that machine, so nothing new has to be kept alive, and this module no longer reaches ProPresenter at all — it only has to be right about Resi, which is the hard part.
The part worth reading twice
Resi republishes a decoder's
positiononly every few seconds, on an irregular cadence, and each reading is already stale when it appears. A countdown built straight from it sits seconds behind and lurches.So in
resi.jsevery reading is kept as media time minus wall time, and the largest recent value is trusted — the reading that arrived with the least delay — which a slow round trip cannot drag backwards. A reading outside the link's measured noise floor counts as a seek: drop the lock, snap, re-read the cue list at once. Playback is only called paused once the playhead has been still for longer than Resi's own cadence, because a fixed timeout mistakes a slow update for a stop and freezes the countdown mid-service.The countdown then ticks in the browser: the server sends the playhead with the instant it was true, the tile adds the elapsed time since, and the Timers module extrapolates from
startedAtthe same way. A snapshot only goes out when something structural changes — a new cue, a pause, a seek — not at 1 Hz to every open dashboard.Testing
resi.test.jsdrives the estimator on a fake clock against nine simulated Resi profiles (cadences 1–12s, staleness to 3s, irregular timing, variable round trips): lock-on, a fired cue, a seek backwards, a pause, and a pause longer than the offset window. A punctual fixture proves nothing here — against one, three separate breakages of the estimator go unnoticed, which is why the fixture is deliberately unkind.server.test.jsruns the real server entry against a canned Resi account and checks what the two consumers actually read, including the/timersshape field by field.Both pass, and every mutation tried against either turns them red. Neither reaches the network. What is not covered: the live Resi calls against a real account, and the tile in a browser — this machine has no Node, so both suites were run under JavaScriptCore with a
requireshim.🤖 Generated with Claude Code
Resi cue countdowns reach the stage screen without Companionto Resi Cues: the countdown to the next cue, on the board and as a timer