Your first connection
This tutorial takes you from an empty app to a live metric on screen in about fifteen minutes. You will mount the provider, gate on Bluetooth, scan for a console, connect to it, and render one number streaming off the hardware.
1. Mount the provider
Section titled “1. Mount the provider”Everything below assumes one <ConsoleProvider> above your tree. If you have not
done this yet, start with Provider setup.
import { ConsoleProvider } from '@rogue/console-sdk';
export default function App() { return ( <ConsoleProvider> <FirstConnection /> </ConsoleProvider> );}2. Gate on Bluetooth
Section titled “2. Gate on Bluetooth”Before scanning, make sure the radio is on and you have permission. Two hooks
give you both as reactive state — no platform branching, no PermissionsAndroid.
import { useBluetoothState, useBlePermissions } from '@rogue/console-sdk';
function BluetoothGate({ children }: { children: React.ReactNode }) { const bluetooth = useBluetoothState(); const [permissions, requestPermissions] = useBlePermissions();
if (permissions.status === 'undetermined') { return <Button title="Enable Bluetooth access" onPress={requestPermissions} />; } if (permissions.status === 'denied') { return <Text>Bluetooth permission is required to find your console.</Text>; } if (bluetooth === 'off') { return <Text>Turn on Bluetooth to continue.</Text>; } if (bluetooth !== 'on') { return <Text>Waiting for Bluetooth…</Text>; } return <>{children}</>;}useBluetoothState() is 'unknown' until the radio reports in, then one of
'unsupported' | 'unauthorized' | 'off' | 'on'. useBlePermissions() returns a
[status, request] tuple in the Expo-camera convention; request() prompts once
and never rejects.
3. Scan, and render the one device list
Section titled “3. Scan, and render the one device list”Scanning is declarative: mounting useDeviceScan() starts a scan and
unmounting stops it — there is nothing to start or stop by hand. Results do not
arrive in a callback; they land on the one unified device list you read with
useDevices(). Filter to { available: true } for “nearby right now”.
import { useDeviceScan, useDevices, useConnectDevice } from '@rogue/console-sdk';
function ConsolePicker() { useDeviceScan(); // scans while mounted, stops on unmount const available = useDevices({ available: true }); const { connect, pendingIds } = useConnectDevice();
if (available.length === 0) { return <Text>Searching for a console…</Text>; } return ( <FlatList data={available} keyExtractor={(device) => device.id} renderItem={({ item }) => ( <DeviceRow name={item.displayName} consoleId={item.consoleId} busy={pendingIds.includes(item.id)} onPress={() => connect(item)} /> )} /> );}useDevices() returns Device[] from the very first render — before any
sighting it is [], so mapping is always safe. Each Device carries a
displayName (“Echo Bike”), a user-visible consoleId, and its live
presence/connection facets.
4. Connect
Section titled “4. Connect”connect(target) accepts the whole Device row you already have. It never
rejects — on failure the typed error lands in errors, keyed by device — so an
onPress handler can never float an unhandled rejection. The row’s own
device.connection.status === 'connecting' is the canonical per-row spinner; the
pendingIds list above exists for screens that do not have the row at hand.
5. Render one live metric
Section titled “5. Render one live metric”Once a device is connected, useLiveMetrics(deviceId, selector) streams frames.
Pass a selector so a change to one field only re-renders the tile bound to it —
a watts change never re-renders the pace tile. The selector receives a non-null
frame; the hook returns null on its own when there is no device or no frame
yet, so the ?. dance is gone.
import { useLiveMetrics } from '@rogue/console-sdk';import type { DeviceId } from '@rogue/console-sdk';
function PaceTile({ deviceId }: { deviceId: DeviceId }) { const pace = useLiveMetrics(deviceId, (m) => m.pace.current); // number | null return <Text>{pace === null ? '—' : formatPace(pace)}</Text>;}That is the whole path: gate, scan, connect, read. Everything after this is more of the same shape.
You connected a console — now connect two
Section titled “You connected a console — now connect two”Nothing above targets “the console”. useDevices() is a list, connect(item)
takes a specific device, and useLiveMetrics(deviceId) is keyed by id. Connect a
second console and this exact code renders both — no API changes, no
single-vs-multi split. That is the whole model; see
The fleet model for why.