The reference tablet
The state keys named in the Reads and Locked by columns below are defined in the protocol’s state table — worth keeping open beside this page.
This table is a convenience, not the source of truth. The host declares the same vocabulary on the
wire. Connect and read server.commands in the handshake, which cannot go stale the way a table
can. See Finding out what a host accepts.
| Button | Sends | Reads | Locked by | Answered by |
|---|---|---|---|---|
Start / Stop |
|
|
|
Blueprint (+ C++ arms the lock) |
Pause |
|
|
|
C++ |
Subtitles |
|
|
Blueprint |
|
Call assistance |
|
|
while pending |
Blueprint |
Restart (corner) |
|
|
C++ |
|
Language (corner) |
|
|
|
C++ |
Volume (admin menu) |
|
|
C++ |
|
Microphone (admin menu) |
|
|
|
C++ |
Joke (admin menu) |
|
|
Blueprint |
|
Manual blackout (admin menu) |
|
|
— |
C++ |
There are three kinds, distinguished by the Reads column. A key that the button sends the
opposite of makes it a flip — it draws its pressed or idle state from that key and never
toggles itself. A key indicating the host is still working on this makes it fire-and-forget
with progress: one command out, then a spinner until the host says otherwise. No key at all means
plain fire-and-forget. See the three kinds of button.
Two flips — Microphone and Manual blackout — keep a single label and indicate their state by being lit. Both are latches: their label names a condition rather than an action, so it reads correctly whether applied or not. Start/Stop, Pause/Resume and Subtitles relabel, because they represent genuinely different actions.
Call assistance is the third kind, and the reason it exists. A real request reaches a person —
via a Teams notification, a pager, or a back-office bell — making the wait the interesting part.
Press it, and the host publishes assistancePending. The button lights up, swaps its label to
"Calling assistance…", shows a spinner instead of its icon, and refuses a second press until the
key clears. Nothing is acknowledged on the wire — the state key is the acknowledgement, meaning
every tablet in the room shows the same wait.
Like Manual blackout, it carries no other busy key: fetching a human is what someone reaches for
when the avatar is the problem, exactly when agentUnready would lock it.
Manual blackout changes what the host will do rather than what it is doing. It ends any
conversation and refuses every attempt to connect — the spacebar, another tablet’s Start, or a
post-travel resume — until lifted. It survives level travel and intentionally carries no busy key,
since it is needed precisely when the avatar is broken. Lifting it does not reconnect; someone must
press Start. Meanwhile, the visitor sees the project’s own widget and animation, bound to
OnBlackoutChanged.
The tablet fogs while it is in force, showing the same spinner an unreachable host gets, because out of service is how a kiosk’s controls should look when the kiosk itself is. This gives the tablet two types of unavailable behind one screen. The difference between them is deliberate:
| Fog | Controls behind it | Who can clear it | |
|---|---|---|---|
Out of service — someone latched it |
up |
live, and the admin menu is not auto-closed |
the operator, from the admin menu |
Out of service — the host’s own diagnostics latched it |
up |
live, admin menu not auto-closed |
the failing check passing again; |
Unreachable — no socket, silence, or |
up |
greyed, slider included |
nobody, and nothing needs to be |
The second row is not a limitation to work around. If the host cannot hear us, a press cannot arrive, and a live-looking button would only lie. Nothing is stranded behind it: a lockout lives in the host process, so a dead host took it with it, and a wedged host is what the game-thread watchdog restarts. An operator only loses the ability to change a state that nothing currently holds.
One line decides which row applies — hostCanAct() in main.ts. The fog, every button, and the
slider all ask it the same way, so they cannot disagree about the type of unavailable state.
Unavailable counts as unreachable for exactly this reason: the socket is alive and the phase
still reads ready, but with no avatar component behind it, a command would be sent, logged, and
answered by nobody.
In the first case, document.body.dataset.phase reads blackout, providing a hook to give that
screen a distinct look. The reference theme does not use it — the fog and spinner look the same for
both — so a deployment wanting them to differ has the hook available with no conflicting rules.
It does not have to be a person pressing it. Set Blackout is a Blueprint node, so anything in
the project can latch it. So can any WebSocket client, because this protocol is the box’s control
plane and a tablet is only its most obvious client. A watchdog noticing that the sensor or vision
process feeding your sessions has died can hold the installation out of service until its restart
succeeds. It connects, sends blackout with true, and sends it again with false when the
process is healthy. Every attached tablet fogs and clears accordingly, since all render one
published key.
And it does not have to be anything outside the plugin. The host takes itself out of service
while any of its own self-diagnostic checks is failing, requiring no setup — see Taking itself out
of service below. This forms a second latch, and the two cannot overrule each other. The
installation is out of service while either latch holds, so a healed check cannot lift a person’s
lockout, and a person cannot force a broken box back into serving visitors. blackout is the OR.
blackoutManual is the person’s half, which the tablet’s toggle renders so the control keeps
following the taps it receives.
Controls in the admin menu sit behind a three-second press on the logo, keeping them from visitors. It is the same press while the tablet is fogged, on the same spot. The fog forwards a hold starting inside the logo’s box and swallows everything else, preventing an absent host from locking operators out of their controls. An unreachable fog closes the admin menu when it appears — dead controls behind frosted glass — ensuring anything open over that spinner was deliberately opened there. An out-of-service fog leaves it open, because the toggle lifting an operator’s lockout is inside it. Note the limit here: the toggle only owns the operator’s latch, so it cannot lift one held by host diagnostics. Instead, the banner names the failing check, while pressing the toggle off is still honoured for the half it owns.
The language list comes from the host’s locales state key, meaning available languages are a
host change (AvailableLocales on the component), not a tablet change. What the corner looks
like belongs to this tablet. A code with no entry in ICONS gets no flag, falling back to a
caption. One with no entry in LANGUAGE_NAMES is spelled out letter by letter to a screen reader.
Both live in tablet.ts, making a new language one property in the editor plus two lines there.
How a button locks, with no acknowledgement on the wire. A busy key comes from the host and
broadcasts to every connected tablet at once, preventing two operators from fighting over one
control. A toggle needs nothing else: re-tapped before its state echo arrives, it reads the old
state and resends the same value, which the host no-ops. Only the stateless actions (restart,
say, assistance) can genuinely double-fire, and they carry a 500 ms tap debounce.