Skip to content

The fleet model

Multi-console support is not a feature of this SDK — it is the shape of the SDK. There is one set of primitives, and they are fleet-shaped. Connecting a single console is simply the case where the fleet has one member. This page explains why that is the model and what it means for your code.

Discovered, saved, and connected are facets of one Device record, not three lists to reconcile. A device you just saw in a scan, a device you saved last week, and a device you are connected to right now are all the same type — the difference is the value of its presence, saved, and connection fields.

That is why a connect screen renders a single useDevices() array with no stitching: “nearby but not saved” is a filter over one type (useDevices({ available: true, saved: false })), not a join across two lists. The old world had a discovered list and a fleet list and hand-reconciled them on every render; here there is nothing to reconcile.

Because there are N devices, every per-device operation names its device explicitly by DeviceId:

useLiveMetrics(deviceId); // this device's frames
useConsoleControls(deviceId); // this device's controls
useDeviceActions(deviceId); // this device's actions
useConnectDevice().connect(target); // connect a specific device

There is no ambient “the console” these operate on. useDevices() returns the list; you pass a specific id to each per-device hook. This is the whole multi-console API — there is no second set of hooks for “multi” mode, because there was never a “single” mode to contrast it with.

The runtime has no “current device” field. There is no active-device global, no pin, no data-driven “switch to whichever console started a workout” heuristic. Those mechanisms existed only to guess which console a single-console API should talk to — and every one of them was a source of bugs (revision-counter hacks, competing streams, pin/unpin gymnastics).

Selecting which device a screen shows is app UI, not SDK state. Your app holds the selected DeviceId in its own state (route param, local state, wherever it belongs) and passes it to the per-device hooks. The SDK does not need to know, and keeping that decision out of the runtime is what makes the absence of an active-device field structural rather than a convention that erodes.

A “primary console” heuristic — auto-selecting one device as the default target — was considered and deliberately rejected. Any such rule (most-recently-connected, strongest-signal, first-in-workout) is right in some flows and wrong in others, and the moment it is wrong the consumer needs an override, which reintroduces the explicit-id API anyway — now alongside a heuristic that fights it. The explicit-id model is the override with no heuristic to fight. Choosing which device is “primary” is a product decision your app makes with its own UI, on top of the unified list.

A single-console app writes the same code a fleet app writes. It calls useDevices() (which happens to return one device), passes that device’s id to the per-device hooks, and renders one set of tiles. Connecting a second console changes zero lines: the same list now has two entries, the same session.devices.map(...) renders two tiles, and the same per-device hooks key on two ids.

The one concession to single-console ergonomics is useDevice(query) — one-line sugar for useDevices(query)[0] ?? null. It is the N=1 convenience over the fleet-shaped model, sharing its exact types, state, and lifecycle — not a separate single-console API.

const rower = useDevice({ modality: 'rower', connected: true }); // Device | null

That is the entire story: one model, keyed by id, with N=1 as its smallest case.