Telemetry HUD
Operational telemetry for a UXV — aerial, ground, surface, or underwater. The same organism, four domain dialects. Two postures: a compact strip for edge overlays, and a four-edge frame for centred map / 3D / video viewports.
Live · frame mode
Four-edge layout around a centred viewport. The consumer's map, 3D view, or video sits in the children slot; telemetry pins to the edges. Top: heading + platform. Left: speed + roll. Right: vertical + pitch. Bottom: battery, vertical rate, and any domain-specific cells. The centre stays clear.
Live · domains · strip mode
Four domains, four label dialects. The vertical concept changes — `ALT` for aerial, `ELEV` for ground, `DEPTH` for surface and underwater. Vertical rate reads `V/S` for aerial, `DIVE` for underwater. Domain-specific cells (`SLOPE` for ground, `BOT` for underwater) surface only when relevant.
Live · stale field
Heading has stopped updating. Rather than presenting a frozen `174°` as if it were live, the cell dims its value and carries a `STALE` age tag — the operator sees the degradation before acting on it. Fresh cells around it are unaffected. This is invariant 5 in the organism itself: mark stale, never hold a silent last-good value.
Anatomy
Same cells, two postures. The cell set is determined by what the consumer passes; the posture is determined by `mode`. Labels resolve from `domain`.
Aerial / ground / surface / underwater. Resolves the vertical concept (ALT / ELEV / DEPTH) and the vertical rate (V/S / DIVE) without forking the organism.
Optional leading cell. Ember dot + mono identifier. Marks 'this is the active RC3 platform.' Drop when the HUD overlays a video tile that already names the source.
Inline horizontal row. Cells flow with hairline dividers, flex-wraps on narrow viewports. Best for edge overlays and side panels.
Four-edge container with the consumer's viewport in the centre. Top: heading + platform. Left: speed + roll. Right: vertical + pitch. Bottom: charge + rate + domain cells.
Success > 50%, warning 20–50%, danger < 20%. Operators read state before reading the number.
A field passed in `stale` dims its value and gains a STALE age tag. The frozen reading stays visible but unmistakably degraded — never silently fresh-looking.
SLOPE (ground only) and BOT — altitude above bottom — surface where the platform's domain makes them meaningful.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| domain | "aerial" | "ground" | "surface" | "underwater" | "aerial" | UXV domain. Drives label semantics: vertical reads as ALT / ELEV / DEPTH; verticalRate reads as V/S / DIVE. Domain-specific cells (slope, altitudeAboveBottom) surface only when meaningful. |
| mode | "strip" | "frame" | "strip" | Layout. strip is an inline horizontal row, suited to edge overlays and side panels. frame is a four-edge container around a centred viewport (map, 3D, video). |
| platform | string | — | Optional platform identifier — e.g. "UGV-04". Renders as the leading Ember-dotted cell. |
| speed | number | — | Speed value. Unit defaults to m/s. |
| speedUnit | "m/s" | "km/h" | "kn" | "m/s" | Speed unit. m/s rounds to one decimal; km/h and kn round to integer. |
| vertical | number | — | Vertical position. Aerial: altitude. Ground: elevation. Surface / underwater: depth. Label resolves from domain. |
| verticalUnit | "m" | "ft" | "m" | Vertical unit. |
| verticalRef | string | — | Optional reference marker appended to the vertical cell — e.g. "AGL" / "MSL" / "BLW". |
| verticalRate | number | — | Vertical rate in m/s. Signed. Label resolves from domain — V/S for aerial, DIVE for underwater. |
| heading | number | — | Heading in degrees (0–359). Rendered three-digit padded — HDG 048°. |
| battery | number | — | Battery state of charge (0–100). Colour-coded — success > 50, warning 20–50, danger < 20. |
| fuel | number | — | Fuel level (0–100). Mutually exclusive with battery; if both are passed, battery wins. |
| roll | number | — | Roll angle in degrees. Signed. |
| pitch | number | — | Pitch angle in degrees. Signed. |
| slope | number | — | Ground-only — slope / grade in degrees. Signed. Renders only when domain="ground". |
| altitudeAboveBottom | number | — | Distance to bottom in metres. Sounder reading; meaningful for underwater and deep-terrain ground platforms. |
| stale | Partial<Record<TelemetryField, number>> | — | Per-field staleness, in seconds since the last fresh update. A listed field keeps its last-known value but renders dimmed with a STALE age tag — a frozen reading never passes for live. Honours invariant 5. |
| children | ReactNode | — | Rendered inside the frame centre when mode="frame". Ignored in strip mode. |
| className | string | — | Forwarded to the root container. |
Types
UxvDomainUXV domain. Drives label semantics for the vertical concept and vertical rate.
type UxvDomain = "aerial" | "ground" | "surface" | "underwater";SpeedUnitSpeed unit. Use `kn` for aviation / surface, `m/s` for ground / underwater.
type SpeedUnit = "m/s" | "km/h" | "kn";VerticalUnitVertical unit. `ft` typical for aviation; `m` for everything else.
type VerticalUnit = "m" | "ft";HudModeLayout posture.
type HudMode = "strip" | "frame";TelemetryFieldThe fields that can be marked stale via the `stale` prop. Keyed by the feeding prop.
type TelemetryField =
| "speed" | "vertical" | "heading"
| "battery" | "fuel" | "roll" | "pitch"
| "verticalRate" | "slope" | "altitudeAboveBottom";Wiring
Read-only — pass whichever fields the platform surfaces. When a field stops updating, mark it stale via `stale` rather than letting a frozen value look live. Set `domain` to the platform class.
// Aerial — strip mode beside a video tile
<TelemetryHud
domain="aerial"
platform="UAV-09"
speed={state.airspeed}
speedUnit="kn"
vertical={state.altitudeFt}
verticalUnit="ft"
verticalRef="AGL"
heading={state.heading}
battery={state.batteryPct}
roll={state.attitude.roll}
pitch={state.attitude.pitch}
verticalRate={state.climbRate}
/>// Underwater — DEPTH and DIVE labels resolve from domain
<TelemetryHud
domain="underwater"
platform="UUV-11"
speed={state.speed}
vertical={state.depth}
heading={state.heading}
battery={state.batteryPct}
pitch={state.diveAngle}
verticalRate={state.diveRate}
altitudeAboveBottom={state.sounderM}
/>// Frame mode — four-edge overlay around a map / 3D viewport
<TelemetryHud mode="frame" domain="aerial" ...telemetry>
<YourMapEngine />
</TelemetryHud>// Mark a field stale when it stops updating — invariant 5
<TelemetryHud
domain="ground"
platform="UGV-04"
speed={state.speed}
heading={state.lastHeading}
battery={state.batteryPct}
stale={{ heading: secondsSince(state.headingUpdatedAt) }}
/>Behavioural rule
Telemetry never silently stale
When a field stops updating, mark it stale via `stale` — the value dims and gains a STALE age tag so a frozen reading can never pass for live. A degraded battery surfaces in danger before it becomes critical; a stalled heading reads as stale, not as a fresh fix.
Accessibility
| Live region | The HUD carries `role="status"` and `aria-live="polite"`. Screen readers announce telemetry transitions without interrupting the operator. |
|---|---|
| Labelled by platform | The root `aria-label` is of the form `Telemetry for {platform}` when a platform identifier is present, otherwise just `Telemetry`. |
| Decorative parts | The Ember dot and the cell dividers are `aria-hidden`. Cell labels and values carry the meaning. |
| Colour and meaning | Battery / fuel state colour is paired with the numeric value — operators with colour-vision differences still read the percentage. The colour amplifies, never replaces, the signal. |
| Stale not colour-only | A stale field carries the literal `STALE {age}` text alongside the dimmed value — the degradation is legible without relying on the danger colour. The `aria-live` region announces the change. |
| Tabular nums | Values use `tabular-nums` so digits do not shift width as values change. The strip stays scannable when the operator's eye returns to the same cell repeatedly. |
JavaFX
Ships in the PRIZM JavaFX library for thick-client C3 apps as Rc3TelemetryHud (extends StackPane). Run the gallery to see it natively.
import design.prizm.fx.rc3.Rc3TelemetryHud;
Rc3TelemetryHud()| Member | Type | Default | Description |
|---|---|---|---|
| UxvDomain | enum | — | AERIAL / GROUND / SURFACE / UNDERWATER — drives ALT / ELEV / DEPTH and V/S / DIVE labels. |
| HudMode | enum | STRIP | STRIP (inline row) or FRAME (four-edge overlay around a viewport). |
| SpeedUnit / VerticalUnit | enum | — | M_S / KM_H / KN and M / FT. |
| TelemetryField | enum | — | Keys for per-field staleness. |
| setDomain / setMode / setPlatform | → void | — | Domain, posture, and the Ember-dotted platform cell. |
| setSpeed / setVertical / setVerticalRate / setHeading / setBattery / setFuel / setRoll / setPitch / setSlope / setAltitudeAboveBottom | → void | — | Domain-tuned cells (slope is ground-only). |
| setStale | (Map<TelemetryField, Integer>) → void | — | Seconds since last fresh; the cell dims + shows a STALE tag. |
| setContent | (Node) → void | — | Centred viewport in FRAME mode. |
Honours invariant 5 — a stale field never passes for live. Battery / fuel use semantic tones; the platform marker is Ember. Mirrors components/rc3/telemetry-hud.tsx.
Usage
Set `domain` to the platform class. Pass whichever fields the platform surfaces — labels resolve automatically. Drop the platform marker when the HUD overlays a video tile that already identifies the source. For map and 3D viewports, prefer `mode="frame"` and let the edges hold the telemetry while the centre stays clear. Keep the HUD honest: when a field stops updating, mark it stale via `stale` rather than letting a frozen value look live.