Wiring a button on the Unreal side

One event in, a handful of functions out — all on PromethistLink. A graph never touches the subsystem hosting the socket:

Command What the graph does

"say"

Send Text To Agent (Value), and stop. Fire-and-forget: nothing goes back to the tablet.

"assistance"

Set Tablet State Bool ("assistancePending", true), then whatever fetches a human — the shipped graph waits five seconds — then Set Tablet State Bool ("assistancePending", false).

"myToggle"

Do the thing, then Set Tablet State Bool ("myToggle", bNowOn) — the new position, nothing else.

The assistance branch demonstrates reporting progress without an acknowledgement. Swap the delay for an HTTP call to Teams and the tablet requires no changes. It already renders a key and does not care what clears it. Publish false from BeginPlay too, as the shipped graph does: a level travel mid-wait destroys the Delay and the graph, so a key left published true locks the button lit with nothing alive to clear it.

A flip button needs its BeginPlay publish too, for the opposite reason. A tablet cannot know which way an unpublished toggle will move. Rather than guess, it keeps the button disabled. A graph that only publishes the key upon being pressed ships a control that can never be pressed. Nothing logs this. This is exactly what the shipped Subtitles button relies on: subtitles is published by the project, not the plugin.

The say branch implements the Joke button entirely. The phrase travels with the command, letting the graph forward Value without knowing the visitor’s language. Adding a second canned phrase requires a change only to the tablet.

On Tablet Command fires for every command, including those answered by C++, allowing projects to react to pause or volume without owning them. Unrecognised commands are logged rather than dropped, exposing typos on the first press. There are two exceptions that never reach the graph: sys.resync, which the socket layer answers directly, and messages refused by the per-client rate budget, logged as Dropped tablet message '<string>'.

Driving the operator controls from the project rather than from the tablet uses the same functions the tablet’s commands land on, avoiding a second path to maintain:

Blueprint node

Set Blackout / Is Blacked Out

take the installation out of service, or ask whether it is

Is Blacked Out By Operator

whether a person is holding it, as opposed to a failing check

Report Health

tell the component a named part of the installation works, or does not

Is Kiosk Healthy / Get Unhealthy Checks

ask what it currently believes about itself

Set Paused / Is Paused

the audio-and-microphone half of a pause

Get Locale

the language the next connect will use — never empty, so it can drive a Connect pin

Send Text To Agent

say something to the agent as if the visitor had

Set Tablet State Bool / Set Tablet State String

publish a key for a button to render from

Set Tablet State Number / Set Tablet State String Array

the same, for a number and for a list — use these rather than formatting either into a string, which the tablet would then have to parse back

SetVolumePercent, SetMicMuted, SetLocale and RestartSession are public C++ but not Blueprint nodes. They exist for the tablet’s commands to land on. The component exposes nodes only when needed, not in anticipation. To promote one, add a UFUNCTION(BlueprintCallable, Category = "Promethist|Control") line above it.

The setters publish their state key — inline for Set Blackout, and within the same frame for the rest, since MirrorTabletState() runs every tick — so the tablet follows changes it did not make. Is Blacked Out and Get Locale are pure reads, and Send Text To Agent carries no key. Events go the other way: On Blackout Changed, On Pause Changed, On Volume Changed, and On Locale Changed. Bind these to drive animations or widgets rather than watching the command channel for a press that may not have come from a tablet. On Blackout Changed also fires on the first tick after a level travel restores a lockout, so driven visuals return automatically.

Commands marked C++ above are answered inside UPromethistLinkComponent because they govern its own settings (audio, microphone, socket). A graph rewiring them manually would only recreate mistakes the component already avoids.

The session command is deliberately split: connecting is the graph’s, because the agent, level and backend are project decisions. C++ does exactly one thing here: it raises sessionBusy before the graph starts. Otherwise, a Blueprint connect taking seconds would leave the button reading "Start", inviting a second tap.

The state machine reaching Ready — meaning the graph actually calls Connect — is what clears it. A session handler connecting via another route never clears it. Consequently, Start, Restart and the language picker sit greyed out on all tablets until a 20-second watchdog gives up, leaving only one Warning line as a clue. If your graph answers session without reaching Connect, this watchdog is the contract you rely on.

This dividing line is worth stating, as it is crucial to get right when adding a button:

  • C++ when the command is the component — changing audio, the microphone, the socket, or whether connecting is allowed. Anything that must remain true regardless of the loaded graph belongs here. Blackout is the clearest case: as a Blueprint bool it would just be advisory, allowing any other connect path to bypass it.

  • Blueprint when the command is a project decision — what to show, what to play, what to say, which agent to connect to. A partner changes these without touching C++.

Either way, the graph still hears every command on On Tablet Command, so reacting to a natively-handled one costs nothing.