Guides

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

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:

StatePayloadMeaning
NotRegisteredThe install hasn't registered/authenticated yet — show a pairing prompt.
RegisteredRegistered, scanning for a known device.
DeviceDiscovereddeviceId: StringA device was found; connection is about to start.
ConnectingdeviceId: StringHandshake in progress.
Activestate: ActiveStateConnected and usable. Capability calls work here.
Disconnectedcause: DisconnectCauseThe 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:

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 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 / 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 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.

  • Sessions — the full connection-state machine and what persists.
  • Background sessions — keeping the connection alive when backgrounded.
  • Error referenceDisconnectCause, ConnectError, and TransportError payloads.
  • Glasses won't connect — first-connection failures.
  • getCapabilityGuide(feature: "connection_state") — the call shape + gotchas.