Units & formatting
@rogue/console-sdk/units is a separate import path holding pure functions:
pace/distance/power/calorie conversions and display formatting. It carries zero
React and zero React Native imports, so it runs anywhere — a server, a
non-RN script, a test — and it is fully tree-shakeable: import one function and a
bundler drops the rest.
Its defining property is firmware parity. These functions deliberately reproduce the console firmware’s own math, including its truncation quirks and its rounded constants, so a value you compute or format matches what the physical console screen shows to the digit. Parity is a correctness requirement here, not an approximation — a coach comparing your app to the console must see the same number.
Formatting a pace
Section titled “Formatting a pace”The workhorse is formatPace. The SDK’s canonical pace is always
seconds-per-500m (that is what LiveMetrics.pace.current and every result carry);
formatPace converts to the requested unit and renders the console’s M:SS /
M:SS.t string:
import { formatPace } from "@rogue/console-sdk/units";
formatPace(135); // "2:15.0" — default per500m, tenths shownformatPace(135, "per1000m"); // "4:30" — whole secondsformatPace(135, "perMile"); // firmware-parity /mile renderingformatPace(0); // "--:--" — a zero/absent paceThe whole-seconds display truncates (floors) rather than rounds, exactly like
the console. Tenths are shown only for per500m; the derived per1000m and
perMile renderings show whole seconds, mirroring how the console labels those
units.
Formatting a duration
Section titled “Formatting a duration”formatDuration is the plain clock string that pairs with formatPace for
workout headers, elapsed timers and split rows — M:SS, or H:MM:SS once it
reaches an hour. Unlike pace it carries no firmware-parity quirk (it’s plain
clock arithmetic), so it’s locale-neutral and floors fractional/negative input.
import { formatDuration } from "@rogue/console-sdk/units";
formatDuration(65); // "1:05"formatDuration(3661); // "1:01:01"formatDuration(0); // "0:00"For a smoothly ticking elapsed header, don’t render a session field on a timer
— derive it: repaint on your own ~500 ms interval and compute
formatDuration(elapsedSecondsAt(session.timing, Date.now())). The SDK exposes
elapsed as a value (elapsedSecondsAt), never a per-second stream, so the tick
cadence is yours to choose.
Driving the unit from device.displayUnits
Section titled “Driving the unit from device.displayUnits”The PaceUnit argument is the SDK’s own DisplayUnits['pace'] union, so a
device’s current display setting flows straight through — no free strings, no
lookup table:
import { formatPace } from "@rogue/console-sdk/units";import { useLiveMetrics } from "@rogue/console-sdk";import type { Device } from "@rogue/console-sdk";
function PaceTile({ device }: { device: Device }) { const pace500 = useLiveMetrics(device.id, (m) => m.pace.current); // seconds/500m | null return <Big>{formatPace(pace500 ?? 0, device.displayUnits.pace)}</Big>;}device.displayUnits.pace is set by
useConsoleControls().setDisplayUnits; pass
it to formatPace and the rendered pace tracks whatever unit the console is
showing.
The conversions
Section titled “The conversions”Every conversion is a plain function with the firmware quirk baked in where one exists:
| Function | Converts | Quirk preserved |
|---|---|---|
metersToMiles |
metres → miles | +0.0005 display bias, then two-decimal truncation |
metersToKilometers |
metres → km | none (plain ratio) |
pace500ToMile |
s/500m → s/mile | integer-millisecond truncation, twice |
pace500To1000m |
s/500m → s/1000m | none (exactly double) |
computePace500 |
distance + time → s/500m | 0 when undefined |
computePace1000 |
distance + time → s/1000m | 0 when undefined |
import { metersToMiles, pace500ToMile } from "@rogue/console-sdk/units";
metersToMiles(1609.344); // 1pace500ToMile(120); // firmware-parity seconds per mile at a 2:00/500m pacePower, calories, and cadence — modality-branching
Section titled “Power, calories, and cadence — modality-branching”The derived-metric math branches exactly as the firmware does. Bikes compute
power and calories from a calorie counter and a watt-tiered inefficiency factor;
rowers, skiers, and rhino (the pace-driven machines) derive power from the
2.8·v³ pace curve. An unknown/null modality falls back to the pace-driven
path, matching the firmware default.
You rarely call the individual formulas — computeRecordSplitMetrics does the
branching for you from a raw split record:
import { computeRecordSplitMetrics } from "@rogue/console-sdk/units";
const derived = computeRecordSplitMetrics({ distanceMeters: 500, durationSeconds: 120, strokeCount: 48, modality: "rower", // omitted/null → pace-driven path calories: 42, // only used on the bike path});// derived: { pace500, avgPower, avgCalPerHour, spm }isBikeModality(modality) is the predicate behind the branch if you need it
directly. The cadence helpers (computeSpm, computeBikeRpm) reproduce the
firmware’s decisecond / 100 ms duration truncation, so a stroke rate matches the
console even mid-second.
Firmware-parity doctrine: the deliberate quirks
Section titled “Firmware-parity doctrine: the deliberate quirks”The constants and truncations are specification, not bugs to fix. Two examples that look wrong until you know why:
MILE_TO_KM_FACTOR = 0.6215is the firmware’s rounded float baked intoapp_motion_pace1000m_to_MILE(). The mathematically exact value is0.62137…, and using it would drift the app’s per-mile pace away from the console screen. The SDK ships the firmware’s number on purpose.metersToMilesadds+0.0005then truncates to two decimals — the firmware’s(uint32_t)(mile * 100) / 100.0fdisplay cast. Rounding “cleanly” would show a different distance than the console.
Every such constant is exported and documented as spec, so the doctrine is visible
rather than hidden inside a helper:
MILE_TO_KM_FACTOR,
METERS_PER_MILE,
ROWER_POWER_COEFFICIENT,
BIKE_CAL_BASE_COEFFICIENT, and
the calorie/BMR constants. If you ever need to explain why the app and the console
agree to the third decimal, this is the answer: they run the same math.
Why a separate subpath
Section titled “Why a separate subpath”/units is isolated behind its own import path for two reasons. First, purity:
with no React or RN in its dependency graph, the same firmware-parity math runs in
a Node script that post-processes a WorkoutResult, a server that renders a
summary, or a test — none of which can (or should) load the RN transport. Second,
tree-shaking: nothing from /units is re-exported at the SDK root, so a UI
that only needs formatPace pulls in formatPace and its transitive constants,
and nothing else.
Reference
Section titled “Reference”formatPace·formatPaceTime·PaceUnit·DisplayUnitscomputeRecordSplitMetrics·DerivedSplitMetrics·isBikeModality·MetricsModalitymetersToMiles·metersToKilometers·pace500ToMile·pace500To1000m·computePace500·computePace1000computeAvgPower·computeCalPerHour·computeBikeAvgPower·computeBikeCalPerHour·computeSpm·computeBikeRpmMILE_TO_KM_FACTOR·METERS_PER_MILE·METERS_PER_KILOMETER·ROWER_POWER_COEFFICIENT·ROWER_CAL_HR_BMR_OFFSET·JOULES_PER_KILOCALORIE·BIKE_CAL_BASE_COEFFICIENT- Related: Live metrics & controls · Reading results.