Handle disconnects
Observe glasses.connection.state, branch on the typed Disconnected.cause to tell a user-pulled-the-glasses-off from a Bluetooth drop, and reconnect cleanly. The connection-state spine for any Meta Ray-Ban app.
Glasses disconnect: the wearer takes them off, walks out of Bluetooth range, the battery dies, or — in the simulator — the session expires. A robust app observes glasses.connection.state, reacts to why the connection dropped (not just that it did), and offers the right recovery. This page is the connection-state spine; the full state machine is documented in Sessions.
Kotlin (
StateFlow) leads the snippets and is the source of truth. iOS exposes the same model viaglasses.connection.state.stream(anAsyncStream); the state and cause types are identical.
Observe the connection state
glasses.connection.state is a StateFlow<GlassesState>. GlassesState is a sealed type with six buckets — collect it and branch:
Active means the transport is up, not that glasses are present. On the
vendorless audio baseline it is reported whenever audio is routable — and the
phone always has a mic and a speaker. A "connected to your glasses" indicator
built on this state alone will claim glasses that aren't there. Pair it with
glasses.capabilities for anything hardware-specific. See
choose your path.
import com.extentos.glasses.core.GlassesState
@Composable
fun GlassesGate(glasses: ExtentosGlasses) {
val state by glasses.connection.state.collectAsState()
when (state) {
GlassesState.NotRegistered -> PairingPromptView()
GlassesState.Registered -> Text("Looking for your glasses…")
is GlassesState.DeviceDiscovered -> Text("Found them — connecting…")
is GlassesState.Connecting -> Text("Connecting…")
is GlassesState.Active -> ConnectedView()
is GlassesState.Disconnected -> DisconnectedView(cause = (state as GlassesState.Disconnected).cause)
}
}The six buckets, in lifecycle order:
| State | Payload | Meaning |
|---|---|---|
NotRegistered | — | The install hasn't registered/authenticated yet — show a pairing prompt. |
Registered | — | Registered, scanning for a known device. |
DeviceDiscovered | deviceId: String | A device was found; connection is about to start. |
Connecting | deviceId: String | Handshake in progress. |
Active | state: ActiveState | Connected and usable. Capability calls work here. |
Disconnected | cause: DisconnectCause | The connection ended — cause tells you why. |
You usually don't need to build any of this UI yourself —
ExtentosConnectionPagerenders the canonical pairing / connecting / connected / disconnected flow. Reach forglasses.connection.statedirectly only when you need a custom in-app affordance (e.g. disabling a "take photo" button while notActive).
Branch on the disconnect cause
Disconnected.cause is a DisconnectCause — a sealed type that tells you whether the drop is the user's intent, a hardware condition, a transient transport blip, or a simulator-session event. Branch on it to give the right message and the right recovery:
import com.extentos.glasses.core.DisconnectCause
@Composable
fun DisconnectedView(cause: DisconnectCause) = when (cause) {
// User intent — they asked to disconnect. No auto-retry; show a "Connect" button.
DisconnectCause.UserRequested ->
Banner("Disconnected.", action = "Connect" to ::reconnect)
// Hardware conditions — retrying won't help until the condition clears.
DisconnectCause.ThermalCritical ->
Banner("Glasses got too hot and disconnected. Let them cool, then reconnect.")
DisconnectCause.HingesClosed ->
Banner("Glasses were folded. Open them to reconnect.")
// Transport blip — Bluetooth dropped / out of range. Retryable; offer (or auto-) reconnect.
DisconnectCause.DeviceDroppedConnection ->
Banner("Lost the glasses — they may be out of range.", action = "Retry" to ::reconnect)
is DisconnectCause.TransportFailure ->
Banner("Connection error.", action = "Retry" to ::reconnect) // cause.cause is the TransportError
// Simulator-only causes — only seen in the browser simulator, never on real hardware.
is DisconnectCause.SimulatorSessionExpired ->
Banner("Simulator session expired (${cause.reason}). Mint a new one.")
is DisconnectCause.SimulatorBrowserClosed ->
Banner("Simulator browser tab closed.")
is DisconnectCause.SimulatorMeterExhausted ->
Banner("Simulator session limit reached.")
}Grouped by how you should treat them:
UserRequested— the app or user calleddisconnect(). Don't auto-reconnect; show a manual "Connect" affordance.DeviceDroppedConnection/TransportFailure— treat as retryable. The glasses went out of range or the link failed;TransportFailure.causecarries the underlyingTransportError(e.g.WebsocketClosed,Timeout) if you want to log specifics. Offer a retry or reconnect automatically with backoff.ThermalCritical/HingesClosed— hardware conditions. Retrying immediately is pointless; tell the user what to do (let them cool / unfold) and reconnect once they act. (Declared, not yet produced by the core — today these drops arrive asDeviceDroppedConnection; keep the branches for forward-compat.)SimulatorSessionExpired/SimulatorBrowserClosed/SimulatorMeterExhausted— only occur underBrowserSimTransport(SimulatorMeterExhaustedis reserved — never emitted today). You won't see these on real glasses; handle them so your dev builds degrade gracefully (mint a new session).SimulatorSessionExpired.reasonis aSessionExpireReason(idle timeout, lifetime cap, user-ended, backend restart).
See the error reference for the full DisconnectCause and TransportError payload shapes.
Reconnect
To recover, call glasses.connection.reconnect(). It disconnects, then connects again, re-running registration and re-fetching device metadata (name, firmware, link state) — so a stuck "looking for glasses" recovers, and changes made elsewhere (e.g. renaming the glasses in the Meta AI app) get picked up. It's serialized: overlapping calls run one at a time.
import com.extentos.glasses.core.ConnectError
import com.extentos.glasses.core.ExtentosResult
suspend fun reconnect(glasses: ExtentosGlasses) {
when (val r = glasses.connection.reconnect()) {
is ExtentosResult.Ok -> { /* state flips to Active via the StateFlow */ }
is ExtentosResult.Err -> when (r.error) {
is ConnectError.NoDeviceAvailable -> prompt("No glasses found — make sure they're nearby and on.")
is ConnectError.PermissionDenied -> requestBluetoothPermissions()
is ConnectError.PairingFailed -> prompt("Pairing failed — try again.")
// The SDK drives Meta registration itself via your activityProvider —
// there is no registration call to make. Surface it and retry.
is ConnectError.RegistrationRequired -> prompt("Approve the connection in the Meta AI app, then try again.")
is ConnectError.TransportFailure -> prompt("Couldn't connect — try again.")
}
}
}connect() and reconnect() return ExtentosResult<Unit, ConnectError> — pattern-match, don't runCatching. (disconnect() returns Unit; it always succeeds.) reconnect() backs the connection page's pull-to-refresh; call it from your own UI control too. You don't need to read the return value to update UI — the state StateFlow is the source of truth and flips to Active (or back to Disconnected) on its own.
A retry policy that won't thrash: auto-reconnect once on
DeviceDroppedConnection/TransportFailurewith exponential backoff (e.g. 1 s, 4 s, 16 s, then give up and show a manual button). Don't auto-retryUserRequestedor the hardware causes. Don't spin a tight retry loop — eachreconnect()re-runs registration.
In-flight operations
A disconnect mid-capture surfaces as the operation's own typed failure, not just a state change: an in-flight capturePhoto() returns CaptureError.NotConnected, recordDiscrete() returns AudioError.NotConnected. Handle those at the call site (see photo capture and audio streaming); they fire independently of the state observer, so a robust app handles both — the per-call error for the immediate operation, the state observer for the UI.
Background and lifecycle
Whether a session survives app-backgrounding is a separate concern — see Background sessions for the Android foreground-service and iOS background-mode setup that keeps the connection alive while your app isn't foregrounded.
Related
- Sessions — the full connection-state machine and what persists.
- Background sessions — keeping the connection alive when backgrounded.
- Error reference —
DisconnectCause,ConnectError, andTransportErrorpayloads. - Glasses won't connect — first-connection failures.
getCapabilityGuide(feature: "connection_state")— the call shape + gotchas.
Related
Sessions
How an Extentos session works — the glasses connection-state machine, what persists across backgrounding and reconnects, and the three browser-simulator roles.
Background sessions
Keep a Meta Ray-Ban session alive when your app backgrounds — the Android foreground service for mic, iOS background modes, and which capabilities survive.
Error reference
Every typed error the Extentos SDK can return — ConnectError, CaptureError, AudioError, TransportError, the ExtentosError umbrella, and the Meta-DAT DeviceSessionError — with their payload fields and meaning. Lifecycle operations return ExtentosResult<T, E> with these concrete failure variants rather than throwing; pattern-match them. Generated from the Rust core.
Glasses won't connect
Fix common first-connection failures on Meta Ray-Ban — no pairing dialog, session won't start, the DAT auth callback never fires, or an immediate disconnect.
Background sessions
Keep a Meta Ray-Ban session alive when your app backgrounds — the Android foreground service for mic, iOS background modes, and which capabilities survive.
From simulator to real glasses
Move a Meta Ray-Ban app from the browser simulator to real glasses — Meta identity, credentials, DAT resolution, the transport switch, and hardware checks.