Skip to content

Connection lifecycle

Every device carries its connection lifecycle as data on device.connection. You never subscribe to connection events — there is no onDisconnect anywhere in the SDK. You read device.connection and branch on status. This page is the map of that state machine.

Per-device connection state machine disconnected transitions to connecting on connect; connecting reaches ready on setup success, or returns to disconnected on timeout or failure. ready drops to reconnecting on a link loss while wanted, holds in updating during an OTA transfer, and returns to disconnected on a deliberate disconnect. reconnecting returns to ready on recovery or to disconnected when attempts are exhausted. disconnected cause · error connecting via · step · startedAt ready via · since · link reconnecting attempt · nextAttemptAt updating since · progress connect() setup ok timeout · failed link lost (wanted) recovered OTA start done disconnect() attempts exhausted
The per-device connection state machine. ready is the working state; reconnecting and updating are degraded, not gone.

Each status carries exactly the data that state needs:

type ConnectionState =
| { status: 'disconnected';
cause: 'never-connected' | 'user' | 'link-lost' | 'bluetooth-off' | 'timeout' | 'failed';
error: ConsoleSdkError | null }
| { status: 'connecting'; via: 'user' | 'auto-reconnect'; startedAt: number;
step: 'link' | 'negotiating' | 'identifying' | 'clock-sync' }
| { status: 'ready'; via: 'user' | 'auto-reconnect'; since: number;
link: 'healthy' | 'unresponsive' }
| { status: 'updating'; since: number; progress: number }
| { status: 'reconnecting'; since: number; attempt: number;
nextAttemptAt: number; lastError: ConsoleSdkError | null };

Read it with an exhaustive switch:

switch (device.connection.status) {
case 'disconnected': /* cause tells you why; never-connected is the fresh state */ break;
case 'connecting': /* step reports setup progress */ break;
case 'ready': /* link === 'unresponsive' badges a degraded-but-alive link */ break;
case 'updating': /* progress 0..1 during an OTA transfer */ break;
case 'reconnecting': /* attempt / nextAttemptAt for a countdown */ break;
}

Two states are degraded, not gone — and treating them as “disconnected” is a classic bug this model prevents.

  • reconnecting means the link dropped while the device was still wanted (auto-reconnect policy, or a drop mid-workout). It is bounded retries with a visible attempt and nextAttemptAt, not a terminal state. A workout keeps its device through a blip; a tile shows “reconnecting”, not an empty slot.
  • ready with link: 'unresponsive' is a degraded health-check reading — the 2-second health check has not heard back, but commands remain callable and may recover the link. Badge it; do not tear the session down.

The device.isConnected convenience is true for ready, reconnecting, and updating — precisely because a session survives all three.

connecting and ready carry via: 'user' | 'auto-reconnect'. This is how the reconnect toast is derived from state rather than stored as an event: a ready connection whose via is 'auto-reconnect' and that you have not acknowledged is, by definition, a recent reconnection. useAutoReconnect() reads exactly that — see Fleet & auto-reconnect. There is no “reconnected” event to catch and no listener to register.

disconnected is the one cause-carrying terminal state. Its cause distinguishes a fresh device ('never-connected'), a deliberate disconnect() ('user', which also suppresses auto-reconnect for the session), an exhausted reconnect ('link-lost'), a radio-off drop ('bluetooth-off'), and setup failures ('timeout' / 'failed'); error carries the typed failure when one applies.

updating is fleet-visible so every screen renders a device consistently during an OTA transfer. Workout arming rejects with command/busy while a device is updating.

When the provider unmounts (or its config changes), the runtime runs an ordered teardown: connected devices are gracefully detached, any session is abandoned, and the transport disposes — in that order. Connection lifetime is provider lifetime. You never write cleanup for this; unmounting the provider is the disposal path (see Architecture).