---
title: Handle disconnects
description: 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.
type: guide
platform: all
vendor: meta
related:
  - /docs/concepts/sessions
  - /docs/guides/background-sessions
  - /docs/reference/errors
  - /docs/troubleshooting/glasses-wont-connect
---

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](/docs/concepts/sessions).

> Kotlin (`StateFlow`) leads the snippets and is the source of truth. iOS exposes the same model via `glasses.connection.state.stream` (an `AsyncStream`); 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:

<Callout type="warn">
  **`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](/docs/getting-started/choose-your-path).
</Callout>

```kotlin
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 — `ExtentosConnectionPage` renders the canonical pairing / connecting / connected / disconnected flow. Reach for `glasses.connection.state` directly only when you need a *custom* in-app affordance (e.g. disabling a "take photo" button while not `Active`).

## 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:

```kotlin
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 called `disconnect()`. 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.cause` carries the underlying `TransportError` (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 as `DeviceDroppedConnection`; keep the branches for forward-compat.)*
- **`SimulatorSessionExpired` / `SimulatorBrowserClosed` / `SimulatorMeterExhausted`** — only occur under `BrowserSimTransport` (`SimulatorMeterExhausted` is 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.reason` is a `SessionExpireReason` (idle timeout, lifetime cap, user-ended, backend restart).

See the [error reference](/docs/reference/errors) 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.

```kotlin
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` / `TransportFailure` with exponential backoff (e.g. 1 s, 4 s, 16 s, then give up and show a manual button). Don't auto-retry `UserRequested` or the hardware causes. Don't spin a tight retry loop — each `reconnect()` 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](/docs/guides/photo-capture#handling-failures) and [audio streaming](/docs/guides/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](/docs/guides/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](/docs/concepts/sessions) — the full connection-state machine and what persists.
- [Background sessions](/docs/guides/background-sessions) — keeping the connection alive when backgrounded.
- [Error reference](/docs/reference/errors) — `DisconnectCause`, `ConnectError`, and `TransportError` payloads.
- [Glasses won't connect](/docs/troubleshooting/glasses-wont-connect) — first-connection failures.
- `getCapabilityGuide(feature: "connection_state")` — the call shape + gotchas.
