Skip to content

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.

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>
);
}

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.

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.

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.

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.