Live metrics & controls
A connected console streams a normalized frame at about 2 Hz. This guide covers
reading it without over-rendering, the shape of the LiveMetrics frame, setting
display units, bridging heart-rate, and the stream-reset rules that make a
stale-data flash impossible.
Reading live metrics
Section titled “Reading live metrics”useLiveMetrics(deviceId) returns the whole latest frame, or null when there
is no device or no frame yet. But for tiles, always pass a selector — the
tile then re-renders only when its selected value changes, so a watts change
never re-renders the pace tile. Frames are coalesced to at most one per animation
frame inside the store, so a chatty console never storms React.
import { useLiveMetrics } from '@rogue/console-sdk';import type { DeviceId } from '@rogue/console-sdk';
function LiveTiles({ deviceId }: { deviceId: DeviceId }) { const pace = useLiveMetrics(deviceId, (m) => m.pace.current); // number | null const watts = useLiveMetrics(deviceId, (m) => m.power.watts); // isolated re-render const cals = useLiveMetrics(deviceId, (m) => m.energy.calories); return ( <Row> <Tile label="Pace" value={pace} /> <Tile label="Watts" value={watts} /> <Tile label="Calories" value={cals} /> </Row> );}The selector receives a non-null frame — the hook short-circuits the “no
device / no frame yet” case itself and returns null without calling your
selector, so you never write m?.pace. Pass a third isEqual argument for
derived object/array selections that should compare by value.
There is no startMonitoring/stopMonitoring. Monitoring is refcounted the same
way scanning is: a mounted useLiveMetrics(id) retains the device’s stream for
its lifetime, and the transport monitor runs while at least one hook (or an active
session) needs it. Out-of-session frames flow freely — a connected console streams
to any mounted tile with no workout running.
The LiveMetrics shape
Section titled “The LiveMetrics shape”Every firmware quirk is normalized at parse time, so the frame you read is clean: deciseconds arrive as fractional seconds, the rest-countdown sentinel is gone, and native bike paces ride alongside the normalized per-500m pace. The frame is a set of typed blocks:
// state: { workout: 'idle' | 'working' | 'resting' | 'finished', workoutType, round, isInRest }// time: { elapsedSeconds, splitSeconds, restRemainingSeconds, ... }// distance: { totalMeters, splitMeters, stageMeters }// pace: { current, average, segment, native: { per1000m, perMile } } // seconds/500m// power: { watts, averageWatts, segmentWatts }// cadence: { rate, averageRate, strokeCount } // SPM or RPM// energy: { calories, workoutCalories, caloriesPerHour, ... }// heartRateBpm, batteryPercent, lastSplit, extended // extended: V2+ tailpace.current and its siblings are always normalized seconds-per-500m;
pace.native carries the firmware’s own per-1000m / per-mile paces (bikes), which
match the console screen exactly. Branch on state.workout for a typed workout
state — there are no FINISH = 0 magic numbers.
Setting display units
Section titled “Setting display units”useConsoleControls(deviceId) exposes the per-device console controls. In M2 the
surface is deliberately minimal — setDisplayUnits only. It is a bare verb: it
never rejects (a command on a non-ready device is a dev-warned no-op routed to
onError), so a picker’s onChange is safe fire-and-forget. The reactive result
lands on device.displayUnits.
import { useConsoleControls } from '@rogue/console-sdk';
function UnitPicker({ device }: { device: Device }) { const { setDisplayUnits, setDisplayUnitsAsync } = useConsoleControls(device.id); return ( <Picker value={device.displayUnits} onChange={(units) => setDisplayUnits(units)} // fire-and-forget; never rejects /> );}Use the rejecting twin setDisplayUnitsAsync when you need to await the write or
handle its failure imperatively. Per-device pace-unit derivation is handled inside
the engine.
Bridging watch heart-rate
Section titled “Bridging watch heart-rate”The SDK does not own watch connectivity — your app feeds it a bpm. Mount
useHeartRateBroadcast(bpm) and while it is mounted, each fresh reading is fanned
out to every ready console (deduped and rate-limited per device) and recorded into
the active session’s heart-rate track. There is zero cleanup: unmount removes
the registration.
import { useHeartRateBroadcast } from '@rogue/console-sdk';
function HeartRateBridge() { const bpm = useWatchHeartRate(); // your app-owned watch integration useHeartRateBroadcast(bpm); // fans out to all ready consoles while mounted return null;}Pass { devices: [id] } to restrict the fan-out to a subset;
null/undefined bpm sends nothing.
Stream reset — why stale data cannot flash
Section titled “Stream reset — why stale data cannot flash”The live stream resets to null at two moments: when a session starts, and when
a completed session is dismissed. Because useLiveMetrics returns null on a
reset frame (and your selector is never called on it), a tile shows its empty
state rather than a leftover number from the previous workout — a stale-data flash
is structurally impossible, not something you guard against.