Control Package
Synchronous Python control of a running Luxar viewer through its control hub.
Driving a running Luxar viewer from Python.
A viewer launched with ?control attaches to the hub that
luxar serve --control exposes; this package is the other end of that hub.
The method names are the viewer’s own embedder API, so anything the browser’s
LuxarApp can do, a script here can ask for:
from luxar.control import Viewer
with Viewer("ws://kiosk.local:5173/control") as viewer:
story = viewer.dimension_index("story") # by name, never a position
viewer.set_dimension_value(story, 3) # fly to the fourth chapter
What it is for: scripted demos, reproducible screenshots, driving a kiosk from
a cron job, and prototyping an agent. What it is not: a second API. If a call
is not in the viewer’s embedder surface it is not here either — see
packages/luxar-viewer/src/core/app/control/method-policy.ts for the list
and docs/guides/specs/REMOTE_CONTROL_SPEC.md for the contract.
- exception luxar.control.ControlError(code: int, message: str)[source]
Bases:
RuntimeErrorA viewer (or the hub) refused a call.
codeis the JSON-RPC error code —-32601for a method the viewer’s policy does not expose,-32001for “no viewer attached”,-32603for a method that threw inside the viewer.
- class luxar.control.Viewer(url: str, *, token: str | None = None, timeout_s: float = 30.0)[source]
Bases:
objectA connected controller for one hub.
Use as a context manager, or call
close()yourself:with Viewer("ws://kiosk.local:5173/control") as viewer: viewer.recenter_camera()
- Parameters:
url – The hub’s WebSocket URL, as
luxar serve --controlprints it.token – Shared secret, when the hub was started with
--control-token.timeout_s – Per-call reply timeout. See
DEFAULT_TIMEOUT_S.
- call(method: str, *params: Any) Any[source]
Invoke an embedder method and return its result.
The escape hatch as much as the engine room: every named method below is one line of this, and a method with no wrapper yet is reachable as
viewer.call("setLayer", "/points", {"visible": False}).- Raises:
ControlError – the viewer or the hub refused the call.
TimeoutError – no reply arrived within the configured timeout.
websockets.exceptions.ConnectionClosed – the hub connection closed.
- notify(method: str, *params: Any) None[source]
Invoke a method without waiting for a reply.
Right for a fire-and-forget nudge (
notify("recenterCamera")) where the round trip is the only cost. An error is not reported back — the viewer logs it instead — so prefercall()when it matters.
- recv_event(timeout_s: float | None = None) Tuple[str, Any] | None[source]
Return the next event as
(name, payload).Not only the events this controller subscribed to: the hub fans a viewer’s events out to every attached controller, so a script that subscribed to nothing still sees whatever another controller — a touch panel, say — asked for. Match on
namerather than assuming.Events received while
call()waits for its reply are buffered and returned first. That buffer holds at mostMAX_BUFFERED_EVENTS; past the cap the OLDEST are dropped, so a long-lived controller that never reads events cannot grow without limit.timeout_s=Nonewaits indefinitely; a finite timeout returnsNonewhen no event arrives before its deadline.- Raises:
ConnectionClosed – If the socket closed while waiting. This reads the same socket as
call()and fails the same way.
- get_viewer_state() Dict[str, Any][source]
Everything at once: src, camera, dimensions, rendering, layers, audio.
- get_dimensions() Dict[str, Any][source]
Dimension metadata, including
categoriesfor a labelled axis.
- set_dimension_value(index: int, value: float) None[source]
Move one dimension — how a chapter jump is expressed.
The viewer’s waypoint driver does the rest: the camera flies, the dimension-bound overlays swap, and
waypoint-arrivedfires.
- dimension_index(name: str) int[source]
The positional index of a dimension, by name.
setDimensionValuetakes an index, but an index is a property of the scene’s dimension order and changes if the author reorders it. Looking it up by name is what makes a script survive that.- Raises:
KeyError – the scene has no dimension with that name.