Controller interface
Live operator input state — sticks, triggers, buttons. Read-only; the consumer wires input from their gamepad / WebSocket / physical controller pipeline. Useful for direct teleop confirmation, deadzone debugging, and binding reference.
Live
Animated demo — sticks, trigger, and the REC button move as if the operator is driving. Below: two static fixtures showing idle and active states with binding labels.
Anatomy
Horizontal strip. Each section renders only when the consumer passes its data. The platform marker carries the Ember signature; operational controls stay neutral.
Optional leading cell — Ember dot + mono identifier. Drop when the controller sits next to a video tile that already names the source.
Circular SVG wells with a faint dashed deadzone, a centre-to-position trace, and a neutral fg dot. Numerical X / Y values below for precise reading.
Thin vertical bars filling from the bottom. 0 to 1 value displayed below. Use for continuous-value inputs (zoom, fine throttle).
Small mono pills with optional binding subtitle. Pressed state inverts the pill (bg-fg / text-bg) for high-contrast read-at-a-glance.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| platform | string | — | Optional platform identifier — e.g. "UGV-04". Renders as the leading Ember-dotted cell. |
| leftStick | StickState | — | Left stick state. { x, y } normalised to -1 / 1 per axis. |
| leftStickLabel | string | "STICK L" | Label above the left stick well — e.g. "DRIVE". |
| rightStick | StickState | — | Right stick state. |
| rightStickLabel | string | "STICK R" | Label above the right stick well — e.g. "GIMBAL". |
| leftTrigger | number | — | Left trigger value, 0 to 1. |
| leftTriggerLabel | string | "TRIG L" | Label above the left trigger bar. |
| rightTrigger | number | — | Right trigger value, 0 to 1. |
| rightTriggerLabel | string | "TRIG R" | Label above the right trigger bar. |
| buttons | ControllerButton[] | — | Button row. Each button has an id, a display label, optional binding subtitle, and live pressed state. |
| className | string | — | Forwarded to the root container. |
Types
StickStateNormalised stick position. Positive y is up by convention. Values outside [-1, 1] are clamped.
interface StickState {
x: number; // -1 (full left) to 1 (full right)
y: number; // -1 (full down) to 1 (full up)
}ControllerButtonPer-button state. The consumer drives `pressed` from the input pipeline; `binding` is the operator-facing what-it-does label.
interface ControllerButton {
id: string; // stable identifier
label: string; // displayed (e.g. "A", "RB", "STOP")
pressed?: boolean; // live press state — inverts the pill when true
binding?: string; // optional binding subtitle (e.g. "EMERGENCY STOP")
}Wiring
Read-only. The consumer reads the gamepad (or WebSocket from a physical controller) and pushes state in on each tick. RC3 does not own the input pipeline — same separation as video tile and telemetry HUD.
// Wired to the browser Gamepad API
const pad = navigator.getGamepads()[0];
<ControllerInterface
platform="UGV-04"
leftStick={{ x: pad.axes[0], y: -pad.axes[1] }}
leftStickLabel="DRIVE"
rightStick={{ x: pad.axes[2], y: -pad.axes[3] }}
rightStickLabel="GIMBAL"
leftTrigger={pad.buttons[6].value}
leftTriggerLabel="ZOOM−"
rightTrigger={pad.buttons[7].value}
rightTriggerLabel="ZOOM+"
buttons={[
{ id: "a", label: "A", binding: "HORN", pressed: pad.buttons[0].pressed },
{ id: "b", label: "B", binding: "REC", pressed: pad.buttons[1].pressed },
{ id: "x", label: "X", binding: "MARK", pressed: pad.buttons[2].pressed },
{ id: "y", label: "Y", binding: "RTL", pressed: pad.buttons[3].pressed },
]}
/>Behavioural rule
Telemetry never silently stale
When a stick or trigger goes unresponsive, omit its prop rather than holding the last-known value. A frozen stick reading masquerading as live input is the same failure shape as a stale video frame — operators act on signal that is no longer true.
Accessibility
| Live region | The strip carries `role="status"` and `aria-live="polite"`. Screen readers announce input transitions without interrupting the operator. |
|---|---|
| Labelled by platform | The root `aria-label` is of the form `Controller input for {platform}` when present, otherwise just `Controller input`. |
| Button pills | Each button carries `role="status"` and an `aria-label` of the form `{label} — {binding}, {pressed | released}` so screen readers announce both the binding and the live state. |
| Decorative visuals | Stick well graphics and trigger bar fills are `aria-hidden`. Labels and numerical values carry the meaning. |
| Colour and meaning | Pressed state uses contrast inversion, not colour alone — a button reads as pressed because it flips bg / fg, not because it changes hue. |
JavaFX
Ships in the PRIZM JavaFX library for thick-client C3 apps as Rc3ControllerInterface (extends HBox). Run the gallery to see it natively.
import design.prizm.fx.rc3.Rc3ControllerInterface;
Rc3ControllerInterface()| Member | Type | Default | Description |
|---|---|---|---|
| StickState | record(double x, double y) | — | -1..1 per axis; y positive up. |
| ControllerButton | record(String id, label, boolean pressed, String binding) | — | binding optional; pressed pills invert. |
| setPlatform | (String) → void | — | Ember-dotted leading cell. |
| setLeftStick / setRightStick | (StickState, String label) → void | — | Stick well + axis readout. |
| setLeftTrigger / setRightTrigger | (Double, String label) → void | — | Vertical trigger bar (0..1). |
| setButtons | (List<ControllerButton>) → void | — | Button pill row. |
| updateInputs | (StickState, StickState, Double, Double) → void | — | Live-update sticks + triggers in one rebuild — for frame-rate input feeds. |
Read-only input visualisation — the consumer wires the gamepad / WebSocket feed. Honours invariant 5 by extension (stop feeding a value rather than holding the last). Mirrors components/rc3/controller-interface.tsx.
Usage
Use whenever the operator is in direct teleop and benefits from seeing what their input is doing — deadzone confirmation, binding reference, and as a safety check that the input pipeline is live. Pass the binding labels even when the operator is experienced; the labels rot less than memory under stress. Touch / virtual-joystick input — where the operator drags the stick on the screen rather than on physical hardware — is a separate concern and not in this organism's scope.