Host integration
Everything an app needs to plug the SDK into its own infrastructure enters
through config, once — not through hook arguments, not through a callback bag,
not through a store swap. This guide covers the five host-integration seams:
telemetry, error reporting, diagnostics, athlete parameters, and the imperative
useConsoleClient escape hatch. All of them are narrow, typed, and optional; zero
config is still a working config.
Telemetry: a typed event sink
Section titled “Telemetry: a typed event sink”config.telemetry is a host-owned sink the SDK calls out to — construction-time
dependency injection with no add/remove lifecycle and no subscription. It has two
optional methods: event(e) receives the typed SdkTelemetryEvent union, and
log(level, message, context?) receives diagnostic lines for your file/remote
logger.
import { createConsoleConfig } from "@rogue/console-sdk";import type { SdkTelemetryEvent } from "@rogue/console-sdk";
const config = createConsoleConfig({ telemetry: { event(e: SdkTelemetryEvent) { switch (e.type) { case "connect.succeeded": analytics.track("console_connected", { model: e.model, ms: e.durationMs, }); break; case "session.completed": analytics.track("workout_done", { seconds: e.durationSeconds, aborted: e.aborted, }); break; // connect.failed | device.disconnected | session.started // | capture.gap | firmware.update — all typed, never stringly. } }, log(level, message, context) { remoteLogger.write(level, message, context); }, },});SdkTelemetryEvent is an exhaustive, never-stringly-typed union, versioned
additively — a switch over e.type narrows each variant’s fields. Because the
sink is method-invoked, a this-bound sink (a ring-buffer class instance, say)
works unchanged. A sink that throws is contained by the runtime’s reporter, so
a bug in your telemetry can never break the SDK’s control flow.
Error reporting: onError, once per failure
Section titled “Error reporting: onError, once per failure”config.onError(error, context) is the cross-cutting failure sink — for a global
toast, crash reporting, or an error-rate metric. It fires exactly once per
failure (once per error instance, not once per subscribed component), and it
complements the state channels rather than replacing them: a failure that also
lands on a hook’s error field or a session snapshot still reports here once.
import { createConsoleConfig } from "@rogue/console-sdk";
const config = createConsoleConfig({ onError: (error, context) => { crashReporter.record(error, { code: error.code, // the namespaced ConsoleSdkErrorCode domain: context.domain, // 'connect' | 'command' | 'session' | 'firmware' | … deviceId: error.deviceId ?? context.deviceId, sessionId: context.sessionId, }); },});The second argument, ErrorContext, says where the failure originated (domain,
plus deviceId / sessionId when applicable). Two documented non-failures are
deliberately excluded — a cancelled connect (connect/cancelled) and firmware
cancel() never reach onError, because cancelling is not a failure (see
error handling).
Diagnostics: raw packets for a dev screen
Section titled “Diagnostics: raw packets for a dev screen”For a hardware-debugging screen, opt into raw-packet capture. It is off by default and never writes files:
import { createConsoleConfig } from "@rogue/console-sdk";import type { RawPacketInfo } from "@rogue/console-sdk";
const config = createConsoleConfig({ diagnostics: { rawPackets: true, onRawPacket: (packet: RawPacketInfo) => devLogFile.append(packet.hex), // optional log tap },});With rawPackets: true, a bounded in-memory ring backs useDiagnostics() —
the reactive reader for a diagnostics screen:
import { useDiagnostics } from "@rogue/console-sdk";
function DiagnosticsScreen() { const { packets, parseSuccessRate, clear } = useDiagnostics({ capacity: 100, }); return ( <> <Header rate={parseSuccessRate} onClear={clear} /> {packets.map((p) => ( <PacketRow key={p.at} dir={p.direction} hex={p.hex} parsed={p.parsed} /> ))} </> );}Each RawPacketInfo carries a timestamp, the deviceId, a direction
('rx'/'tx'), the bytes as base64 and hex, and a parsed outcome (the decoded
command name, or why decoding failed). parseSuccessRate is a well-defined 0
when the ring is empty — never NaN. When rawPackets is off, useDiagnostics()
returns a stable empty state with a no-op clear(), so a dev screen renders
harmlessly in production without a guard.
Athlete parameters: what the console needs
Section titled “Athlete parameters: what the console needs”config.athlete carries the handful of athlete facts the console firmware
uses — chiefly weight, which drives its calorie computation — plus a default
display-unit preference. It is pushed to consoles on connect. There are no
defaults: when the block is absent, nothing is pushed.
import { createConsoleConfig } from "@rogue/console-sdk";
const config = createConsoleConfig({ athlete: { weightKg: 82, // feeds the console's calorie model heightCm: 181, sex: "male", units: { pace: "per500m", distance: "metric" }, // default display units pushed on connect },});This is the console-facing remnant of a profile — not profile management. Forms, persistence, and any server round-trip for the athlete’s profile stay in your app; the SDK only forwards the parameters the hardware consumes. Because a config change is a clean recreate, updating athlete params means building a new config object (see provider setup).
useConsoleClient: the imperative escape hatch
Section titled “useConsoleClient: the imperative escape hatch”Hooks are the primary surface, but some call sites are not React function
components — a class component, a non-React utility, an NFC/QR entry point that
fires before any screen mounts. useConsoleClient() resolves the current
provider’s ConsoleClient: snapshot reads plus rejecting commands, for
exactly those cases.
import { useConsoleClient } from "@rogue/console-sdk";
function useDeepLinkConnect() { const client = useConsoleClient(); return async (consoleId: string) => { // Snapshot read — point-in-time, NOT reactive. const already = client.devices.list({ connected: true }); if (already.length > 0) return already[0]; // Rejecting command — imperative callers want exceptions, not mirrored state. return client.devices.connect({ consoleId }); };}The client has two surfaces: client.devices (list(query?) / get(id) snapshot
reads; connect / disconnect / forget / controls(id) rejecting commands) and
client.session (current() snapshot; start / end rejecting cores).
The contract, stated plainly:
- It is not a singleton. Each provider assembles its own client, stable for
that provider’s lifetime.
useConsoleClient()returns the same object every render. - Reads are point-in-time, not reactive.
client.devices.list()andclient.session.current()read the current snapshot once — they do not subscribe. For live updates useuseDevices/useWorkoutSession. The client deliberately has no subscribe surface. - Commands reject. Unlike the hooks’ never-rejecting bare verbs,
client.devices.connect()andclient.session.start()throw the typed error on failure — because imperative callers want atry/catch, not a state field to poll.
Reach for it only when a hook won’t fit; if you are in a function component, prefer the hooks.
One entry point, no threading
Section titled “One entry point, no threading”Every seam above is set once on the config and never re-passed through hook
arguments, navigation callbacks, or a redux bridge. That is the whole
host-integration story: narrow typed config in, no lifecycle to manage, no
readiness flag to poll (useConsoleSdk().status is reactive), and provider unmount
is disposal. Cross-cutting host concerns enter here and nowhere else.
Reference
Section titled “Reference”ConsoleConfigOptions·createConsoleConfig·TelemetrySink·SdkTelemetryEvent·TelemetryLogLevelErrorContext·RawPacketInfouseDiagnostics·DiagnosticsOptions·DiagnosticsResultuseConsoleClient·ConsoleClient·ConsoleClientDevices·ConsoleClientSession- Related: Error handling · Provider setup · Architecture.