Render on the display
Render UI on the Ray-Ban Display from your smart-glasses app. Capture a photo, save it to your own store, render a browsable list of cards with the glasses.display DSL, and show one full-surface on select — driven by direct calls with no LLM, or by assistant tools. Gate every call on glasses.display.isAvailable so display-less devices fall back gracefully. Android-first; iOS port pending.
This guide builds a small media gallery on the Ray-Ban Display with no LLM at all: capture a photo, save it to your own store, render a browsable list of cards, and show one full-surface when the user pinches it. Then it points at the assistant-driven variant, where the same display DSL is driven by voice.
Read the display capability for the full DSL, the additive-light rule, and the input model. This page is the task walkthrough.
Android-first; iOS port pending; real-hardware rendering being validated. The display surface is shipped and sim-verified on Android — the iOS
DisplayClientdoesn't exist yet, and real-hardware rendering plus the mid-pinch back gesture are being validated against DAT now. On a display-less device every call is a safe no-op.
Gate on isAvailable
Display is per-device: the Ray-Ban Display has a screen, the Ray-Ban Meta doesn't. Branch on the runtime signal, never on a model name, and fall back to voice when there's no screen:
if (!glasses.display.isAvailable) {
glasses.audio.speak("Saved — you've got ${'$'}{store.all().size} photos.")
return
}
glasses.display.show { /* ... */ }show() on a display-less device is a silent no-op, so the guard is about UX, not safety — give the user a voice path. The gate is live in the simulator: switching the device model flips isAvailable mid-session with no reconnect.
Capture → store → browse (no LLM)
A self-contained gallery driven by direct display calls plus a button or voice trigger. show {} replaces the entire surface each call, so own your nav state app-side (list vs detail) and re-render whole views on every transition. Each show registers its own onBack because back is view-contextual.
import com.extentos.glasses.core.Background
import com.extentos.glasses.core.ExtentosGlasses
import com.extentos.glasses.core.TextStyle
import com.extentos.glasses.core.valueOrNull
import kotlinx.coroutines.*
// 'store' is YOUR persisted gallery (Room / DataStore / files). SavedPhoto.toPhoto()
// rebuilds a Photo the SAME way for show AND forget, so both hit the same hosted copy.
class GalleryDisplay(
private val glasses: ExtentosGlasses,
private val store: PhotoStore,
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main),
) {
// Wire this to a button onClick or a voice trigger.
fun capture() = scope.launch {
val photo = glasses.camera.capturePhoto().valueOrNull() ?: return@launch
store.add(photo)
showList()
}
// Root view: one card per saved photo.
fun showList() = scope.launch {
if (!glasses.display.isAvailable) return@launch
val items = store.all().take(5)
glasses.display.show(onBack = { scope.launch { glasses.display.clear() } }) {
column(gap = 8, padding = 12) {
text("Photos (${'$'}{store.all().size})", style = TextStyle.HEADING)
for (p in items) {
column(
onClick = { showDetail(p) },
id = "photo-${'$'}{p.id}", // stable id — gestures/agents target it
background = Background.CARD,
) {
text(p.label, style = TextStyle.BODY)
}
}
}
}
}
// Detail view: the photo full-surface; back returns to the list.
private fun showDetail(p: SavedPhoto) = scope.launch {
glasses.display.show(onBack = { showList() }) {
image(p.toPhoto()) // auto-hosts the local capture, then renders the hosted URL
}
}
// On delete, release the hosted display copy (built the SAME way as the show).
fun delete(p: SavedPhoto) = scope.launch {
store.remove(p)
glasses.display.forgetHostedImage(p.toPhoto())
showList()
}
}What's happening:
- Cards are clickable
columncontainers with a stableid("photo-" + p.id). Without an explicit id, a clickable container falls back to a positionalbox-0/box-1that shifts on reorder — coupling input routing to render order. image(photo)is root-only and full-surface. The glasses fetch onlyhttp(s)URLs, so the SDK hosts your local capture atshow()time and renders the hosted URL — you never hand-roll hosting. (A photo hosts in well under a second; the video equivalentvideo(clip)takes a few seconds.)onBackis registered pershow: the detail view'sonBackre-renders the list; the root's clears. The back gesture is the Neural Band mid-pinch on hardware.onClick/onBackare plain() -> Unit, butshow()/clear()are suspend — launch a coroutine inside the handler.forgetHostedImagetears down the hosted copy when the user deletes the photo. Rebuild thePhotothe same way forshowandforgetso both resolve to the same copy. It's idempotent and doesn't touch the local file.
Add a Back button to the detail view for discoverability alongside the gesture:
button("Back", style = ButtonStyle.OUTLINE, id = "back") { showList() }Test it in the simulator
A fresh sim session defaults to rayban_meta (no display) — set the device first, or every show is a silent no-op:
await setSimDevice({ sessionId, device: "rayban_display" });
// Read EXACTLY what's on the glasses — structured tree, no pixels, no browser needed:
const d1 = await getDisplayState({ sessionId });
// d1.tree = the rendered DisplayNode tree; d1.interactiveIds = the pinchable card ids
// Pinch a card the way the Neural Band would (needs the browser tab — ensureSimulatorBrowser):
await injectInput({ sessionId, action: "select", targetId: d1.interactiveIds[0] });
const d2 = await getDisplayState({ sessionId }); // the detail view
// Back via the gesture — mid-pinch on hardware, injected back in the sim:
await injectInput({ sessionId, action: "back" }); // onBack fires, re-renders the listgetDisplayState reads the live hub snapshot and works headless; injectInput needs the browser tab attached (it owns the rendered tree + focus). The durable trace is getEventLog(filter: "display") — display_show, display_select, display_navigate, display_back — where event order proves causality. Test both branches: rayban_display renders; setSimDevice({ device: "rayban_meta" }) flips isAvailable to false and your voice fallback runs. See the MCP tools reference.
Dead URLs show nothing. Additive light can't render a failed media fetch — the panel stays dark while the tree checks look green. The simulator shows a "video failed to load" placeholder and a
display_erroron theerrorschip; on hardware adisplay.video_errorruntime event fires so you can fall back to a text view. Check theerrorschip first when a display flow looks green but shows nothing. (Errors — there is noDisplayErrorreturn type.)
The assistant-driven variant
The same display DSL can be driven by voice instead of buttons: an assistant tool calls glasses.display.show(...), and "show my notes" / "open the groceries note" / "go back" drive the same app-side state machine the Neural Band does. One state machine, two input rails, no conflict — gate the tool on isAvailable and return the reason in ToolResult.Err so the model explains it ("These glasses have no display — want me to read it aloud?").
tool(
"show_notes",
"Show the browsable list of the user's notes on the glasses display. " +
"Also call when the user asks to go back to the list from an open note.",
) {
if (!glasses.display.isAvailable) {
return@tool ToolResult.Err("These glasses have no display. Offer to read the notes aloud instead.")
}
showBrowse()
ToolResult.Ok("Notes are on the display — slide between them, pinch one to open it.")
}For the full two-view browse ⇄ detail pattern with both rails wired, see the assistant runtime and getCodeExample(pattern: "display_browse_detail") via the MCP server.
Related
- The display capability — the full DSL, additive-light rule, hosting, and input model
- The assistant runtime — drive the display by voice from assistant tools
- Capabilities — the per-device capability model and the full SDK vocabulary
- Error reference — the no-
DisplayErrormodel and the display runtime events - Vendors: Meta Ray-Ban — the device family and the Ray-Ban Display
Related
The display capability
Render UI on the Ray-Ban Display with the glasses.display Kotlin DSL — text, images, buttons, media, and Neural Band input. Gated per device; Android-first.
The assistant runtime
Build a voice assistant on smart glasses with glasses.assistant — wake/sleep, tools, vision, barge-in, memory, on the managed AI gateway. Phase-4 preview.
Capabilities
The Extentos capability vocabulary — the vendor-agnostic SDK primitives (audio, camera, voice, assistant, display, hardware events) your handler subscribes to.
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.
Meta Ray-Ban (Meta DAT)
Complete Meta Ray-Ban developer guide — hardware tiers (Ray-Ban Meta, Ray-Ban Meta Gen 2, Ray-Ban Meta Display), the Meta Device Access Toolkit (DAT) public capability matrix, distribution and development state, what you can build and ship today vs what's still gated behind Meta partnerships, and how Extentos abstracts the toolkit so the same code runs against the simulator and real glasses.
Audio streaming
Stream raw mic audio off Meta Ray-Ban with audioChunks, get continuous STT with transcriptions(), and play TTS with speak(). Covers A2DP/HFP coexistence.
Hardware events
What Meta Ray-Ban surfaces today via glasses.runtime.events, connection.state, and toggles — and which hardware-alert streams aren't exposed yet. No IMU API.